Error Messaging

Slides from Hadley Wickham’s Masterclass for R Package Development

Unable to display PDF file. Download instead.

R script for slides

General Structure for Unit Testing

  • Use small unit testing chunks test_that("<descriptive text>", {...})

  • While it can be different for various functions, but one chunk per function argument is often sufficient

  • Be purposeful when creating snapshot tests. For example, if you’re testing an ARD object correctly captured the error messages, you don’t need to snapshot the entire ARD data frame.

  • Do not snapshot printed tibbles or card objects. Each of these has a print method that truncates rows and columns, which will not be captured in a snapshot. Moreover, we don’t want small changes to a print method to break snapshot tests.

  • Snapshot tests must be human readable. Note that I DO NOT consider a printed data frame with breaks across the column to be human readable. If needed, break a snapshot test into chunks, e.g. snapshot the first few columns, followed by a snapshot with the remaining columns.

  • REVIEW THE SNAPS BEFORE COMMITTING. Not only are you going to inspect the results in the console while writing a snapshot test, you must also review the actual snapshot test. Pay particular attention to these notes from tidyselect, that for now, will only appear in a testing frame work (i.e. when unit tests are run in their own environment, not interactively).

      Warning:
      Use of .data in tidyselect expressions was deprecated in tidyselect 1.2.0.
      i Please use `"variable_level"` instead of `.data$variable_level`

    You can use the options(lifecycle_verbosity = "error") option to convert these testing-only messages to errors, which makes them much easier to track down.

  • Should each snapshot test live in its own chunk? That will give a proper heading to each of the snapshots, so we know what is being tested by that snapshot.

Testing Error Messages

There are various ways to check appropriate error messaging, and snapshot testing is my favorite. This ensures the exact error you’re planning for is the one returned.

expect_snapshot(
  error = TRUE, 
  <expr>
)

Calling Environments

We have a somewhat complex messaging situation. {cards} functions are going to be called from {cardx} and {gtsummary}: in these cases, we want the user to be messaged about the function they ran, even if the error is thrown from {cards}.

To handle these types of scenarios, we set the calling environment at the top of every user-facing function. In the near future, we’ll be implementing something like this:

# function called by user
user_facing_function <- function() {
  cards:::set_cli_abort_call()
  check_for_errors()
}

# an internal function OR exported function from another pkg that throws the error
check_for_errors <- function() {
  cli::cli_abort(c("!" = "This is an error!", "i" = "Be better"), call = cards:::get_cli_abort_call())
}

# this exhibits the user experience
user_facing_function()
#> Error in `user_facing_function()`:
#> ! This is an error!
#> ℹ Be better

# check the default still work when no option is set
check_for_errors()
#> Error in `check_for_errors()`:
#> ! This is an error!
#> ℹ Be better

The {cards}, {cardx}, and {gtsummary} packages all already have the set_cli_abort_call() and get_cli_abort_call() in them, so there is no need to reference these functions with the triple colon.

Checking Functions

Each of the packages has a script called R/import-standalone-checks.R. This file contains many functions for checking function inputs. When writing a new function, be sure to utilize these functions to ensure the users pass valid arguments.