These exercises were written by Martin Morgan and Laurent Gatto for a Bioconductor Developer Day workshop.
Why unit testing?
- Writing code to test code;
- anticipate bugs, in particular for edge cases;
- anticipate disruptive updates;
- document and test observed bugs using specific tests.
Each section provides a function that supposedly works as expected, but quickly proves to misbehave. The exercise aims at first writing some dedicated testing functions that will identify the problems and then update the function so that it passes the specific tests. This practice is called unit testing and we use the RUnit package for this.
See the
Unit Testing How-To
guide for details on unit testing using the
RUnit
package. The
testthat
is
another package that provides unit testing infrastructure. Both
packages can conveniently be used to automate unit testing within
package testing.
This function should return the elements of x
that are in y
.
## Example
isIn <- function(x, y) {
sel <- match(x, y)
y[sel]
}
## Expected
x <- sample(LETTERS, 5)
isIn(x, LETTERS)
## [1] "I" "U" "G" "L" "P"
But
## Bug!
isIn(c(x, "a"), LETTERS)
## [1] "I" "U" "G" "L" "P" NA
Write a unit test that demonstrates the issue
## Unit test:
library("RUnit")
test_isIn <- function() {
x <- c("A", "B", "Z")
checkIdentical(x, isIn(x, LETTERS))
checkIdentical(x, isIn(c(x, "a"), LETTERS))
}
test_isIn()
## Error in checkIdentical(x, isIn(c(x, "a"), LETTERS)): FALSE
Update the buggy function until the unit test succeeds
## updated function
isIn <- function(x, y) {
sel <- x %in% y
x[sel]
}
test_isIn() ## the bug is fixed and monitored
## [1] TRUE
What are the exact matches of x
in y
?
isExactIn <- function(x, y)
y[grep(x, y)]
## Expected
isExactIn("a", letters)
## [1] "a"
## Bugs
isExactIn("a", c("abc", letters))
## [1] "abc" "a"
isExactIn(c("a", "z"), c("abc", letters))
## Warning in grep(x, y): argument 'pattern' has length > 1 and only the
## first element will be used
## [1] "abc" "a"
## Unit test:
library("RUnit")
test_isExactIn <- function() {
checkIdentical("a", isExactIn("a", letters))
checkIdentical("a", isExactIn("a", c("abc", letters)))
checkIdentical(c("a", "z"), isExactIn(c("a", "z"), c("abc", letters)))
}
test_isExactIn()
## Error in checkIdentical("a", isExactIn("a", c("abc", letters))): FALSE
## updated function:
isExactIn <- function(x, y)
x[x %in% y]
test_isExactIn()
## [1] TRUE
If x
is greater than y
, we want the difference of their
squares. Otherwise, we want the sum.
ifcond <- function(x, y) {
if (x > y) {
ans <- x*x - y*y
} else {
ans <- x*x + y*y
}
ans
}
## Expected
ifcond(3, 2)
## [1] 5
ifcond(2, 2)
## [1] 8
ifcond(1, 2)
## [1] 5
## Bug!
ifcond(3:1, c(2, 2, 2))
## Warning in if (x > y) {: the condition has length > 1 and only the first
## element will be used
## [1] 5 0 -3
## Unit test:
library("RUnit")
test_ifcond <- function() {
checkIdentical(5, ifcond(3, 2))
checkIdentical(8, ifcond(2, 2))
checkIdentical(5, ifcond(1, 2))
checkIdentical(c(5, 8, 5), ifcond(3:1, c(2, 2, 2)))
}
test_ifcond()
## Warning in if (x > y) {: the condition has length > 1 and only the first
## element will be used
## Error in checkIdentical(c(5, 8, 5), ifcond(3:1, c(2, 2, 2))): FALSE
## updated function:
ifcond <- function(x, y)
ifelse(x > y, x*x - y*y, x*x + y*y)
test_ifcond()
## [1] TRUE
Calculate the euclidean distance between a single point and a set of other points.
## Example
distances <- function(point, pointVec) {
x <- point[1]
y <- point[2]
xVec <- pointVec[,1]
yVec <- pointVec[,2]
sqrt((xVec - x)^2 + (yVec - y)^2)
}
## Expected
x <- rnorm(5)
y <- rnorm(5)
(m <- cbind(x, y))
## x y
## [1,] -0.9485964 -0.59736705
## [2,] 0.3720422 -0.24132514
## [3,] -0.7689038 0.04499722
## [4,] -0.3545548 0.13775908
## [5,] 0.1169861 0.85728364
(p <- m[1, ])
## x y
## -0.9485964 -0.5973671
distances(p, m)
## [1] 0.0000000 1.3677911 0.6670242 0.9451433 1.8031846
## Bug!
(dd <- data.frame(x, y))
## x y
## 1 -0.9485964 -0.59736705
## 2 0.3720422 -0.24132514
## 3 -0.7689038 0.04499722
## 4 -0.3545548 0.13775908
## 5 0.1169861 0.85728364
(q <- dd[1, ])
## x y
## 1 -0.9485964 -0.5973671
distances(q, dd)
## x
## 1 0
## Unit test:
library("RUnit")
test_distances <- function() {
x <- y <- c(0, 1, 2)
m <- cbind(x, y)
p <- m[1, ]
dd <- data.frame(x, y)
q <- dd[1, ]
expct <- c(0, sqrt(c(2, 8)))
checkIdentical(expct, distances(p, m))
checkIdentical(expct, distances(q, dd))
}
test_distances()
## Error in checkIdentical(expct, distances(q, dd)): FALSE
## updated function
distances <- function(point, pointVec) {
point <- as.numeric(point)
x <- point[1]
y <- point[2]
xVec <- pointVec[,1]
yVec <- pointVec[,2]
dist <- sqrt((xVec - x)^2 + (yVec - y)^2)
return(dist)
}
test_distances()
## [1] TRUE
Calculate the square root of the absolute value of a set of numbers.
sqrtabs <- function(x) {
v <- abs(x)
sapply(1:length(v), function(i) sqrt(v[i]))
}
## Expected
all(sqrtabs(c(-4, 0, 4)) == c(2, 0, 2))
## [1] TRUE
## Bug!
sqrtabs(numeric())
## [[1]]
## [1] NA
##
## [[2]]
## numeric(0)
## Unit test:
library(RUnit)
test_sqrtabs <- function() {
checkIdentical(c(2, 0, 2), sqrtabs(c(-4, 0, 4)))
checkIdentical(numeric(), sqrtabs(numeric()))
}
test_sqrtabs()
## Error in checkIdentical(numeric(), sqrtabs(numeric())): FALSE
## updated function:
sqrtabs <- function(x) {
v <- abs(x)
sapply(seq_along(v), function(i) sqrt(v[i]))
}
test_sqrtabs() # nope!
## Error in checkIdentical(numeric(), sqrtabs(numeric())): FALSE
sqrtabs <- function(x) {
v <- abs(x)
vapply(seq_along(v), function(i) sqrt(v[i]), 0)
}
test_sqrtabs() # yes!
## [1] TRUE
expect_that(object_or_expression, condition)
with conditions
- equals:
expect_that(1+2,equals(3))
orexpect_equal(1+2,3)
- gives warning:
expect_that(warning("a")
,gives_warning())
- is a:
expect_that(1, is_a("numeric"))
orexpect_is(1,"numeric")
- is true:
expect_that(2 == 2, is_true())
orexpect_true(2==2)
- matches:
expect_that("Testing is fun", matches("fun"))
orexpect_match("Testing is fun", "f.n")
- takes less:
than expect_that(Sys.sleep(1), takes_less_than(3))
and
test_that("description", {
a <- foo()
b <- bar()
expect_equal(a, b)
})
library("testthat")
test_dir("./unittests/")
test_file("./unittests/test_foo.R")
- Create a directory
./mypackage/tests
. - Create the
testthat.R
file
library("testthat")
library("mypackage")
test_check("sequences")
-
Create a sub-directory
./mypackage/tests/testthat
and include as many unit test files as desired that are named with thetest_
prefix and contain unit tests. -
Suggest the unit testing package in your
DESCRIPTION
file:
Suggests: testthat
From the ./sequences/tests/testthat/test_sequences.R
file:
We have a fasta file and the corresponding DnaSeq
object.
-
Let's make sure that the
DnaSeq
instance is valid, as changes in the class definition might have altered its validity. -
Let's verify that
readFasta
regenerates and identicalDnaSeq
object given the original fasta file.
test_that("dnaseq validity", {
data(dnaseq)
expect_true(validObject(dnaseq))
})
test_that("readFasta", {
## loading _valid_ dnaseq
data(dnaseq)
## reading fasta sequence
f <- dir(system.file("extdata",package="sequences"),pattern="fasta",full.names=TRUE)
xx <- readFasta(f[1])
expect_true(all.equal(xx, dnaseq))
})
Let's check that the R, C and C++ (via Rcpp
) give the same result
test_that("ccpp code", {
gccountr <-
function(x) tabulate(factor(strsplit(x, "")[[1]]))
x <- "AACGACTACAGCATACTAC"
expect_true(identical(gccount(x), gccountr(x)))
expect_true(identical(gccount2(x), gccountr(x)))
})