Skip to contents

Why to test inside function

Most of the functions have at least one function parameter that can be controlled by the user. Usually the parameters have some limits – they might be limited to some class or some particular value. Or can take one of predefined values. If user passes wrong value to the parameter, in most cases the function throws an error. Or we want the function to throw an error. Default error messages do not look good, or sometimes they are not very informative, so we rather want to use our own standard error messages. For instance, look what happens when you try to multiply character by numeric:

2 * "a"

[1m
[33mError
[39m in `2 * "a"`:
[22m

[33m!
[39m non-numeric argument to binary operator

How wonderful would it be if the message looked like:


[1m
[33mError
[39m:
[22m

[1m
[22m
[31m✖
[39m Cannot multiply 
[34m<numeric>
[39m by 
[34m<character>
[39m

So take home message is: we use internal tests to assure that user passes only allowed values to parameters. If they pass wrong values then we show nicely formatted and easy to understand error messages.

g6tr.ui tests

There is a huge amount of possible test that we can run. However, some of them are more common. In g6tr.ui we focused on the tests that we use very often. We wrapped them into test functions that are easy to use, and have informative names. The most important is to check arguments as soon as possible within function, so if anything is wrong the function will fail quickly.

The whole function flow can be generalized as:

new_function <- function(arg_a, arg_b, arg_c){
  #test arguments
  check_class(arg_a, "character")
  check_length(arg_b, arg_length = 2L)
  check_scalar(arg_c, arg_class = "integer")
  #...
  #function body
  #function output
}

Our tests can be divided into two groups: single tests and complex tests. Complex tests are wrappers around more than one single test or test more than one thing at once or are wrappers allowing to run same test for multiple arguments.

Single tests

For all the simple tests (besides check_file and check_dir) it is possible to pass additional message that will be displayed when function throws an error.

Complex tests

  • check_scalar() – checks if argument is of expected class and has length 1;
  • check_scalar_sign() – checks if argument is a numeric of length 1 and is negative/positive;
  • check_strings() – wrapper over check_exact_strings, check_needed_strings and check_allowed_strings;
  • check_multiple() – allows running same test over multiple arguments.
  • check_null_value() – checks if argument is NULL OR a certain value.

How Stuff Works

Easiest way to grasp how to use the functions is by looking at examples. Below you will see examples for each of the functions. Some of them have additional arguments like alt_null or add_msg. In the examples we do not cover all the possible use cases, so every time you should refer to function documentation and examples where you will find all the necessary information.

check_bool

Use this if you expect single TRUE or FALSE. As an alternative you can allow the argument to be NULL.

lgl_v <- c(TRUE, FALSE)
nl1 <- NULL
tr1 <- TRUE
check_bool(x = lgl_v)

[1m
[33mError
[39m in `check_bool()`:
[22m

[1m
[22m
[33m!
[39m `lgl_v` should be either `TRUE` or `FALSE`

[31m✖
[39m `lgl_v` is a 
[34m<logical>
[39m vector, but should be a scalar (single value)
check_bool(x = tr1) # returns invisible NULL
check_bool(nl1)

[1m
[33mError
[39m in `check_bool()`:
[22m

[1m
[22m
[33m!
[39m `nl1` should be either `TRUE` or `FALSE`

[31m✖
[39m `nl1` is 
[34m<NULL>
[39m
check_bool(nl1, alt_null = TRUE) # returns invisible NULL

check_class

This function checks if class of the argument matches the expected class. As an alternative you can allow the argument to be NULL.

character_s <- "x"
character_v <- c("x", "1")
df_1 <- data.frame(x = 1:4)
check_class(character_s, arg_class = "logical")

[1m
[33mError
[39m in `check_class()`:
[22m

[1m
[22m
[33m!
[39m `character_s` should be of class 
[34m<logical>
[39m

[31m✖
[39m `character_s` is 
[34m<character>
[39m
check_class(character_v, arg_class = "numeric")

[1m
[33mError
[39m in `check_class()`:
[22m

[1m
[22m
[33m!
[39m `character_v` should be of class 
[34m<numeric>
[39m

[31m✖
[39m `character_v` is 
[34m<character>
[39m
check_class(df_1,  arg_class = "data.frame")

check_color

Useful function for any plotting function that allows user to define colors. Additionally you can pass additional message.

wrong_color <- c("sausage", "green")
check_color(wrong_color,add_msg = "God I Love {wrong_colors}")

[1m
[33mError
[39m in `check_color()`:
[22m

[1m
[22m
[36mℹ
[39m God I Love sausage

[33m!
[39m `wrong_color` should contain only valid color names or color's hex values

[31m✖
[39m `wrong_color` contains 1 value that is not color: 
[34m"sausage"
[39m

[36mℹ
[39m Use `colors()` to get the full list of color names

check_file and check_dir

check_file("xyz.Rmd", exists = TRUE, msg = "error")

[1m
[33mError
[39m in `check_file()`:
[22m

[1m
[22m
[33m!
[39m File 
[34mxyz.Rmd
[39m does not exist. Aborting operation.
check_file("no_file_for_old.Men", exists = TRUE, msg = "error")

[1m
[33mError
[39m in `check_file()`:
[22m

[1m
[22m
[33m!
[39m File 
[34mno_file_for_old.Men
[39m does not exist. Aborting operation.
check_dir("no_directory", create = FALSE)

[1m
[33mError
[39m in `check_dir()`:
[22m

[1m
[22m
[33m!
[39m Directory 
[34mno_directory
[39m does not exist. Aborting operation.

check_length

Simply checks if the argument has expected length. Usually we want to check for length to be 1. But also might get handy in some plotting functions where we expect the length of colors vector to be same length as number of groups.

n1 <- 1.5
c3 <- c("a", "w", "z")
check_length(n1, arg_length = 2L)

[1m
[33mError
[39m in `check_length()`:
[22m

[1m
[22m
[33m!
[39m `n1` should be of length 
[34m2
[39m

[31m✖
[39m `n1` is of length 
[34m1
[39m
check_length(c3, arg_length = 1L)

[1m
[33mError
[39m in `check_length()`:
[22m

[1m
[22m
[33m!
[39m `c3` should be of length 
[34m1
[39m

[31m✖
[39m `c3` is of length 
[34m3
[39m

check_allowed_strings, check_exact_strings or check_needed_strings

These three functions operate with character vectors. The check_allowed_strings throws an error when the argument contains any string that is outside of allowed strings. However it won’t throw an error when the argument doesn’t contain all of the allowed strings.

fail_allowed <- c("I", "don't", "like", "pvalues")
pass_allowed <- "pvalues"
check_allowed_strings(fail_allowed, allowed_strings = c("I", "like", "pvalues"))

[1m
[33mError
[39m in `check_allowed_strings()`:
[22m

[1m
[22m
[31m✖
[39m `fail_allowed` has the following unwanted string: 
[34m"don't"
[39m.
check_allowed_strings(pass_allowed, allowed_strings = c("I", "like", "pvalues"))

The check_exact_strings() function expects the whole string vector to match the expected strings. This might be handy if we pass a list with some elements, and we expect that it has exactly the elements we need. Since it checks that two character vectors have the same elements, it will also inform you about lacking strings as well as about additional strings.

fail_list <- list(a = 1, b = 2, d = 4)
fail_list2 <- list(a = 1, b = 2, e = 4)
pass_list <- list(a = 4, b = 3, c = 2, d = 1)
check_exact_strings(names(fail_list), c("a", "b", "c", "d"))

[1m
[33mError
[39m in `check_exact_strings()`:
[22m

[1m
[22m
[31m✖
[39m `names(fail_list)` is missing the following string: 
[34m"c"
[39m.

[32m✔
[39m `names(fail_list)` has no unwanted strings.
check_exact_strings(names(fail_list2), c("a", "b", "c", "d"))

[1m
[33mError
[39m in `check_exact_strings()`:
[22m

[1m
[22m
[31m✖
[39m `names(fail_list2)` is missing the following strings: 
[34m"c"
[39m and 
[34m"d"
[39m.

[31m✖
[39m `names(fail_list2)` has the following unwanted string: 
[34m"e"
[39m.
check_exact_strings(names(pass_list), c("a", "b", "c", "d"))

In the case of check_needed_strings() it is meant to check if argument contains all the needed strings, but it does not care about any additional strings. A good example when it comes handy is when we want data.frame to have some specific columns (e.g. when we expect response_id and weight), but are flexible in the case of others.

fail_df <- data.frame(response_id = 1:3, m1_q1 = 2:4, m2_q1 = 5:7)
pass_df <- data.frame(response_id = 1:3, weight = rep(1.5,3), m5_q1 = 9:11)
check_needed_strings(names(fail_df), c("response_id", "weight"))

[1m
[33mError
[39m in `check_needed_strings()`:
[22m

[1m
[22m
[31m✖
[39m `names(fail_df)` is missing the following string: 
[34m"weight"
[39m.
check_needed_strings(names(pass_df), c("response_id", "weight"))

check_inequality

This function checks if argument is greater or smaller than expected value. By setting arg_equal = TRUE the test will pass if the argument is equal to the expected value.

n1 <- 666.67
n2 <- -3.14
check_inequality(n1, arg_inequality = "greater", arg_value = 667)

[1m
[33mError
[39m in `check_inequality()`:
[22m

[1m
[22m
[33m!
[39m `n1` should be greater than 
[34m667
[39m

[31m✖
[39m `n1` is 
[34m666.67
[39m
check_inequality(n2, arg_inequality = "lower", arg_equal = TRUE, arg_value = -3.14)

check_scalar

First complex function check if argument is scalar. It means that it will check if the argument is of proper class and it’s length is exactly 1.

numeric_v <- c(2.43, 11.4)
numeric_s <- 3.14
check_scalar(numeric_v, arg_class = "numeric")

[1m
[33mError
[39m:
[22m

[1m
[22m
[36mℹ
[39m `numeric_v` should be 
[34m<numeric>
[39m of length 
[34m1
[39m

[33m!
[39m `numeric_v` should be of length 
[34m1
[39m

[31m✖
[39m `numeric_v` is of length 
[34m2
[39m
check_scalar(numeric_s, arg_class = "logical")

[1m
[33mError
[39m:
[22m

[1m
[22m
[36mℹ
[39m `numeric_s` should be 
[34m<logical>
[39m of length 
[34m1
[39m

[33m!
[39m `numeric_s` should be of class 
[34m<logical>
[39m

[31m✖
[39m `numeric_s` is 
[34m<numeric>
[39m

check_scalar_sign

The check_scalar_sign in addition to checking if argument is a scalar, it will also check if it is positive or negative. By setting arg_equal = TRUE it will rather check if the scalar is non-positive or non-negative. It is also possible to allow the argument to be NULL.

pos_s <- 44
neg_s <- -11
zero_s <- 0
nl_s <- NULL
check_scalar_sign(pos_s, arg_sign = "negative")

[1m
[33mError
[39m in `check_scalar_sign()`:
[22m

[1m
[22m
[33m!
[39m `pos_s` should be smaller than 
[34m"0"
[39m

[31m✖
[39m `pos_s` is 
[34m44
[39m
check_scalar_sign(neg_s, arg_sign = "positive")

[1m
[33mError
[39m in `check_scalar_sign()`:
[22m

[1m
[22m
[33m!
[39m `neg_s` should be greater than 
[34m"0"
[39m

[31m✖
[39m `neg_s` is 
[34m-11
[39m
check_scalar_sign(zero_s, arg_sign = "negative", arg_equal = TRUE)
check_scalar_sign(nl_s, arg_sign = "positive", alt_null = TRUE)

[1m
[33mError
[39m in `check_scalar_sign()`:
[22m

[1m
[22m
[33m!
[39m `nl_s` should be of length 
[34m1
[39m

[31m✖
[39m `nl_s` is of length 
[34m0
[39m

check_multiple

Allows to use same test over multiple variables. As soon as the check fails, it will abort and throw error. It means that if more than one argument is wrong, you will still need to work them one by one. This function additionally requires user to pass the arguments wrapped into check_arg function (e.g. check_multiple(check_arg(arg1, arg2), check_function, arg_check_function = x)))

n1 <- 1
n2 <- c(1,2)
n3 <- c(1,2,3)
check_multiple(check_args(n1, n2, n3), check_length, arg_length = 1L)

[1m
[33mError
[39m:
[22m

[1m
[22m
[33m!
[39m `n2` should be of length 
[34m1
[39m

[31m✖
[39m `n2` is of length 
[34m2
[39m
n1 <- 1
n2 <- c(2)
n3 <- c(1,2,3)
check_multiple(check_args(n1, n2, n3), check_length, arg_length = 1L)

[1m
[33mError
[39m:
[22m

[1m
[22m
[33m!
[39m `n3` should be of length 
[34m1
[39m

[31m✖
[39m `n3` is of length 
[34m3
[39m
check_multiple(check_args(character_v, character_s), check_class, arg_class = "integer")

[1m
[33mError
[39m:
[22m

[1m
[22m
[33m!
[39m `character_v` should be of class 
[34m<integer>
[39m

[31m✖
[39m `character_v` is 
[34m<character>
[39m

check_strings

Is a wrapper over functions that check if character vector contains allowed, exact or needed strings. It also needs you to wrap the argument with check_args function.

check_strings(check_args(fail_allowed), expected_strings = c("don't"), type = "allowed")

[1m
[33mError
[39m:
[22m

[1m
[22m
[31m✖
[39m `fail_allowed` has the following unwanted strings: 
[34m"I"
[39m, 
[34m"like"
[39m, and
  
[34m"pvalues"
[39m.
check_strings(check_args(names(fail_list)), expected_strings = c("a", "b"), type = "exact")

[1m
[33mError
[39m:
[22m

[1m
[22m
[32m✔
[39m `names(fail_list)` has all the expected strings.

[31m✖
[39m `names(fail_list)` has the following unwanted string: 
[34m"d"
[39m.
check_strings(check_args(names(fail_df)), expected_strings = c("response_id", "weight"), type = "needed")

[1m
[33mError
[39m:
[22m

[1m
[22m
[31m✖
[39m `names(fail_df)` is missing the following string: 
[34m"weight"
[39m.

check_null_value

This function’s fate is still uncertain. It is meant to check if an argument is NULL and if not if it has a certain value. Since this is very limited scope, we might remove it from package to avoid clutter.

nl1 <- NULL
c1 <- "a"
n1 <- 11L
check_null_value(x = nl1, arg_value = 11)
check_null_value(x = n1, arg_value = 11) #fails since we pass numeric value and it expects integer

[1m
[33mError
[39m in `check_null_value()`:
[22m

[1m
[22m
[33m!
[39m `n1` should be of value 
[34m11
[39m or 
[34m<NULL>
[39m

[31m✖
[39m `n1` is 
[34m<11>
[39m
check_null_value(x = c1, arg_value = "a")