Learning Objectives
- Understand how to write a basic test function.
- Understand how to write custom error messages in a function.
- Understand multiple ways to debug your code.
Suggested Readings
- Chapter 11 in Jenny Bryan and Jim Hester’s fantastic book, “What They Forgot to Teach You About R”.
Here are some other great resources: > * Debugging techniques in RStudio - Amanda Gadrow’s talk at rstudio::conf 2018. > * Debugging in RStudio article
Writing test cases is part of the process of understanding a problem; if you don’t know what the result of an example input should be, you can’t know how to solve the problem.
Test cases are also used to verify that a solution to a problem is correct – that it works as expected. Without a good set of test cases, we have no idea whether our code actually works!
Test cases vary based on the problem, but you generally want to ensure that you have at least one or two of each of the following test case types:
n < 2
, two important edge cases are when
n = 2
and n = 3
, which trigger different
behaviors. Other edge cases include the first / last characters in a
string or items in a list.0
and
1
for integers, the empty string (""
), and
input values of different types than are expected.TRUE
and
FALSE
cases among your tests!stopifnot()
The stopifnot()
function does what you might expect - it
stops the function if whatever is inside the ()
is not
TRUE
. Let’s look at an example.
Consider the function isEvenNumber()
, which takes a
numeric value and returns TRUE
if it is an even number and
FALSE
otherwise:
isEvenNumber <- function(n) {
# A number is "even" if it is divisible by 2 with no remainder
remainder <- n %% 2
return(remainder == 0)
}
Here is a simple test function for isEvenNumber()
that
uses the stopifnot()
function to examine the following two
test cases:
isEvenNumber(42)
to be
TRUE
isEvenNumber(43)
to be
FALSE
testIsEvenNumber <- function() {
cat("Testing isEvenNumber()... ")
stopifnot(isEvenNumber(42) == TRUE)
stopifnot(isEvenNumber(43) == FALSE)
cat("Passed!\n")
}
In this test function, we called the stopifnot()
function and used the ==
operator to assess whether the
output of isEvenNumber()
is equal to the value we expected.
We can run these cases by simply calling our test function:
testIsEvenNumber()
#> Testing isEvenNumber()... Passed!
The two test cases we used for isEvenNumber()
are
“normal” cases because they use typical inputs and test for expected
outputs. A better test function would also include a few other
statements to test other points of failure.
One particular common error is when a user inputs the wrong data type to a function:
isEvenNumber('42')
#> Error in n%%2: non-numeric argument to binary operator
Here we’ve input a string instead of a number, and R sent us an error message. To account for this possibility, we can modify our function and test function:
isEvenNumber <- function(n) {
# First make sure the input is a numeric type
if (! is.numeric(n)) {
return(FALSE)
}
remainder <- n %% 2
return(remainder == 0)
}
testIsEvenNumber <- function() {
cat("Testing isEvenNumber()... ")
stopifnot(isEvenNumber(42) == TRUE)
stopifnot(isEvenNumber(43) == FALSE)
stopifnot(isEvenNumber('not_a_number') == FALSE)
cat("Passed!\n")
}
testIsEvenNumber()
#> Testing isEvenNumber()... Passed!
stop()
Another approach to checking input types is to explicitly provide a
better error message so the user can know what went wrong. For example,
rather than return FALSE
when we input a string to
isEvenNumber()
, we can use stop()
to halt the
function and send an error message:
isEvenNumber <- function(n) {
if (! is.numeric(n)) {
stop('Oops! This function requires numeric inputs!')
}
remainder <- n %% 2
return(remainder == 0)
}
isEvenNumber('42')
#> Error in isEvenNumber("42"): Oops! This function requires numeric inputs!
Bugs are a natural part of the programming process. However, you can reduce the number of bugs you encounter by following a few tips:
The most common case you will run into a bug is when writing new code yourself. Often the mistake is obvious and easily fixed, but sometimes it only appears after multiple levels of calls and is harder to diagnose. There are a few common strategies to use when debugging your own code.
traceback()
to determine where a given error is
occurring.print()
,
cat()
or message()
statements.browser()
to open an interactive debugger before
the errordebug()
to automatically open a debugger at the
start of a function call.trace()
to start a debugger at a location inside a
function.The traceback()
function can be used to print a summary
of how your program arrived at the error. This is also called a call
stack, stack trace or backtrace.
In R this gives you each call that lead up to the error, which can be very useful for determining what lead to the error.
You can use traceback()
in two different ways, either by
calling it immediately after the error has occurred.
f <- function(x) {
return(x + 1)
}
g <- function(x) {
return(f(x) - 1)
}
g("a")
#> Error in x + 1 : non-numeric argument to binary operator
traceback()
#> 2: f(x) at #1
#> 1: g("a")
Or by using traceback()
as an error handler, which will
call it immediately on any error. (You could even put this in your .Rprofile
)
options(error = traceback)
g("a")
#> Error in x + 1 : non-numeric argument to binary operator
#> 2: f(x) at #1
#> 1: g("a")
print()
Once you know where an error occurs it is then helpful to know why. Often errors occur because functions are given inputs their authors did not expect, so it is useful to print the value of objects during execution.
The most basic way to do this is to sprinkle messages throughout your
code, with print()
or str()
.
str()
is often more useful because it gives more detail
into the exact structure of an object, which may not be the structure
you expect it to be.
The main downsides to the print approach is you often have to add them in multiple places to narrow down the error, and you cannot further investigate the object.
browser()
A more sophisticated debugging method is to put a call to
browser()
in your code. This will stop execution at that
point and open R’s interactive debugger. In the debugger you can run any
R command to look at objects in the current environment, modify them and
continue executing.
Some useful things to do are
ls()
to determine what objects are available in the
current environment. This allows you to see exactly what things you can
examine.str()
, print()
etc. to examine the
objectsn
to evaluate the next statement. Use
s
to evaluate the next statement, but step into function
calls.where
to print a stack
tracec
to leave the debugger and continue executionQ
to exit the debugger and return to the R
prompt.RStudio provides some additional tooling for debugging over using R
on the command line. First you can set an editor breakpoint by clicking
to the left of the line number in the source file, or by pressing
Shift+F9
with your cursor on the line. A breakpoint is
equivalent to a browser()
call, but you avoid needing to
change your code like browser()
.
If you are trying to hunt down a particular error it is often useful
to have RStudio enter the debugger when it occurs. You can control the
error behavior with
(Debug -> On Error -> Error Inspector
).
The RStudio debugging console has a few buttons to make debugging a
little nicer, From left to right they are, next (equivalent to
n
), step info (s
), continue (c
)
and Stop (Q
).
Page sources:
Some content on this page has been modified from other courses, including: