- stubthat
- Installation
- Introduction
- Usage
- Use cases
- API
- A note regarding
with_mock
- License
- Install the latest stable version from CRAN with
install.packages("stubthat")
- Install the development version from github with
devtools::install_github("sainathadapa/stubthat")
stubthat package provides stubs for use while unit testing in R. The API is highly inspired by Sinon.js. This package is meant to be used along with testthat and mockr packages, specifically the ‘mockr::with_mock’ function.
To understand what a stub is and how they are used while unit testing, please take a look at this Stackoverflow question What is a “Stub”?.
There are three main steps for creating & using a stub of a function -
- Invoke the stub function with the function that needs to be mocked
jedi_or_sith <- function(x) return('No one')
jedi_or_sith_stub <- stub(jedi_or_sith)
- Define the behavior. This is explained in detail in the API section.
jedi_or_sith_stub$withArgs(x = 'Luke')$returns('Jedi')
- Once the behavior is defined, you can use the stub by calling the
jedi_or_sith_stub$f
function.
jedi_or_sith('Luke')
#> [1] "No one"
jedi_or_sith_stub$f('Luke')
#> [1] "Jedi"
Stubs are generally used in the testing environment. Here is an example:
library(httr) # provides the GET and status_code functions
url_downloader <- function(url) GET(url)
check_api_endpoint_status <- function(url) {
response <- url_downloader(url)
response_status <- status_code(response)
ifelse(response_status == 200, 'up', 'down')
}
This function check_api_endpoint_status should make a GET request
(via the url_downloader function) to the specified url (say
https://example.com/endpoint
) and it should return ‘up’ if the
status code is ‘200’. Return ‘down’ otherwise. While testing, it is
generally a good idea to avoid making repeated (or any) requests to
external sources.
Using stubs (and with_mock
from
mockr), the above function can be
tested without accessing the external source, as shown below:
url_downloader_stub <- stub(url_downloader)
url_downloader_stub$withArgs(url = 'good url')$returns(200)
url_downloader_stub$withArgs(url = 'bad url')$returns(404)
# testthat package provides the expect_equal function
# mockr package provides the with_mock function
check_api_endpoint_status_tester <- function(x) {
mockr::with_mock(url_downloader = url_downloader_stub$f,
check_api_endpoint_status(x))
}
(testthat::expect_equal(check_api_endpoint_status_tester('good url'), 'up'))
#> [1] "up"
(testthat::expect_equal(check_api_endpoint_status_tester('bad url'), 'down'))
#> [1] "down"
Another use case: Consider the outline of a function f1
f1 <- function(...) {
{...some computation...}
interim_val <- f2(...)
{...more computation...}
return(ans)
}
Here, the function f1
calls f2
within its body. Suppose f2
takes
more than few seconds to run (e.g.: Simulations, Model building, etc).
Let’s assume that the f2
function already has separate tests written
to test its validity. As f2
function’s validity is ensured, and since
it takes a lot of time to finish, it may be better to skip the
interim_val <- f2(...)
statement in tests for the f1
function. Also,
a general expectation from a suite of tests is that they should finish
within few minutes (if not seconds). In such a case, using a stub of
f2
while testing f1
is desirable.
Stub will check the incoming arguments for the specified set of arguments. Throws an error if there is a mismatch.
sum <- function(a, b = 1) return(a + b)
stub_of_sum <- stub(sum)
stub_of_sum$expects(a = 2)
stub_of_sum$f(2)
#> NULL
stub_of_sum$f(3)
#> Error in stub_of_sum$f(3): Following arguments are not matching: {'a'}
#> Argument: 'a':
#> 1/1 mismatches
#> [1] 2 - 3 == -1
The set of specified arguments should be exactly matched with the set of incoming arguments.
sum <- function(a, b = 1) return(a + b)
stub_of_sum <- stub(sum)
stub_of_sum$strictlyExpects(a = 2)
stub_of_sum$f(2)
#> Error in stub_of_sum$f(2): Function was called with the following extra arguments: 'b'
The above call resulted in the error because the incoming set of
arguments was a = 2, b = 1
, but the defined set of expected arguments
consisted only a = 2
.
stub_of_sum$strictlyExpects(a = 2, b = 1)
stub_of_sum$f(2)
#> NULL
The stub expects the specifed arguments on the nth call.
sum <- function(a, b = 1) return(a + b)
stub_of_sum <- stub(sum)
stub_of_sum$onCall(3)$expects(a = 2)
stub_of_sum$f(100)
#> NULL
stub_of_sum$f(100)
#> NULL
stub_of_sum$f(100)
#> Error in stub_of_sum$f(100): Following arguments are not matching: {'a'}
#> Argument: 'a':
#> 1/1 mismatches
#> [1] 2 - 100 == -98
The stub expects the exact set of specifed arguments on the nth call.
sum <- function(a, b = 1) return(a + b)
stub_of_sum <- stub(sum)
stub_of_sum$onCall(3)$strictlyExpects(a = 2, b = 2)
stub_of_sum$f(2)
#> NULL
stub_of_sum$f(2)
#> NULL
stub_of_sum$f(2)
#> Error in stub_of_sum$f(2): Following arguments are not matching: {'b'}
#> Argument: 'b':
#> 1/1 mismatches
#> [1] 2 - 1 == 1
Unless otherwise specified, the stub always returns the specified value.
sum <- function(a, b = 1) return(a + b)
stub_of_sum <- stub(sum)
stub_of_sum$returns(0)
stub_of_sum$f(2)
#> [1] 0
The stub returns the specified value on the nth call.
sum <- function(a, b = 1) return(a + b)
stub_of_sum <- stub(sum)
stub_of_sum$onCall(2)$returns(0)
stub_of_sum$f(2)
#> NULL
stub_of_sum$f(2)
#> [1] 0
The stub returns the specified value when it is called with the specified arguments.
sum <- function(a, b = 1) return(a + b)
stub_of_sum <- stub(sum)
stub_of_sum$withArgs(a = 2)$returns(0)
stub_of_sum$f(1)
#> NULL
stub_of_sum$f(2)
#> [1] 0
The stub returns the specified value when it is called with the exact set of specified arguments.
sum <- function(a, b = 1) return(a + b)
stub_of_sum <- stub(sum)
stub_of_sum$withExactArgs(a = 2)$returns(0) # won't work because value for b is not defined
stub_of_sum$withExactArgs(a = 2, b = 1)$returns(1)
stub_of_sum$f(1)
#> NULL
stub_of_sum$f(2)
#> [1] 1
Unless otherwise specified, the stub throws an error with the specified message.
sum <- function(a, b = 1) return(a + b)
stub_of_sum <- stub(sum)
stub_of_sum$throws('some err msg')
stub_of_sum$f(2)
#> Error in output_func(do_this$behavior, do_this$return_val): some err msg
The stub throws an error on the nth call.
sum <- function(a, b = 1) return(a + b)
stub_of_sum <- stub(sum)
stub_of_sum$onCall(2)$throws('some err msg')
stub_of_sum$f(0)
#> NULL
stub_of_sum$f(0)
#> Error in output_func(do_this$behavior, do_this$return_val): some err msg
The stub throws an error when it is called with the specified arguments.
sum <- function(a, b = 1) return(a + b)
stub_of_sum <- stub(sum)
stub_of_sum$withArgs(a = 2)$throws('some err msg')
stub_of_sum$f(1)
#> NULL
stub_of_sum$f(2)
#> Error in output_func(do_this$behavior, do_this$return_val): some err msg
The stub returns the specified value when it is called with the exact set of specified arguments.
sum <- function(a, b = 1) return(a + b)
stub_of_sum <- stub(sum)
stub_of_sum$withExactArgs(a = 2)$throws('good') # won't work because value for b is not defined
stub_of_sum$withExactArgs(a = 2, b = 1)$throws('nice')
stub_of_sum$f(1)
#> NULL
stub_of_sum$f(2)
#> Error in output_func(do_this$behavior, do_this$return_val): nice
Using this, one can obtain the number of times, the stub has been called.
sum <- function(a, b = 1) return(a + b)
stub_of_sum <- stub(sum)
ans <- stub_of_sum$f(3)
ans <- stub_of_sum$f(3)
stub_of_sum$calledTimes()
#> [1] 2
ans <- stub_of_sum$f(3)
stub_of_sum$calledTimes()
#> [1] 3
Convenience functions to reduce repetition of code.
On nth call, the stub will check for the specified arguments, and if satisfied, returns the specified value.
sum <- function(a, b = 1) return(a + b)
stub_of_sum <- stub(sum)
stub_of_sum$onCall(1)$expects(a = 1)$returns('good')
stub_of_sum$onCall(3)$expects(a = 3)$returns('nice')
stub_of_sum$f(3)
#> Error in stub_of_sum$f(3): Following arguments are not matching: {'a'}
#> Argument: 'a':
#> 1/1 mismatches
#> [1] 1 - 3 == -2
stub_of_sum$f(3)
#> NULL
stub_of_sum$f(3)
#> [1] "nice"
This is same as calling stub$onCall(#)$expects(...)
and
stub$onCall(#)$returns(...)
separately.
sum <- function(a, b = 1) return(a + b)
stub_of_sum <- stub(sum)
stub_of_sum$onCall(1)$expects(a = 1)
stub_of_sum$onCall(1)$returns('good')
stub_of_sum$onCall(3)$returns('nice')
stub_of_sum$onCall(3)$expects(a = 3)
stub_of_sum$f(3)
#> Error in stub_of_sum$f(3): Following arguments are not matching: {'a'}
#> Argument: 'a':
#> 1/1 mismatches
#> [1] 1 - 3 == -2
stub_of_sum$f(3)
#> NULL
stub_of_sum$f(3)
#> [1] "nice"
On nth call, the stub will check for the exact set of specified arguments, and if satisfied, returns the specified value.
sum <- function(a, b = 1) return(a + b)
stub_of_sum <- stub(sum)
stub_of_sum$onCall(1)$strictlyExpects(a = 3)$returns('good')
stub_of_sum$onCall(3)$strictlyExpects(a = 3, b = 1)$returns('nice')
stub_of_sum$f(3)
#> Error in stub_of_sum$f(3): Function was called with the following extra arguments: 'b'
stub_of_sum$f(3)
#> NULL
stub_of_sum$f(3)
#> [1] "nice"
On nth call, the stub will check for the specified arguments, and if satisfied, throws an error with the specified message.
sum <- function(a, b = 1) return(a + b)
stub_of_sum <- stub(sum)
stub_of_sum$onCall(1)$expects(a = 1)$throws('good')
stub_of_sum$onCall(3)$expects(a = 3)$throws('nice')
stub_of_sum$f(3)
#> Error in stub_of_sum$f(3): Following arguments are not matching: {'a'}
#> Argument: 'a':
#> 1/1 mismatches
#> [1] 1 - 3 == -2
stub_of_sum$f(3)
#> NULL
stub_of_sum$f(3)
#> Error in output_func(do_this$behavior, do_this$return_val): nice
On nth call, the stub will check for the exact set of specified arguments, and if satisfied, throws an error with the specified message.
sum <- function(a, b = 1) return(a + b)
stub_of_sum <- stub(sum)
stub_of_sum$onCall(1)$strictlyExpects(a = 3)$throws('good')
stub_of_sum$onCall(3)$strictlyExpects(a = 3, b = 1)$throws('nice')
stub_of_sum$f(3)
#> Error in stub_of_sum$f(3): Function was called with the following extra arguments: 'b'
stub_of_sum$f(3)
#> NULL
stub_of_sum$f(3)
#> Error in output_func(do_this$behavior, do_this$return_val): nice
testthat::with_mock
function is going to be deprecated in a future
release of testthat
. mockr
library’s with_mock
function is meant to be the replacement for
testthat::with_mock
. Slight changes will be needed while replacing
testthat::with_mock
with mockr::with_mock
. Refer to mockr’s README
for more details.
Also, it is no longer possible to mock functions from external packages.
If you are doing this, either change the code to avoid such a case or
use a wrapper function similar to the
url_downloader <- function(url) GET(url)
example in this document. To
know more about the reasons behind these changes, refer to the following
github issues : with_mock interacts badly with the
JIT, Prevent with_mock
from touching base R
packages.
Released under MIT License.