Day 1 Session 3: Unit tests
Invalid Date
At the end of this section you will be able to:
testthat (Wickham 2011) workflowtestthat
When we run:
The objects italy and spain should be:
So we mightβ¦
Try these:
β¦is informal testing. We:
uss_make_matches()
devtools::load_all()
uss_make_matches() interactivelyuss_make_matches() if neededdevtools::load_all()
uss_make_matches() interactivelyProblem: you forget all the interactive testing youβve done
Solution: have a system to store and re-run the tests!
Fewer bugs: you are explicit about behaviour of functions.
Encourages good code design. If it is hard to write unit tests your function may need refactoring
Opportunity for test-driven development
Robustness
Read more about testing in the recently updated chapter in R Packages (Wickham and Bryan 2020)
.
βββ CITATION.cff
βββ DESCRIPTION
βββ LICENSE
βββ LICENSE.md
βββ man
β βββ matches.Rd
βββ NAMESPACE
βββ R
β βββ matches.R
βββ README.md
βββ tests
β βββ testthat
β β βββ test-matches.R
β βββ testthat.R
βββ ussie.Rprojtests/testthat/
test-xxxx.R
tests/testthat.R: runs the tests when devtools::check() is calledtest-xxxx.R contains several tests. Might be:
expect_zzzz(actual_result, expectation)
actual_result == expectation no erroractual_result != expectation Errortestthat: usethis::use_testthat(3) ONCEusethis::use_test()
testthat::test_file()
devtools::test() and devtools::check()
To set up your package to use testthat: usethis::use_testthat(3) which:
tests/testthat/: this is where the test files liveedits DESCRIPTION:
Suggests: testthat (>= 3.0.0)
Config/testthat/edition: 3
tests/testthat.R: this runs the test when you do devtools:check() DO NOT EDIT㪠Set up your package to use testthat:
usethis::use_testthat(3)3 means testthat edition 3 (testthat 3e)
As well as installing that version of the package, you have to explicitly opt in to the edition behaviours.
Before we try to make a test, letβs look at some of the expect_zzzz() functions we have available to us.
Form: expect_zzzz(actual_result, expectation)
expectation is what you expectactual_result is what you are comparing to the expectationexpect_zzzz() have additional arguments# to try out testhtat interactively we load
# and request edition 3
# but, you do *not* do that in a package.
library(testthat)
local_edition(3)# when the actual result is 42
result <- 42
# and we expect the result to be 42: no error
expect_identical(result, 42)# when the actual result is "a"
result <- "a"
# and we expect the result to be "a": no error
expect_identical(result, "a")# when the actual result is 45
result <- 45
# and we expect the result to be 42: Error
expect_identical(result, 42)Error: `result` (`actual`) not identical to 42 (`expected`).
`actual`: 45
`expected`: 42
# when the actual result is "bob"
result <- "bob"
# and we expect the result to be "a": Error
expect_identical(result, "A")Error: `result` (`actual`) not identical to "A" (`expected`).
`actual`: "bob"
`expected`: "A"
Some types of expect_zzzz()
expect_identical()
expect_equal()
expect_true()
expect_named()
expect_error()
# when the actual result is 42
result <- 42
# and we expect the result to be 42: no error
expect_equal(result, 42)# and when the actual result is 42.0000001
result <- 42.0000001
# and we expect the result to be 42: we still do
# not have an error because expect_equal()
# has some tolerance
expect_equal(result, 42)# but when the actual result is 42.1
result <- 42.1
# and we expect the result to be 42: error because
# 0.1 is bigger than the default tolerance
expect_equal(result, 42)Error: `result` (`actual`) not equal to 42 (`expected`).
`actual`: 42.1
`expected`: 42.0
We can set the wiggle room:
# but when the actual result is 42.1
result <- 42.1
# and we expect the result to be 42: no error if we
# provide a tolerance
expect_equal(result, 42, tolerance = 0.1)# when the result is "bill"
a <- "bill"
# and we expect the result not to be "bob": no error
expect_true(a != "bob")# when the result is "bill"
a <- "bob"
# and we expect the result not to be "bob": error
expect_true(a != "bob")Error: a != "bob" is not TRUE
`actual`: FALSE
`expected`: TRUE
# vector of named values
x <- c(a = 1, b = 2, c = 3)
# test whether x has names: no error
expect_named(x)
# test if the names are "a", "b", "c": no error
expect_named(x, c("a", "b", "c"))
# test if the names are "b", "a", "c": error
expect_named(x, c("b", "a", "c"))Error: Names of `x` ('a', 'b', 'c') don't match 'b', 'a', 'c'
You can create and open (or just open) a test file for blah.R with use_test("blah").
π¬ Create a test file for matches.R
usethis::use_test("matches")For example:
Generally:
You list the expectations inside test_that( , { })
You can have as many expectations as you need.
Ideally, 2 - 6 ish or consider breaking the function into simpler functions.
We will add three expectations:
uss_make_matches() is a tibble with expect_true()
uss_make_matches() has columns with the right names with expect_named()
uss_make_matches() output is correct with expect_identical()
We will do them one at a time so you can practice the workflow.
test-matches.R
π¬ Add a test to check the output of uss_make_matches() is a tibble with expect_true():
test_that("uss_make_matches works", {
# use the function
italy <- uss_make_matches(engsoccerdata::italy, "Italy")
expect_true(tibble::is_tibble(italy))
})We use uss_make_matches() and examine the output with an expectation.
π¬ Run the test with testthat::test_file()
testthat::test_file("tests/testthat/test-matches.R")ββ Testing test-matches.R ββββββββββββββββββββββ
[ FAIL 0 | WARN 0 | SKIP 0 | PASS 1 ] Done!π₯³
You can also use the βRuns Testsβ button
devtools::test()π¬ Run all the tests with devtools::test()
devtools::test()βΉ Loading ussie
βΉ Testing ussie
β | F W S OK | Context
β | 1 | matches [0.4s]
ββ Results ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Duration: 0.5 s
[ FAIL 0 | WARN 0 | SKIP 0 | PASS 1 ]
π Your tests are the bee's knees πNow you will test the output of uss_make_matches() to make sure it has columns with the right names expect_named()
we expect the names to be:
βcountryβ, βtierβ, βseasonβ, βdateβ, βhomeβ, βvisitorβ, βgoals_homeβ, βgoals_visitorβ
π¬ Use expect_named() in test-matches.R to check the column names of italy
test_that("uss_make_matches works", {
# use the function
italy <- uss_make_matches(engsoccerdata::italy, "Italy")
expect_true(tibble::is_tibble(italy))
expect_named(
italy,
c("country", "tier", "season", "date", "home", "visitor",
"goals_home", "goals_visitor")
)
})π¬ Run the edited test file
testthat::test_file("tests/testthat/test-matches.R")Or the βRuns Testsβ button
devtools::test()βΉ Loading ussie
βΉ Testing ussie
β | F W S OK | Context
β | 2 | matches [0.2s]
ββ Results ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Duration: 0.2 s
[ FAIL 0 | WARN 0 | SKIP 0 | PASS 2 ] π₯³
Now check that the country column of the uss_make_matches() output is correct with expect_identical()
π¬ Use expect_identical() in test-matches.R to compare the values in italy$country to βitalyβ
π¬ Run the tests
test_that("uss_make_matches works", {
# use the function
italy <- uss_make_matches(engsoccerdata::italy, "Italy")
expect_true(tibble::is_tibble(italy))
expect_named(
italy,
c("country", "tier", "season", "date", "home", "visitor",
"goals_home", "goals_visitor")
)
expect_identical(unique(italy$country), "Italy")
})Running a practice session for this course, we found a bug:
test_that("uss_make_matches works", {
# use the function
italy <- uss_make_matches(engsoccerdata::italy, "Italy")
expect_true(tibble::is_tibble(italy))
expect_named(
italy,
c("country", "tier", "season", "date", "home", "visitor",
"goals_home", "goals_visitor")
)
expect_identical(unique(italy$country), "Italy")
# when you find a bug, add a test: π from Ian
expect_s3_class(italy$tier, "factor")
})Test coverage is the percentage of package code run when the test suite is run.
covr (Hester 2020) packageThere are two functions you might use interactively:
coverage on the active file: devtools::test_coverage_active_file()
coverage on the whole package:devtools::test_coverage()
π¬ Make sure matches.R is active in the editor and do:
devtools::test_coverage_active_file()
π¬ Check the coverage over the whole package:
devtools::test_coverage()
Now would be a good time to commit your changes and push them to GitHub
testthat βtries to make testing as fun as possibleβ
tests/testthat/
test-xxxx.R
test_that("something works", { *expectations* })
tests/testthat.R: runs the tests and should not (normally) be editedexpect_zzzz(actual_result, expectation)
usethis::use_testthat(3) sets up your package to use testthat
usethis::use_test(xxxx) creates test-xxxx.R
testthat::test_file() runs the tests in a test filedevtools::test() runs the tests in all the test filesdevtools::test_coverage_active_file()
devtools::test_coverage()