TAP-compliant Unit Testing

Share:

Description

Concise TAP-compliant unit testing package. Authored unit tests can be run using R CMD check with minimal implementation overhead. If you want more features there are other unit testing packages (see 'See Also').

Details

The unittest package provides two functions, ok and ok_group. The ok function prints ok when the expression provided evaluates to TRUE and prints not ok if the expression evaluates to anything else or results in a runtime error; this is the TAP format (http://testanything.org/) for reporting test results. The ok_group function is a convenience function for grouping related unit tests and produces TAP compliant comments in the output to separate the unit test groups.

A unit test summary is produced at the end of a session when a set of unit tests are run in non-interactive mode, for example when the unit tests are run using Rscript or by R CMD check. For using with R CMD check, see ‘I'm writing a package, how do I put tests in it?’.

For a list of all documentation use library(help="unittest").

Getting started

You don't need to be writing a package to use unittest. Suppose you have a file biggest.R with the following function

1
2
    biggest <- function(x,y) {max(c(x,y))}
  

To test this create a file called test_biggest.R in the same directory containing

1
2
3
4
5
6
7
    library(unittest, quietly = TRUE)

    source('biggest.R')

    ok(biggest(3,4) == 4, "two numbers")    
    ok(biggest(c(5,3),c(3,4)) == 5, "two vectors")    
  

Now in an R session source() the test file

1
2
    source('test_biggest.R')
  

and you will see output like this

1
2
3
    ok - two numbers
    ok - two vectors
  

and that's it.

Now each time you edit biggest.R re-sourcing test_biggest.R reloads your function and runs your unit tests.

I'm writing a package, how do I put tests in it?

First, add unittest to Suggests: in the package DESCRIPTION file.

1
2
    Suggests: unittest
  

Then create a directory called tests in your package source, alongside your R directory.

Any .R file in the tests directory will be run by R CMD check. The unittest package “knows” that it is being run by CMD check and so at the end of the tests will produce a summary and throw an error if any tests fail; throwing an error will in turn cause CMD check to report the error and fail the check.

Assuming your package contains (and exports) the biggest() function from ‘Getting started’, we could add a tests/test_biggest.R that contains

1
2
3
4
5
6
    library(mypackage)
    library(unittest, quietly = TRUE)

    ok(biggest(3,4) == 4, "two numbers")
    ok(biggest(c(5,3),c(3,4)) == 5, "two vectors")
  

and that's it. R CMD check will run the tests and fail if any of the tests fail.

Grouping tests

When dealing with many unit tests in one file it can be useful to group related unit tests. The ok_group function is used like this

1
2
3
4
5
6
7
8
9
    ok_group("Test addition", {
      ok(1 + 1 == 2, "Can add 1")
      ok(1 + 3 == 4, "Can add 3")
    })
    ok_group("Test subtraction", {
      ok(1 - 1 == 0, "Can subtract 1")
      ok(1 - 3 == -2, "Can subtract 3")
    })
  

which will produce output like this

1
2
3
4
5
6
7
    # Test addition
    ok - Can add 1
    ok - Can add 3
    # Test subtraction
    ok - Can subtract 1
    ok - Can subtract 3
  

Cookbook

Checking error conditions

Define a helper function test_for_error that uses tryCatch. If your test results in an error that does not match, then the test fails and the actual error will be included in your test results.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
      test_for_error <- function(code, expected_regexp) {
          tryCatch({
                  code
                  return("No error returned")
              }, error = function(e) {
                  if(grepl(expected_regexp, e$message)) return(TRUE)
                  return(c(e$message, "Did not match:-", expected_regexp))
              }
          )
      }

      add_four <- function(x) {
          if( ! is.numeric(x) ) stop("x must be numeric")
          return( x+4 )
      }

      ok(test_for_error(add_four("a"), "must be numeric"), "add_four() argument not numeric correctly throws an error")
    

this will result in the output

1
2
      ok - add_four() argument not numeric correctly throws an error
    

Testing un-exported package functions

If you have some unit tests which require access to un-exported functions, or un-exported S3 methods, you can use local.

1
2
3
4
5
6
      local({
        ok(internal_function() == 3)
        ok(another_internal_function() == 4)
        ok(final_internal_function() == 5)
      }, asNamespace('mypackage'))
    

Comparing multivalue results

Use all.equal(...)

1
2
3
4
      a <- c(1,2,3)
      b <- 1:3
      ok(all.equal(a,b), "a and b are equal")
    

Alternatively, the following helper function will give coloured output showing what's different

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
      cmp <- function(a, b) {
        if(identical(all.equal(a,b), TRUE)) return(TRUE)

        if (file.exists(Sys.which('git'))) {
          totmp <- function(x) {
            f <- tempfile(pattern = "str.")
            capture.output(str(x,
              vec.len = 1000,
              digits.d = 5,
              nchar.max = 1000), file = f)
            return(f)
          }

          return(suppressWarnings(system2(
            Sys.which('git'),
            c("diff", "--no-index", "--color-words", totmp(a), totmp(b)),
            input = "",
            stdout = TRUE, stderr = TRUE)))
        }

        return(c(
          capture.output(str(a)),
          "... does not equal...",
          capture.output(str(b))
        ))
      }
    

To see the coloured output, try the following:

1
2
    ok(cmp(1:3, 1:8))
    

Author(s)

Maintainer: Jamie Lentin <jm@ravingmantis.com>, Anthony Hennessey <ah@ravingmantis.com>.

References

Inspired by Perl's Test::Simple (http://search.cpan.org/perldoc?Test::Simple).

See Also

testthat, RUnit, svUnit.

Want to suggest features or report bugs for rdrr.io? Use the GitHub issue tracker.