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.Rproj
tests/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()