Before you begin, get to know your way around the testthat package. Take a look at ls("package:testthat")
and have a quick read of ?testthat
.
Consider the hypotenuse
function we created earlier.
hypotenuse <- function(x, y)
{
sqrt(x ^ 2 + y ^ 2)
}
Write a test to make sure that our function works with a 5-12-13 triangle.
test_that(
"hypotenuse, with inputs 5 and 12, returns 13",
{
expected <- 13
actual <- hypotenuse(5, 12)
expect_equal(actual, expected)
}
)
Modify the expected value so that the test fails. How much do you need to change the expected value by before it fails?
test_that(
"hypotenuse, with inputs 5 and 12, returns 13",
{
expected <- 13 + 2e-7
actual <- hypotenuse(5, 12)
expect_equal(actual, expected)
}
)
## Error: Test failed: 'hypotenuse, with inputs 5 and 12, returns 13'
## Not expected: actual not equal to expected
## 13 - 13 == 2e-07.
expect_equal
make the comparison using Base R’s all.equal
function, which takes a tolerance
argument to determine how closely the two numbers must match. In this case, moving the expected value by about 2e-7
is enough to make the test fail.
Write a test to see what happens when you pass it very large inputs: use x = 1e300
and y = 1e300
. Use Pythagorus’s theorem to calculate the expected value. Does the test pass? If not, why not?
# big number test
test_that(
"hypotenuse, with inputs both 1e300, returns sqrt(2) * 1e300",
{
expected <- sqrt(2) * 1e300
actual <- hypotenuse(1e300, 1e300)
expect_equal(actual, expected)
}
)
## Error: Test failed: 'hypotenuse, with inputs both 1e300, returns sqrt(2) * 1e300'
## Not expected: actual not equal to expected
## 1.41e+300 - Inf == -Inf.
The hypotenuse function square its input, and 1e300 ^ 2
is greater than the largest numeric
value that R can represent (see .Machine$double.xmax
). That mean that the function returns infinity. There are better algorithms for hypotenuses that don’t suffer this overflow problem, but we might not have considered using them without this test.
Similarly, write a test for the hypotenuse
function to see if it works with very small inputs: use x = 1e-300
and y = 1e-300
. Does this test pass? Should this test pass?
# small number test
test_that(
"hypotenuse, with inputs both 1e-300, returns sqrt(2) * 1e-300",
{
expected <- sqrt(2) * 1e-300
actual <- hypotenuse(1e-300, 1e-300)
expect_equal(actual, expected)
}
)
This test passes, but there is still a problem with the function. Here we have the opposite problem: squaring a very small input causes the answer to underflow to zero. The tolerance argument of all.equal
means that we don’t pick up the difference though.
What happens if you reduce the allowed tolerance to a small number, using tol = 1e-305
?
# small number test, lower tolerance
test_that(
"hypotenuse, with inputs both 1e-300, returns sqrt(2) * 1e-300",
{
expected <- sqrt(2) * 1e-300
actual <- hypotenuse(1e-300, 1e-300)
expect_equal(actual, expected, tolerance = 1e-305)
}
)
## Error: Test failed: 'hypotenuse, with inputs both 1e-300, returns sqrt(2) * 1e-300'
## Not expected: actual not equal to expected
## 1.41e-300 - 0 == 1.41e-300.
Now the test correctly fails.
Write a test (or several tests!) for the behaviour of hypotenuse
when one or more of the inputs are character vectors.
# character test
test_that(
"hypotenuse, with a character input, throws an error",
{
expect_error(
hypotenuse("5", 12),
"Error in x\\^2 : non-numeric argument to binary operator"
)
}
)
The second argument of expect_error
takes a regular expression, which means that you need to escape the ^
, otherwise it means the start of the string. Regular expressions are useful to learn, though in this case you could get away with just matching the end of the error message, “non-numeric argument to binary operator”.