12

I am calling a function from base in my code and I want to mock this function in my testthat unit test.

How can I do this?

library(testthat)

my.func <- function() {
  return(Sys.info()["sysname"])   # e. g. "Linux"
}

my.func()
# sysname 
# "Linux" 

test_that("base function can be mocked",
  with_mock(
    Sys.info = function() return(list(sysname = "Clever OS")),  # see edit 2 !!!
    expect_equal(my.func(), "Clever OS", fixed = TRUE)
  )
)
# Error: Test failed: 'base function can be mocked'
# * my.func() not equal to "Clever OS".

?with_mock says:

Functions in base packages cannot be mocked, but this can be worked around easily by defining a wrapper function.

I could encapsulate the base function call to Sys.info() by a wrapper function that I call from my.func then but let's assume I cannot do this because I am testing a function from a package that I cannot change...

Any solution for this?

I am using R3.4.4 64 Bit on Ubuntu 14.04 with testthat 2.0.0.9000.

Edit 1:

Using

`base::Sys.info` = function() return(list(sysname = "Clever OS"))

results in the testthat error msg:

Can't mock functions in base packages (base)

Edit 2: As @suren show in his answer my code example here is wrong (the mocked function returns another class then the original one :-(

The correct mock function should be: Sys.info = function() return(c(sysname = "Clever OS"))

R Yoda
  • 8,358
  • 2
  • 50
  • 87

2 Answers2

6

The error message is my.func() not equal to "Clever OS".. The reason is that Sys.info returns a named character vector while your mocking function a list.

Just modify the mocking function and the expected value and it works:

test_that("base function can be mocked",
  with_mock(
    Sys.info = function() return(c(sysname = "Clever OS")),
    expect_equal(my.func(), c(sysname = "Clever OS"), fixed = TRUE)
  )
)

This works even within a package.

Note: Mocking of base functions shouldn't work according to the help of with_mock but it does (at the moment at least).

The following (my.func()$`sysname`) seems to pass the test with the original code from the question.

test_that("base function can be mocked",
          with_mock(
            Sys.info = function() return(list(sysname = "Clever OS")), 
            expect_equal(my.func()$`sysname`, "Clever OS", fixed = TRUE)
          )
)

Alternatively, have a list where the string in expect_equal

test_that("base function can be mocked",
          with_mock(
            Sys.info = function() return(list(sysname = "Clever OS")),  
            expect_equal(my.func(), list(`sysname` = "Clever OS"), fixed = TRUE)
          )
)
R Yoda
  • 8,358
  • 2
  • 50
  • 87
kangaroo_cliff
  • 6,067
  • 3
  • 29
  • 42
  • Thanks a lot, it seems to work even within a package. Your answer is based on my defective example code were `Sys.info` returns a character vector while my mock function returns a list so you have fixed my problem + it works now even to mock base functions. Either the help for `with_mock` is outdated or something else has changed. I will **not** update my wrong code example for consistency, please update your answer to correct my mock with `Sys.info = function() return(c(sysname = "Clever OS"))`. – R Yoda May 21 '18 at 10:08
  • I have published a github repo to evaluate the problem within a package (mocking a base function within a package works! - even though the help for `with_mock` says it does not work): https://github.com/aryoda/testthat_base_mocking – R Yoda May 21 '18 at 10:13
  • @RYoda Glad, it helped. Why don't you edit my answer? If not, I will do so later. – kangaroo_cliff May 21 '18 at 10:19
  • 1
    @Surren Edit done, please verify and adjust as you want – R Yoda May 21 '18 at 11:17
3

Warning about mocking base functions using with_mock:

Even though mocking of base functions may work in case of my question and the accepted answer of @Suren many issues around with_mock in the package testthat disencourage to mock base package functions (or even functions outside of the package under test), e. g.

testthat 2.0.0 - Breaking API changes

  • "Can't mock functions in base packages": You can no longer use with_mock() to mock functions in base packages, because this no longer works in R-devel due to changes with the byte code compiler. I recommend using mockery or mockr instead.

Don't allow base packages to be mocked

with_mock() function seems to interact badly with the JIT compiler

  • As I mentioned in hadley/testthat#546 (comment), mockery already has a replacement function for with_mock called stub, which takes a different approach to mocking things out that is not restricted by the issues raised in the other threads. If with_mock breaks, I think this package would work just as well with mockery. I also think mockery would be a reasonable place to house with_mock, where it could live alongside stub for legacy reasons.

Prevent with_mock from touching base R packages

R Yoda
  • 8,358
  • 2
  • 50
  • 87