Custom expectations

This vignette shows you how to create custom expectations that work identically to the built-in expect_ functions. Since these functions will need to be loaded when your package is loaded for testing, it is recommended that expect_ functions be defined in test-helpers.R in your packages R/ directory.

Creating an expectation

There are three main parts to writing an expectation, as illustrated by expect_length():

expect_length <- function(object, n) {
  # 1. Capture object and label
  act <- quasi_label(rlang::enquo(object), arg = "object")

  # 2. Call expect()
  act$n <- length(act$val)
  expect(
    act$n == n,
    sprintf("%s has length %i, not length %i.", act$lab, act$n, n)
  )

  # 3. Invisibly return the value
  invisible(act$val)
}

Quasi-labelling

The first step in any expectation is to capture the actual object, and generate a label for it to use if a failure occur. All testthat expectations support quasiquotation so that you can unquote variables. This makes it easier to generate good labels when the expectation is called from a function or within a for loop.

By convention, the first argument to every expect_ function is called object, and you capture it’s value (val) and label (lab) with act <- quasi_label(enquo(object)), where act is short for actual.

Verify the expectation

Next, you should verify the expectation. This often involves a little computation (here just figuring out the length), and you should typically store the results back into the act object.

Next you call expect(). This has two arguments:

  1. ok: was the expectation successful? This is usually easy to write

  2. failure_message: What informative error message should be reported to the user so that they can diagnose the problem. This is often hard to write!

    For historical reasons, most built-in expectations generate these with sprintf(), but today I’d recommend using the glue package

Invisibly return the input

Expectation functions are called primarily for their side-effects (triggering a failure), so should invisibly return their input, act$val. This allows expectations to be chained:

mtcars %>%
  expect_type("list") %>%
  expect_s3_class("data.frame") %>% 
  expect_length(11)

succeed() and fail()

For expectations with more complex logic governing when success or failure occurs, you can use succeed() and fail(). These are simple wrappers around expect() that allow you to write code that looks like this:

expect_length <- function(object, n) {
  act <- quasi_label(rlang::enquo(object), arg = "object")

  act$n <- length(act$val)
  if (act$n == n) {
    succeed()
    return(invisible(act$val))
  }

  message <- sprintf("%s has length %i, not length %i.", act$lab, act$n, n)
  fail(message)
}

Testing your expectations

Use the expectations expect_success() and expect_failure() to test your expectation.

test_that("length computed correctly", {
  expect_success(expect_length(1, 1))
  expect_failure(expect_length(1, 2), "has length 1, not length 2.")
  expect_success(expect_length(1:10, 10))
  expect_success(expect_length(letters[1:5], 5))
})
#> Test passed 🥇