Expectations, Verified! Dive into the World of Unit Tests with expect_*()

Author : Murielle
Categories : development, package, tips
Tags : expect, Tests
Date :

Unit tests are essential in the development of an R package. They ensure that your functions work as expected while protecting you from regressions when you improve or modify your code.

Thanks to the {testthat} package, writing and automating tests in R becomes simple and intuitive. At the core of this approach are the expect_*() functions, which play a central role in validating the behaviors you expect from your code.

No time to read: What is it about?

In this article, we will explore the most used expect_*() functions, why they are indispensable, and how to implement them to write robust and maintainable tests.

First, let’s load the package:

library(testthat)

Why use expect_*()?

The expect_*() functions allow you to compare the actual behavior of your code with the expected results. Each test checks a specific “expectation” (hence the name “expect”), whether it’s a return value, an error, a warning, or even a user message. If a function does not meet this expectation, a test failure is reported, alerting you to a potential issue.

Let’s now look at the most commonly used expect_*() functions to test different aspects of your code.

expect_equal()

  • Use:

Checks that two objects are equal in value, allowing for minor differences (such as numerical imprecision).

  • Example:
test_that("Multiplication works", {
  result <- 2 * 3
  expect_equal(object = result, expected = 6)
})
#> Test passed 🎊

expect_identical()

  • Use:

Checks that two objects are strictly identical, including their attributes and structure.

  • Example:
test_that("Objects are strictly identical", {
  list1 <- list(a = 1, b = 2)
  list2 <- list(a = 1, b = 2)
  expect_identical(object = list1, expected = list2)
})
#> Test passed 🎉

Unlike expect_equal(), here the test fails if the objects differ in any attribute or detail, such as numerical precision:

test_that("expect_identical vs expect_equal with numerical tolerance", {
  num1 <- 0.1 + 0.2
  num2 <- 0.3
  # Test with expect_equal: This test will pass due to a small numerical tolerance
  expect_equal(object = num1, expected = num2)
  # Test with expect_identical: This test will fail because the comparison is strictly exact
  expect_identical(object = num1, expected = num2)
})

expect_true() et expect_false()

  • Use:

Checks that a logical expression is true or false.

  • Example:

test_that("True and false logic test", {
  expect_true(object = (1 + 1 == 2))
  expect_false(object = (1 + 1 == 3))
})
#> Test passed 🥇

expect_error()

  • Use:

Checks that a function triggers an error. You can also specify the expected error message for finer validation.

  • Example:
test_that("Error when input is negative", {
  expect_error(object = log("fake"), regexp = "non-numeric argument to mathematical function")
})

Here, the test only passes if running log("fake") generates an error with the message “non-numeric argument to mathematical function”.

⚠️ : Only take the error message without the “Error in … :” part.

expect_message()

  • Use:

Checks that a function generates a message via the message() function, thus validating that your code properly informs the user.

  • Example:
test_that("Message displayed correctly", {
  expect_message(object = message("Hello !"), regexp = "Hello !")
})
#> Test passed 😸

expect_length()

  • Use:

Checks that the length of an object (vectors, lists, or other objects) is equal to the expected value.

  • Example:
test_that("Vector has correct length", {
  vec <- c(1, 2, 3)
  expect_length(object = vec, n = 3)
})
#> Test passed 😀

expect_type()

  • Use:

Checks that the tested object is of the expected type (such as “double”, “character”, “data.frame”, etc.). This is useful for validating that functions return objects of the appropriate type.

  • Example:
test_that("Object type is correct", {
  num <- 42
  expect_type(object = num, type = "double")
  vec <- c(TRUE, FALSE, TRUE)
  expect_type(object = vec, type = "logical")
})
#> Test passed 😀

Conclusion: Test Your Expectations Precisely

The expect_*() functions from the {testthat} package are powerful tools for writing unit tests in R. They allow you to cover a wide range of use cases, whether it’s verifying numerical values, errors, warnings, or object types. By integrating these tests into your workflow, you increase the robustness of your code and simplify its long-term maintenance.

So, what are you waiting for? Dive into the world of unit tests, write your first expectations with expect_*(), and ensure that your R functions always meet the defined expectations!

To go further

For those who want to go further and master the art of creating robust packages, including best practices for unit testing, check out our training


Comments


Also read