Testing Inside a Function

Why test inside a function?

It’s important to have tests that will serve as a sanity check inside our functions. These tests are usually made to throw an error if something is wrong with the entered parameters, which are passed to a function call. For instance, it can be valuable to check if an parameter is of character type or if all the necessary columns are present in a data.frame.

To make your life easier we developed the g6tr.ui package. This package contains a set of default functions to help you with writing tests. These functions allows you to catch the most common errors and will produce standardized and explicit error messages.

What and how to test inside a function with g6tr.ui

In general, you should write tests for all of your functions' parameters.

Checking parameter class

A typical error is entering an parameter that has the wrong class. For instance your function is expecting a numeric value and you pass x = c("1", "2"). You can deal with this using the check_class() function like in the code snippet:

fun1 <- function(x){
  check_class(x, arg_class = "numeric")
  x + 10
}

Checking if a parameter is scalar

Many times our parameter can take only a single value that is exactly 1 length. This is called a scalar. A scalar can be of different classes: numeric, integer, boolean, character. If you are dealing with parameters that needs to be scalar you can use: check_scalar() or special case for boolean check_bool():

fun2 <- function(x, y){
  check_scalar(x, arg_class = "character")
  check_bool(y)
  
  if(y){
    print(paste("one", x))
  }
}

Checking strings

We may need to check if a character parameter has the right values. There are three possible cases here:

  • We may expect that the character vector will take all predefined values, but also can take more, that are not of our interest.
  • We may expect that the character vector will take some values from a pool of allowed values.
  • We may expect that the character vector will take exactly the predefined values.

Now when dealing with a parameter that takes character vector, in most cases you will deal with 2^nd^ case. You can use check_allowed_strings() in this case.

fun3 <- function(x, y, z){
  check_scalar(x, "numeric")
  check_scalar(y, "numeric")
  check_allowed_strings(z,  c("add", "multiply"))

  if(z == "multiply"){
    return(x * y}
  } if else(z == "add"){
    return(x + y)
  }

}

Now you may wonder when can we expect the 1^st^ and 3^rd^ case? Actually quite often. But probably the most obvious case is when we are doing analyses. We usually divide our work into read -> assert -> tidy -> model workflow. Each of the outputs has its own specific structure (it might be a named list, or a data frame with standardized names for columns). This is where the functions come handy. We can check if the input has not only the proper class, but if there are elements that we need (like proper elements of list, or proper names of columns).

data_read <- read_data(x) {
  check_class(x, "data.frame")
  mandatory_colnames <- c("respondent_id", "m1_cjt_2", "m1_cjt_2")
  check_needed_strings(names(x), mandatory_colnames)
  
  y <- vector("list", 2)
  names(y) <- c("data", "n_resp")
  
  y$data <- x %>%
    select(all_of(mandatory_colnames))
    
  y$n_resp <- x %>%
    select("respondent_id") %>%
    unique %>%
    nrow
}

data_asserted <- assert_data(x) {
  check_class(x, "list")
  check_exact_strings(names(x), c("data", "n_resp"))
  check_class(x$data, "data.frame")
  check_exact_strings(
    names(x$data), c("respondent_id", "m1_cjt_2", "m1_cjt_2")
  )
  check_class(x$n_resp, "numeric")
  
  fun_assert_1(x)
  fun_assert_2(x)
  ...
}

As you can see the test may sometimes get a little bit more complex. But in the long run it helps us avoid mistakes when they are cheap (before any serious computation).

Other common tests

The above cases are probably the most common tests used by our dev team. However, there are also other functions that might come in handy from time to time:

  • check_color() – checks if the provided strings are valid color names (both as a common name or a hash value);
  • check_dir() and check_file() – check if the file or directory exists;
  • check_inequality() – checks if the parameter value is greater or lower than some boundary value;
  • check_length() – checks the length of the parameter. For parameters of expected length value 1 (most common case), it is included in check_scalar() function;
  • check_scalar_sign() – checks if the parameter is of length 1, and if it is positive or negative (optionally if the parameter is non-negative or non-positive).

Testing multiple parameters at once

In more complex functions, with several parameters (a good example here are our plotting functions), we might want to do exactly the same checks for different parameters. If we test everything one by one, the code would become cluttered. To avoid this we developed the check_multiple() function, that will run the same check on multiple parameter for you. The only trick here is that you need to wrap unquoted parameter names with the check_args() function, and correctly name all the sub-function parameters. If this is not clear please check the example below.

plot_fun <- function(data, color, fill, bg_color, title_size, axis_size) {

check_class(data, "data.frame")
check_needed_strings(data, c("group", "percentage"))
check_multiple(check_args(color, fill, bg_color), check_color)
check_multiple(check_args(title_size, axis_size), check_scalar, arg_class = "numeric")

data %>%
 ggplot(aes(x = group, y = percentage)) +
 geom_point(color = color, fill = fill) +
 labs(title = "Plot function") +
 theme(
   plot.title = element_text(size = title_size),
   axis.text.x = element_text(size = axis_size),
   axis.text.y = element_text(size = axis_size),
   panel_background = element_rect(fill = bg_color)
 )
}