6 Testing

Now that we have some documentation devtools::check() should run without any problems.

-- R CMD check results ------------------------------------------- mypkg 0.0.0.9000 ----
Duration: 15.2s

0 errors √ | 0 warnings √ | 0 notes √

(This is just the bottom part of the output to save space)

While we pass all the standard package checks there is one kind of check that we don’t have yet. Unit tests are checks to make sure that a function works in the way that we expect. The examples we wrote earlier are kind of like informal unit tests because they are run as part of the checking process but it is better to have something more rigorous. One approach to writing unit tests is what is known as “test driven development”. The idea here is to write the tests before you write a function. This way you know exactly what a function is supposed to do and what problems there might be. While this is a good principal it can take a lot of advance planning. A more common approach could be called “bug-driven testing”. For this approach whenever we come across a bug we write a test for it before we fix it, that way the same bug should never happen a again. When combined with some tests for obvious problems this is a good compromise better testing for every possible outcome and not testing at all. For example let’s see what happens when we ask make_shades() for a negative number of shades.

Error in seq(colour_rgb[1], end, length.out = n + 1)[1:n] : 
  only 0's may be mixed with negative subscripts

This doesn’t make sense so we expect to get an error but it would be useful if the error message was more informative. What if we ask for zero shades?

[1] "#DAA520"

That does work, but it probably shouldn’t. Before we make any changes to the function let’s design some tests to make sure we get what we expect. There are a few ways to write unit tests for R packages but we are going to use the testthat package. We can set everything up with usethis.

✔ Adding 'testthat' to Suggests field in DESCRIPTION
✔ Creating 'tests/testthat/'
✔ Writing 'tests/testthat.R'
● Call `use_test()` to initialize a basic test file and open it for editing.

Now we have a tests/ directory to hold all our tests. There is also a tests/testthat.R file which looks like this:

All this does is make sure that our tests are run when we do devtools::check(). To open a new test file we can use usethis::use_test().

✔ Increasing 'testthat' version to '>= 2.1.0' in DESCRIPTION
✔ Writing 'tests/testthat/test-colours.R'
● Modify 'tests/testthat/test-colours.R'

Just like R files our test file needs a name. Tests can be split up however you like but it often makes sense to have them match up with the R files so things are easy to find. Our test file comes with a small example that shows how to use testthat.

Each set of tests starts with the test_that() function. This function has two arguments, a description and the code with the tests that we want to run. It looks a bit strange to start with but it makes sense if you read it as a sentence, “Test that multiplication work”. That makes it clear what the test is for. Inside the code section we see an expect function. This function also has two parts, the thing we want to test and what we expect it to be. There are different functions for different types of expectations. Reading this part as a sentence says something like “Expect that 2 * 2 is equal to 4”. For our test we want to use the expect_error() function, because that is what we expect.

To run our tests we use devtools::test().

Loading mypkg
Testing mypkg
√ |  OK F W S | Context
x |   0 2     | colours
--------------------------------------------------------------------------------
test-colours.R:2: failure: n is at least 1
`make_shades("goldenrod", -1)` threw an error with unexpected message.
Expected match: "n must be at least 1"
Actual message: "only 0's may be mixed with negative subscripts"

test-colours.R:4: failure: n is at least 1
`make_shades("goldenrod", 0)` did not throw an error.
--------------------------------------------------------------------------------

== Results =====================================================================
OK:       0
Failed:   2
Warnings: 0
Skipped:  0

No one is perfect!

We can see that both of our tests failed. That is ok because we haven’t fixed the function yet. The first test fails because the error message is wrong and the second one because there is no error. Now that we have some tests and we know they check the right things we can modify our function to check the value of n and give the correct error.

Let’s add some code to check the value of n. We will update the documentation as well so the user knows what values can be used.

Writing parameter checks

These kinds of checks for parameter inputs are an important part of a function that is going to be used by other people (or future you). They make sure that all the input is correct before the function tries to do anything and avoids confusing error messages. However they can be fiddly and repetitive to write. If you find yourself writing lots of these checks two packages that can make life easier by providing functions to do it for you are checkmate and assertthat.

Here we have used the stop() function to raise an error. If we wanted to give a warning we would use warning() and if just wanted to give some information to the user we would use message(). Using message() instead of print() or cat() is important because it means the user can hide the messages using suppressMessages() (or suppressWarnings() for warnings). Now we can try our tests again and they should pass.

Loading mypkg
Testing mypkg
√ |  OK F W S | Context
√ |   2       | colours

== Results =====================================================================
OK:       2
Failed:   0
Warnings: 0
Skipped:  0

There are more tests we could write for this function but we will leave that as an exercise for you. If you want to see what parts of your code need testing you can run the devtools::test_coverage() function (you might need to install the DT package first). This function uses the covr package to make a report showing which lines of your code are covered by tests.