5 minute read

Exercises����������������������������������������������������������������������������������������������������������������������

Chapter 12 ■ testing and paCkage CheCking

Using testthat

Advertisement

The testthat framework (see https://github.com/hadley/testthat) provides functions for writing unit tests and makes sure that each test is run in a clean environment (so you don’t have functions defined in one test leak into another because of typos and such). It needs a few modifications to your DESCRIPTION file and your directory structure, but you can automatically make these adjustments by running the following:

devtools::use_testthat()

This adds testthat to the Suggests packages, makes the directory tests/testthat and the file tests/ testthat.R. You can have a look at the file, but it isn’t that interesting. Its purpose is to make sure that the package testing—that runs all scripts in the tests/ directory—will also run all the testthat tests.

The testthat tests should all go in the tests/thestthat directory and in files whose names start with test. Otherwise, testthat cannot find them. The tests are organized in contexts and tests to make the output of running the tests more readable—if a test fails, you don’t just want to know that some test failed somewhere, but you want some information about which test failed and where, and that is provided by the contexts.

At the top of your test files, you set a context using the context function. It just gives a name to the following batch of tests. This context is printed during testing so you can see how the tests are progressing and if you keep to one context per file, you can see in which files tests are failing.

The next level of tests is wrapped in calls to the test_that function. This function takes a string as its first argument, which should describe what is being tested. Its second argument is a statement that will be the test. The statement is typically more than one single statement, and in that case, it is wrapped in {} brackets.

At the beginning of the test statements you can create some objects or whatever you need for the tests and after that you can do the actual tests. Here, testthat also provides a whole suite of functions for testing if values are equal, almost equal, if an expression raises a working, triggers an error, and much more. All these functions start with expect_ and you can check the documentation for them in the testthat documentation.

The test for computing the area and circumference of rectangles would look like this in a testthat test:

context("Testing area and circumference")

test_that("we compute the correct area and circumference", { r <- rectangle(width = 2, height = 4)

expect_equal(area(r), 2*4) expect_equal(circumference(r), 2*2 + 2*4) })

Try to add this test to your shapes packet from last chapter’s exercises and see how it works. Try modifying it to trigger an error and see how that works.

You should always worry a little bit when testing equality of numbers, especially if they are floatingpoint numbers. Computers do not treat floating-point numbers the way mathematics treat real numbers. Because floating-point numbers have to be represented in finite memory, the exact number you get depends on how you compute it, even if mathematically two expressions should be identical.

For the tests we do with the rectangle, this is unlikely to be a problem. There aren’t really that many ways to compute the two quantities we test for and we would expect to get exactly these numbers. But how about the quantities for circles?

283

Chapter 12 ■ testing and paCkage CheCking

circle <- function(radius) { structure(list(r = radius), class = c("circle", "shape"))

} area.circle <- function(x) pi * x$r**2 circumference.circle <- function(x) 2 * pi * x$r test_that("we compute the correct area and circumference", { radius <- 2 circ <- circle(radius = radius)

expect_equal(area(circ), pi * radius^2) expect_equal(circumference(circ), 2 * radius * pi) })

Here I use the built-in pi but what if the implementation used something else? Here we are definitely working with floating-point numbers, and we shouldn’t ever test for exact equality. Well, the good news is that expect_equal doesn’t. It tests for equality within some tolerance of floating-point uncertainty—that can be modified using an additional parameter to the function—so all is good. To check exact equality, you should instead use the function expect_identical, but it is usually expect_equal that you want.

Writing Good Tests

The easiest way to get some tests written for your code is to take the experiments you make when developing the code and translate them into unit tests like this right away—or even put your checks in a unit test file, to begin with. By writing the tests at the same time as you write the functions—or at least immediately after—you don’t build a backlog of untested functionality (and it can be very hard to force yourself to go and spend hours just writing tests later on). Also, it doesn’t really take that much longer to take the informal testing you write to check your functions while you write them and put them into a testthat file and get a formal unit test.

If this is all you do, at least you know that the functionality that was tested when you developed your code is still there in the future, or you will be warned if it breaks at some point because the tests will start to fail.

If you are writing tests anyway, you might as well be a little more systematic about it. We always tend to check for the common cases—the cases we have in mind when we write the function—and forget about special cases. Special cases are frequently where bugs hide, however, so it is always a good idea to put them in your unit tests as well.

Special cases are situations such as empty vectors and lists or NULL as a list. If you implement a function that takes a vector as input, make sure that it also works if that vector is empty. If it is not a meaningful value for the function to take, and you cannot think of a reasonable value to return if the input is empty, make sure the function throws an error rather than just do something that it wasn’t designed to do.

For numbers, exceptional cases are often zero or negative numbers. If your functions can handle these cases, excellent (but make sure you test it!); if they cannot handle these special situations, throw an error.

For the shapes, it isn’t meaningful to have non-positive dimensions, so in my implementation I raise an error if I get that and a test for it, for rectangles, could look like this:

test_that("Dimensions are positive", { expect_error(rectangle(width = -1, height = 4)) expect_error(rectangle(width = 2, height = -1)) expect_error(rectangle(width = -1, height = -1))

284

This article is from: