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:

usethis::use_test("numeric_summary")
✓ 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:

devtools::test()
ℹ 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:

devtools::test()
ℹ 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 ]