Chapter 6 Testing
Every time we make a change to our package, we should run devtools:: check()
function and make sure that there are no errors, warnings, or notes.
However, we should also add some unit tests to insure that the functions in our package work as expected. The package users (and it might also happen to you!) will try to run the functions in your package with wrong arguments!
For example, if we call our numeric_summary()
with a character vector as an input we get the following output:
> numeric_summary(c("a", "b", "c"))
min max mean sd length Nmiss
"a" "c" NA NA "3" "0"
Warning messages:
1: In mean.default(x, na.rm = na.rm) :
argument is not numeric or logical: returning NA
2: In var(if (is.vector(x) || is.factor(x)) x else as.double(x), na.rm = na.rm) :
NAs introduced by coercion
If we call our function with a logical vector, we get no error or warning message. However we should probably get an error message:
> numeric_summary(c(T, F, F, T, NA))
min max mean sd length Nmiss
NA NA NA NA 5 1
We should change our function so it returns an informative error message if the input is not a numeric vector and write a unit test that checks that the error message is issued if the input vector is not numeric.
To write a unit tests we will use testthat package:
> usethis::use_testthat()
✓ Adding 'testthat' to Suggests field in DESCRIPTION
✓ Setting Config/testthat/edition field in DESCRIPTION to '3'
✓ Creating 'tests/testthat/'
✓ Writing 'tests/testthat.R'
• Call `use_test()` to initialize a basic test file and open it for editing.
The above function adds testthat package to the list of suggested packages in the DESCRIPTION
file and then creates a directory tests that will contain a subdirectory testthat with the unit tests. testthat.R
file that is also created ensures that our unit tests are run anytime we run the check()
function.
6.1 Unit tests
To create a unit test for our numeric_summary()
function, we execute:
::use_test("numeric_summary") usethis
✓ Writing 'tests/testthat/test-numeric_summary.R'
• Modify 'tests/testthat/test-numeric_summary.R'
Note:The argument we use within use_test()
function does not have to be the same as our function name.
Here is an example of a function that checks that an error message is given if numeric_summary()
is called with a character or logical vector as an input:
test_that("x is a numeric vector", {
expect_error(numeric_summary(c("a", "b", "c")),
"x must be a numeric vector")
expect_error(numeric_summary(c(T, F, F, T, NA)),
"x must be a numeric vector")
})
To run the tests we call devtools::test()
function:
::test() devtools
ℹ Loading myutils
ℹ Testing myutils
✓ | F W S OK | Context
x | 2 2 0 | numeric_summary [0.4s]
─────────────────────────────────────────────────────────────────────────────────────────
Warning (test-numeric_summary.R:2:3): x is a numeric vector
argument is not numeric or logical: returning NA
Backtrace:
1. testthat::expect_error(numeric_summary(c("a", "b", "c")), "x must be a numeric vector")
at test-numeric_summary.R:2:2
7. myutils::numeric_summary(c("a", "b", "c"))
9. base::mean.default(x, na.rm = na.rm)
Warning (test-numeric_summary.R:2:3): x is a numeric vector
NAs introduced by coercion
Backtrace:
1. testthat::expect_error(numeric_summary(c("a", "b", "c")), "x must be a numeric vector")
at test-numeric_summary.R:2:2
7. myutils::numeric_summary(c("a", "b", "c"))
8. stats::sd(x, na.rm = na.rm)
at myutils/R/my_summaries.R:21:2
9. stats::var(...)
Failure (test-numeric_summary.R:2:3): x is a numeric vector
`numeric_summary(c("a", "b", "c"))` did not throw the expected error.
Failure (test-numeric_summary.R:4:3): x is a numeric vector
`numeric_summary(c(T, F, F, T, NA))` did not throw the expected error.
─────────────────────────────────────────────────────────────────────────────────────────
══ Results ══════════════════════════════════════════════════════════════════════════════
Duration: 0.4 s
[ FAIL 2 | WARN 2 | SKIP 0 | PASS 0 ]
We can see that our tests did not pass. This is because we have not implemented the check in our function yet. Let’s fix it by adding the following line into numeric_summary()
function:
if(!is.numeric(x))stop("x must be a numeric vector")
Now, when we run test()
function we should see that all tests pass:
::test() devtools
ℹ Loading myutils
ℹ Testing myutils
✓ | F W S OK | Context
✓ | 2 | numeric_summary [0.2s]
══ Results ══════════════════════════════════════════════════════════════════════════════
Duration: 0.2 s
[ FAIL 0 | WARN 0 | SKIP 0 | PASS 2 ]
6.2 Popular testthat functions
expect_equal()
,expect_identical()
: Does code return the expected value? *expect_equal() function ignores small numeric differences.expect_setequal()
,expect_mapequal()
: Does code return a vector containing the expected values?expect_error()
,expect_warning()
,expect_message()
: Does code throw an error, warning, or message?expect_true()
,expect_false()
: Does code returnTRUE
orFALSE
?expect_null()
: Does code returnNULL
?expect_output()
: Does code print output to the console?expect_output_file()
: Does the output matches the content of a file?expect_vector()
: Does code return a vector with the expected size and/or prototype?expect_length()
: Does code return a vector with the specified length?expect_match()
: Does a string match a regular expression?expect_named()
: Does code return a vector with (given) names?
6.2.1 Testing for equality
There are three functions that usually used for checking if the output of a function is equal to the expected object: expect_identical()
, expect_equal()
, and expect_equivalent()
:
expect_identical()
checks that the values, attributes, and type of both objects are the same.expect_equal()
checks that the values, and attributes of both objects are the same. This function will ignore small floating point differences. An optional argument tolerance can be adjusted.expect_equivalent()
checks that the values, of both objects are the same.
6.2.2 Testing non-exported functions
Since only exported functions are loaded when the tests are run, you can test non-exported functions by adding a package name as a prefix followed with 3 columns: myutils:::myfun()
6.2.3 Add safeguards to the functions
To prevent errors in the package and inform the user about wrong input, we can use one of three mechanisms:
stop()
- gives an error message and stops the execution of the programwarning()
- prints a message to allert the user about a potential error, but continues to execute the codemessage()
- prints a message
Note: As usual, do not forget to:
- Use
load_all()
function to load new versions of your functions to your environment - Run
check()
- Make a commit for your new changes.