R语言 单元测试
单元测试基本上是测试和帮助编写健壮代码的小功能。我们所说的健壮的代码是指在改变时不会轻易破坏的代码,可以简单地重构,可以在不破坏其他部分的情况下进行扩展,并且可以轻松地进行测试。当涉及到动态类型的脚本语言时,单元测试是非常有用的,因为没有编译器的帮助,无法显示函数可能以无效参数被调用的地方。
单元测试是如何工作的
一个简单的函数是做什么的,它接受一些输入x并返回一个输出y。在单元测试中,我们验证函数的精确性和正确性,在调用函数时,对于特定的x值返回y的预期值。一般来说,要测试不同的(x,y)对。如果我们的代码有副作用,例如读/写文件,访问一些数据库,等等,那么单元测试是如何工作的。在这种情况下,测试的准备工作就比较复杂了。它可能只是由一堆模拟函数的对象组成,用于模拟对数据库的访问。这影响了编程风格,抽象层可能成为必要的。在某些情况下,在执行测试前需要生成输入文件,而在测试后需要检查输出文件。单元测试的基本思想很简单,你写一个脚本,自动评估你的代码片段,并根据预期行为进行检查。现在让我们看看一些例子,以便更好地理解单元测试的实际含义和工作方式。
在R中的实现
在R编程中, testthat 包帮助我们在代码中实现单元测试。要安装 testthat 包,只需要在他的R控制台运行以下代码。
if (!require(testthat)) install.packages('testthat')
testthat 使用 test_that() 函数来创建一个测试,并使用期望值对代码进行单元测试。一个期望值允许我们断言由一个函数返回的值与我们应该得到的值相匹配。
test_that("Message to be displayed",
{ expect_equal(function_f(input x), expected_output_y)
expect_equivalent(function_f(input x), expected_output_y)
expect_identical(function_f(input x), expected_output_y)
.
.
.
})
testthat 软件包里有20多个期望。
论点 | 期望值 |
---|---|
expect_lt(), expect_lte(), expect_gt(), expect_gte() | 返回值是否小于或大于指定值? |
expect_equal(), expect_identical() | 一个对象是否等于一个参考值? |
expect_error(), expect_warning(), expect_message(), expect_condition() | 代码是否抛出一个错误、警告、消息或其他条件? |
expect_invisible(), expect_visible() | 表达式是有形的还是无形的返回? |
skip(), skip_if_not(), skip_if(), skip_if_not_installed(), skip_if_offline(), skip_on_cran(), skip_on_os(), skip_on_travis(), skip_on_appveyor(), skip_on_ci(), skip_on_covr(), skip_on_bioc(), skip_if_translated() | 跳过一个测试。 |
expect_length() | 一个向量是否具有指定的长度? |
expect_match() | 字符串是否与正则表达式匹配? |
expect_named() | 对象是否有名字? |
expect_setequal(), expect_mapequal() | 两个向量是否包含相同的值? |
expect_output() | 代码是否打印输出到控制台? |
expect_reference() | 两个名字是否指向同一个底层对象? |
expect_snapshot_output(), expect_snapshot_value(), expect_snapshot_error(), expect_snapshot_condition() | 快照测试。 |
expect_vector() | 该对象是否有矢量属性? |
expect_silent() | 代码是否无声? |
expect_type(), expect_s3_class(), expect_s4_class() | 该对象是否继承自S3或S4类,或者是一个基类型? |
expect_true(), expect_false() | 该对象是否为真/假? |
verify_output() | 验证输出 |
例子: 定义一个函数 “阶乘 “,接收一个数值n并返回其阶乘。
# create a recursive program that
# calculates the factorial of number n
factorial <- function(n)
{
if(n == 0)
{
return(1)
}
else
{
return(n * factorial(n - 2))
}
}
现在,让我们对我们的函数阶乘进行单元测试,测试它的准确性并调试我们的程序。
# import testthat package
library(testthat)
# use expect_that to create tests
expect_that
(
"Factorial of number $n",
{
expect_equal(factorial(5), 120)
expect_identical(factorial(2), 2)
expect_equal(factorial(8), 40320)
}
)
输出
Error: Test failed: 'Factorial computed correctly'
* factorial(5) not equal to 120.
1/1 mismatches
[1] 15 - 120 == -105
* factorial(8) not equal to 40320.
1/1 mismatches
[1] 384 - 40320 == -39936
测试给出了一个错误,这意味着我们的函数没有返回预期的结果。我们明知故犯地写错了代码。在阶乘函数中,我们使用的递归方法有一个错误。让我们根除这个错误,再测试一次我们的函数。
# create a recursive program that
# calculates the factorial of number n
factorial <- function(n)
{
if(n == 0)
{
return(1)
}
else
{
# notice we used (n-2) instead
# of (n-1) in our previous code
return(n * factorial(n - 1))
}
}
# import testthat package
library(testthat)
# use expect_that to create tests
expect_that
(
"Factorial of number $n",
{
expect_equal(factorial(5), 120)
expect_identical(factorial(2), 2)
expect_equal(factorial(8), 40320)
}
)
# no error
注意:如果你的源代码和软件包不在同一个目录下,你必须运行一行带有函数 source( )的 代码来运行测试。
source("your_file_path") # This is only needed if your project is not a package
组织你的文件和测试真的很重要。我们应该有一个名为R的文件夹,里面有所有的R代码文件,还有一个名为test/testthat的文件夹,里面有所有的测试脚本。在R控制台,你可以在一个文件中运行所有的测试,用
test_file("./path/to/file")
而所有的测试都放在一个文件夹里,其中有
test_dir("./path/to/folder")
上述两个函数都接受一个特殊的参数reporter,它有几个选项可以提供,分别是
- progress 它是默认值
- minimal 用于最小的报告
- location 显示测试运行的文件、行和列(失败或其他)。
- debug 允许你对一个失败的测试进行交互式调试,以及更多。
test_dir("./path/to/folder", reporter=c("minimal", "location"))
如果你想要一个没有错误的、形式良好的代码,单元测试是必要的。