Best Practices for Unit Testing and Linting with R

Written by Kristjana Popovski and Hannah Kennedy — Members of Microsoft’s Commercial Software Engineering (CSE) team

Collaborative coding was the backbone of our project — Photo by Alvaro Reyes on Unsplash

Unit Testing

STOP! … in the name of testing — Photo by Michael Mroczek on Unsplash
.
├── testthat
│ ├── test_labelling_utils.R
│ ├── test_cleaning_utils.R
│ ├── test_featurisation_utils.R
├── testthat.R
├── testthat_load_mock_data.R
└── testthat_source_input.R
# load the necessary libraries
library(testthat)
library(tidyverse)
# source all files, testing utilities
source("tests/testthat_source_input.R")
# load mock datasets
source("tests/testthat_load_mock_data.R")
for (file in files) { source(file) }# run all test scripts in the /tests/testthat/ directory
# mimic "stop_on_failure" here so that tests stop
# if there's a file with a failure.
for (testFile in testFiles) {
testResult <- test_file(testFile, reporter = "Summary")
testResult <- as.data.frame(testResult)
if (any(testResult$failed == 1)) {
stop(paste("Failure on", testFile, sep = " "))
}
}
This gathers the test files and source files.
Uniform styling makes for elegant code — Photo by Edgar Chaparro on Unsplash

Linting

library("lintr")# We feed the R script a file to lint
args <- commandArgs(trailingOnly = TRUE)
# The linters to be used
linterList <- list(useRelPaths = lintr::absolute_path_linter,
useArrowAssignment = lintr::assignment_linter,
closedCurly = lintr::closed_curly_linter,
spaceCommas = lintr::commas_linter,
noCommentedCode = lintr::commented_code_linter,
infixSpaces = lintr::infix_spaces_linter,
lineLength = lintr::line_length_linter(100),
spacesOnly = lintr::no_tab_linter,
...
)
# Method to run the linters against a file, prints any caught code
# Then returns the number of instances of caught code
runLinterOnFile <- function(file, lintList = linterList) {
result <- lintr::lint(file,
linters = lintList,
exclude_start = "# Exclude Start",
exclude_end = "# Exclude End")
print(result)
return(length(result))
}
# Apply the linting to provided file
lintingOutput <- runLinterOnFile(args[1])
git diff origin/master --staged --name-only --diff-filter=d | grep -i '.*\.R$'
THE_VARIABLE=4-( 10*3 )
The results of running `Rscript linter.R tests.R`

Mock Data

Time to play with random chance — Photo by Jonathan Petersson on Unsplash
An example of the mock heart rate data
An example of one type of mock ACT device data
For comparison, a pseudo-random signal… or literal white noise — courtesy of Omegatron [CC BY-SA 3.0] from wikimedia page

Continuous Integration

The stream of information — Photo by Emre Karataş on Unsplash
trigger:
branches:
include:
- master

variables:
- group: docker-repo-settings
jobs:
- job: UpdateContainerImage
pool:
vmImage: 'ubuntu-16.04'
displayName: Update the container if any packages or devops files changed

steps:
- script: |
# Check if devops files changed
devops_files=(install_packages.R package_list.txt build/Dockerfile build/azure-pipelines.yml)
changed=0
for file in $(git diff HEAD HEAD~ --name-only); do
if [[ " ${devops_files[@]} " =~ " ${file} " ]]; then
changed=$((changed+1))
echo "$file $changed"
fi
done
echo "##vso[task.setvariable variable=devops_files_changed]$((changed))"
displayName: Set devops_files_changed to the number of devops files changed

- script: |
docker build -t $(docker_repo_user)/$(docker_repo_name):$(Build.BuildNumber) -t $(docker_repo_user)/$(docker_repo_name) -f build/Dockerfile .
docker image ls
docker login -u $(docker_repo_user) -p $(docker_repo_pwd)
docker push $(docker_repo_user)/$(docker_repo_name)
displayName: Build and push the container of the R environment
condition: gt(variables['devops_files_changed'], 0)

Conclusion

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store