Christopher Gandrud
IQSS Tech Talk
22 March 2017
Talk is most applicable to R statistical software development
Part of a larger IQSS effort ("Social Science Software Toolkit")
We want your contributions especially for other languages.
Software involves many interconnected parts and user behaviours.
Difficult to anticipate how a change will affect software behaviour.
So your software will fail to meet expectations.
Spelling out your expectations so that they can be automatically tested, makes your expectations clear to collaborators.
-- Lets you all know when collaborators have broken something --
Tests help ensure that software actually follows its stated API. Avoids API Drift
By enforcing an API across software updates, tests enhance backwards compatibility
Proliferation of testing types, e.g. V-Model:
Software necessarily has limitations.
Let your users know when they have reached these limitations, why, and suggest what to do about it.
Let them know as soon as possible.
# Initialize Zelig5 least squares object
z5 <- zls$new()
# Estimate ls model
z5$zelig(Fertility ~ Education, data = swiss)
# Simulate quantities of interest
z5$sim()
# Plot quantities of interest
z5$graph()
Warning message:
In par(old.par) : calling par(new=TRUE) with no plot
Missing setx()
# Initialize Zelig5 least squares object
z5 <- zls$new()
# Estimate ls model
z5$zelig(Fertility ~ Education, data = swiss)
# Simulate quantities of interest
z5$sim()
Warning message:
No simulations drawn, likely due to insufficient inputs.
Error in models4[[model]]: invalid subscript type 'symbol'.
Estimation model type was not specified.
Select estimation model type with the "model" argument.
z.out <- zelig(y ~ x1 + x2, data = example)
No estimation model
type specified
The sum of the different software behaviours that are tested.
Maximise the testing surface.
But, there is a trade off between maximising the testing surface and reasonable test run time
The longer your tests take to run, the less likely you are to run them.
Obligatory xkcd comic
The proportion of source code that is run during testing.
Proxy for the testing surface.
Test coverage does not mean that your test is an accurate proxy.
# Create function to find mean
my_mean <- function(x) {
mean <- x
return(mean)
}
# Test
testthat::expect_error(my_mean(1:10), NA)
# Create function to find mean
my_mean <- function(x) {
mean <- x
return(mean)
}
# Test
testthat::expect_equal(my_mean(1:10), 5.5)
# Create function to find mean
my_mean <- function(x) {
mean <- x
return(mean)
}
# Test
testthat::expect_equal(my_mean(1:10), 5.5)
Error: my_mean(1:10) not equal to 5.5.
Lengths differ: 10 vs 1
Executable code can be included as documentation examples with Roxygen2.
It is executed as part of CRAN check.
#' Find the mean of a numeric vector
#'
#' @param x a numeric vector
#'
#' @examples
#' my_mean(1:10)
# Create function to find mean
my_mean <- function(x) {
mean <- x
return(mean)
}
Executable code can be included as documentation examples with Roxygen2.
It is executed as part of CRAN check.
#' Find the mean of a numeric vector
#'
#' @param x a numeric vector
#'
#' @examples
#' my_mean(1:10)
# Create function to find mean
my_mean <- function(x) {
mean <- x
return(mean)
}
But, the implicit expectation is that given the numeric vector 1:10, the function will not return an error.
The testthat package allows you to specify a broader range of expectations including:
expect_equal
expect_equivalent
expect_match
expect_true
expect_false
expect_error
expect_warning
and more.
library(testthat)
# Create function to find mean
my_mean <- function(x) {
mean <- x
return(mean)
}
test_that("Correct mean of a numeric vector is returned", {
expect_equal(my_mean(1:10), 5.5)
})
Error: my_mean(1:10) not equal to 5.5.
Lengths differ: 10 vs 1
# Create function to find mean
my_mean <- function(x) {
if (!is.numeric(x)) stop('x must be numeric.', call. = FALSE)
mean <- sum(x) / length(x)
return(mean)
}
# Test
test_that('my_mean failure test when supplied character string', {
expect_error(my_mean('A'), 'x must be numeric.')
}
devtools::use_testthat()
Creates:
In package directory:
Run tests locally with:
testthat::test_package()
# or
devtools::test()
# or
devtools::check(args = c('--as-cran'))
or in RStudio
Now every time you push changes to GitHub:
Once a package uses testthat you can find and explore code coverage with the covr package:
library(covr)
cov <- package_coverage()
shine(cov)
library(covr)
cov <- package_coverage()
shine(cov)
codecov.io
codecov.io
1. Add to .travis.yml:
r_github_packages:
- jimhester/covr
after_success:
- Rscript -e 'covr::codecov()'
2. Login to codecov with GitHub username and add package repo:
Always start at master
Create feature/hotfix branch
Create test
Create feature/fix
Run test locally
Did it pass?
If yes, merge into master
Push master to GitHub to initiate CI
Passes + accumulated changes
IQSSdevtools::check_best_practices()
Documentation:
readme: yes
news: yes
bugreports: yes
vignettes: yes
pkgdown_website: no
License:
gpl3_license: yes
Version_Control:
git: yes
github: yes
Testing:
uses_testthat: yes
uses_travis: yes
uses_appveyor: yes
build_check:
build_check_completed: yes
no_check_warnings: yes
no_check_errors: yes
no_check_notes: yes
test_coverage: 86
Background:
package_name: Zelig
package_version: 5.0-18
package_commit_sha: d5a8dcf0c9655ea187d2533fa977919be90612f6
iqssdevtools_version: 0.0.0.9000
check_time: 2017-03-17 16:16:55
IQSSdevtools::check_best_practices()
Documentation:
readme: yes
news: yes
bugreports: yes
vignettes: yes
pkgdown_website: no
License:
gpl3_license: yes
Version_Control:
git: yes
github: yes
Testing:
uses_testthat: yes
uses_travis: yes
uses_appveyor: yes
build_check:
build_check_completed: yes
no_check_warnings: yes
no_check_errors: yes
no_check_notes: yes
test_coverage: 86
Background:
package_name: Zelig
package_version: 5.0-18
package_commit_sha: d5a8dcf0c9655ea187d2533fa977919be90612f6
iqssdevtools_version: 0.0.0.9000
check_time: 2017-03-17 16:16:55