You might be familiar with unit testing using the testthat package. The goal of unit tests is to check if your function is developed correctly. An assertion checks if your function is used correctly.

Unit tests are meant for the developer and are executed on command. Assertions are meant for the user and are executed at every function call.

Fail fast

A run-time test, commonly known as an assertion, is a tiny piece of code that checks a condition and fails if the condition isn’t met. Assertions enable your code to fail fast — and that’s a good thing. When something is off we don’t want our program to continue until the error compounds and reveals itself down the line. On the contrary, we want the program to fail immediately at the source of the error with a clear and precise error message.

This goal of this article is to make your R functions fail fast. Assuming your function has no bugs or side-effects, the only way an error can creep into your function is through its input. In this article, you will learn how to use assertions to recognize bad input and warn the user (including future you).

There are two ways for input to be bad:

  1. The function input contains errors. Some errors are spotted by R: from impossible or missing values to inconsistent use of commas and points in decimal numbers. Other errors are more insidious. Think about invalid credit card numbers or non-existing postal codes and bad email formats.

  2. You developed your function with a particular use case in mind. You can’t imagine all the ways a user will try to use your code. He or she may use your function with technically correct input but not in the intended way. Have you tried sorting a list with sort.list(list(2, 1, 3))?

Why the R package “assertive”

R has several packages for writing assertions. This article advocates the R package assertive for three reasons:

  1. the package provides lots of functions for many different cases
  2. its functions are very easy to read
assert_all_are_positive(c(1, -2, 3))
  1. and provide highly informative error messages
Error: is_positive : c(1, -2, 3) contains non-positive values.
There was 1 failure:
 Position Value Cause
1 2 -2 too low

Install the package directly from CRAN and load it into your R session:

install.packages("assertive")
library(assertive)

Writing your first assertion

Imagine you wrote a function that sums the elements of a numeric vector:

add_numbers <- function(numbers) {
  result <- sum(numbers)
  return(result)
}

The values of the vector are allowed to be anything as long as they are numbers. Let’s write an assertion at the beginning of the function to check our input vector:

add_numbers <- function(numbers) {
  assert_is_numeric(numbers)
  result <- sum(numbers)
  return(result)
}add_numbers(c(1, 2, 2, 3)) # pass

Note that assertion is written inside the function. This means it’s always there, waiting to be executed each time the function is called. Checking an assertion only takes a couple of milliseconds which is fine for all but the most performance-critical applications.

Checking multiple conditions

Imagine you modified your function to expect a vector of unique values. Now you need two assertions:

  1. one to check that the vector is numeric
  2. another one to check that the vector has no duplicate values

You can keep your code readable by chaining two or more assertions with the forward pipe %>% from the magrittr package:

library(magrittr)add_unique_numbers <- function(input_vector) {
  input_vector %>% 
    assert_is_numeric %>%
    assert_has_no_duplicates
  result <- sum(input_vector)
  return(result)
}

add_unique_numbers(c(1.5, 2, 2, 3)) # fail

There are a lot of assertive functions for a wide range of cases. It’s not practical to memorize them all. Just search the package documentation on CRAN to find a function that suits your particular need.

Let’s try this with an exercise. The function below expects a vector of percentage values, e.g.percentages <- c(64, 55, 97, 85) and computes the mean percentage. Can you find an appropriate assertion in the assertive package?

calculate_mean_percentage <- function(percentages) {
    # ... your assertion goes here 
    result <- mean(percentages)
    return(result)
}

Tip: write ls("package:assertive", pattern = "percent") in your R console to search for functions in the assertive whose name matches “percent”.

Of assertions and predicates

You may start to see a pattern here. All assertions are composed of three parts:

  1. they always begin with assert_
  2. followed by the predicate is_ or has_
  3. and end with an expectation, e.g. numeric

The predicate is_ changes to all_are_ or any_are_ in when the individual elements of an object are checked instead of the object itself. For example, the assertion below checks if all numbers passed to the function are whole numbers:

add_whole_numbers <- function(whole_numbers) {
  assert_all_are_whole_numbers(whole_numbers)
  result <- sum(whole_numbers)
  return(result)
}

add_unique_numbers(c(1.5, 2, 2, 3)) # fail

Beyond checking numbers

So far we’ve only dealt with numbers. But the assertive package contains assertions for all kind of cases. To illustrate, we’ll finish our discussion with some examples to check text, dates and even the host operating system.

Does your function expect a vector of personal names? Check for missing or empty strings:

assert_all_are_non_missing_nor_empty_character()

Have a function that only works on Windows? Then make sure the user is running Windows:

assert_is_windows()

Does your function expect a person’s date of birth? Make sure it’s in the past:

assert_is_in_past()

Conclusion

In this article, you learned how to write assertions using the assertive package. Thanks to assertions your functions will do what they are supposed to do or fail fast with a clear error message.


References

Cotton, Richard. (2017). Testing R code (1st Edition). Chapman & Hall.

Hunt, Andrew. Thomas, Dave. (1999). The Pragmatic Programmer (25th Printing, February 2010). Addison Wesley Longman, Inc.