Testing in R

To test a given function in R, I use the package ‘testthat’.

A very nice intro to the package is present here: https://journal.r-project.org/archive/2011/RJ-2011-002/RJ-2011-002.pdf

However, in order to write good quality code and to do it only once, you got to carry out a paradigm shift in your writing procedure:

How to write a tests-oriented program

You might have the natural tendency to write the tests after your code.

However, this is not the best approach, indeed, after that, you might need to rewrite big part of the code, to make it more ‘testable’.

In order to avoid that, you need to write your test before the program.

Certainly, this is is a bit harder to implement, especially the first times.

Therefore, to make it easier, follow these guidelines:

Guidelines

  • Independent files:
    • One for the program, one for the tests
  • Code stile for the program:
    • Atomicity:
      • A single R file for each task/objective.
      • Single functions for each step of your algorithm. This will help later in the test
    • A main function to be invoked. This will list all the functions (steps) of the program.
  • Tests, in and out:
    • Within the program file
      • Tests that define and check the inputs
      • Tests that define and check the outputs
    • Within the test file
      • A test with correct inputs
      • Wrong inputs
      • Exceptions

File needed

For each given function create a test-file within a path similar to this:

tests/testthat/taskA/test-something.R

If you need to some files to test your function save in a path similar to this:

tests/testthat/taskA/data-.../something.csv

Code stile for the program

MainFunction = function(inputA, inputB){
  
  if(inputA!=IsSupposedToBe){ 
    stop('inputA is not what IsSupposedToBe') ## In general, a stop message is preferable
  }
  if(inputB!=IsSupposedToBe){
    warning('inputB is not what IsSupposedToBe') ## But sometimes you can use warning
  }
  
  ## For each function there is a test
  ItemA = Function1(inputA, inputB)
  ItemB = Function2(ItemA)
  ItemC = Function3(ItemB)
  
  return(ItemC) 
}

Test Examples

Let’s consider two simple function:

Function1 = function(x) return(x^2)
Function2 = function(x,y) return(x + Function1(y))

Three are the main kind of test I suggest to use: One that test if the result is correct, one that is not correct, one the deals with error and exception.

testthat::expect_true(Function2(1,2) == 5)
testthat::expect_false(Function2(1,2) == 3)
testthat::expect_error(Function2(1,) == 5)

Structure of a test-file:

## Name and descripiton of the tests present in the file

unlink('temp', recursive = TRUE) ## Create a temporary folder where to test
if(!file.exists('temp')){
  dir.create('temp')
}
setwd('temp')

It is probably not correct practice but I advise to live the packages needed and function to be tested as comment at the beginning of the file.

Body

##library(needed)
##source(../../Rscripts/function_to_be_tested.R)

load('../data-.../something.csv') ## Load what you need


## Test if the result is correct
testthat::test_that(I'm a long and clear desciption of the test, {
  result = MainFunction(inputA, inputB)
  
  testthat::expect_true(all(c(ColA,ColB) %in%  names(result)))
  testthat::expect_true(ncol(result)==2)
  testthat::expect_true(nrow(result)==8)
})


## Testing wrong inputs
testthat::test_that(I'm a long and clear desciption of the test, {
  
  testthat::expect_error(MainFunction(inputA, wrong))
  testthat::expect_error(MainFunction(wrong, inputB))
  
  testthat::expect_warning(MainFunction(wrong, wrong))
})


## Testing ouptut
testthat::test_that(I'm a long and clear desciption of the test, {
  testthat::expect_true(file.exists(result.pdf))
})

Exit

### We conclued by exiting the file
setwd('../')
unlink('temp', recursive = TRUE)

Mocking a function

By mocking a function we want to force a particular output from a function that can be encountered during the test.

Function1 = function(x) return(x^2)
Function2 = function(x,y) return(x+Function1(y))

testthat::expect_true(Function2(1,2) == 5)
testthat::expect_false(Function2(1,2) == 3)
testthat::expect_error(Function2(1,) == 5)

In this way, you can avoid to use Function1, because it takes too much time to run, or simply cannot be used right now, so we mock it.

This means that next time Function2 looks for Function1, it is redirected to use the result of m

m = mockery::mock(1)

testthat::with_mock(I'm nice and clear description, Function1 = m, {
  testthat::expect_true(Function2(1,2) == 2)
})

Stubbing

Stubbing is similar to mocking but, Stub, is simple fake object. It just makes sure that the test runs smoothly.

Instead, mock is smarter stub. That verifies your test passes through it.

Function1 = function(x) return(x+1)
Function2 = function(x) return(Function1(x)^2)
Function3 = function(x,y) return(x+Function2(y))

testthat::test_that(I'm nice and clear description, {
  mockery::stub(Function3, Function1;, 2)
  testthat::expect_false(Function3(1,2) == 5)
  testthat::expect_false(Function3(1,2) == 3)
})

Function1 = function(x) return(x)
Function2 = function(x) return(x)
Function3 = function(x,y,z) return(x + Function1(y) + Function2(z))

print(Function3(1,2,3))
Function3(1,NA,NA)

# 6
# NA

testthat::test_that('demonstrate stubbing', {
  mockery::stub(Function3, 'Function1', 2)
  mockery::stub(Function3, 'Function2', 2)
  testthat::expect_true(Function3(1,,) == 5)
})

More in https://github.com/MattiaCinelli/notes/blob/master/Notes%20on%20R%20testing.ipynb

Leave a Reply

Your email address will not be published. Required fields are marked *