Test R in VSTS
I recently wrote about how to test Python on VSTS.
Now the time has come to R.
The fundamental difference between Python and R on VSTS is that Python is available on VSTS’s “hosted” computers whereas we need to bring our own environment for testing R.
The solution I present here use a Docker image to provide such an environment.
I build on the Docker images introduced in an earlier post.
The big lines are as follows:
- I use a VSTS Hosted machine to act as host for the container that run the tests.
- The results of the tests (two xml files) are copied from the container to the host.
- The two xml files are copied from the host to VSTS.
Making an R package
Just as in my post about testing Python I make a small package called VSTS
with two simple function add
and subtract
, that fully live up to their names.
My knowledge about R packages stem from Hadley Wickham’s book “R Packages”.
In particular, I use the suggested devtools, roxygen2 and testthat packages.
Furthermore, I use the covr package for computing code coverage.
Create the new package with e.g. RStudio through File
> New Project...
or with devtools::create
.
My package has the following files:
VSTS
├── DESCRIPTION
├── man
│ ├── add.Rd
│ └── subtract.Rd
├── NAMESPACE
├── R
│ └── arithmetic.R
├── tests
│ ├── testthat
│ │ └── test_arithmetic.R
│ └── testthat.R
└── VSTS.Rproj
The two announced functions are in arithmetic.R
:
#' Add two numbers
#'
#' @param x A number
#' @param y A number
#'
#' @return The sum x + y.
#'
#' @export
add <- function(x, y) {
x + y
}
#' Subtract two numbers
#'
#' @inheritParams add
#'
#' @return The difference x - y.
#'
#' @export
subtract <- function(x, y) {
x - y
}
The functions are tested in test_arithmetic.R
:
context("Arithmetic")
test_that("Add", {
expect_equal(add(1,1), 2)
})
test_that("Subtract", {
expect_equal(subtract(1,1), 0)
})
The generated NAMESPACE
and Rd
files are generated by devtools::document
.
In RStudio I usually go to Tools
> Project Options...
> Build Tools
and configure Generate documentation using Roxygen
to roxygenize when running Build & Reload
.
The latter will install the package and generate documentation when running Install and Restart
in the Build
tab.
Dockerfile for testing
Building on the r-devtools
Docker image from my post about Dockerfiles I create a Docker image called r-test
that contains the R packages needed for testing:
ARG R_VERSION
FROM r-devtools:${R_VERSION}
USER root
RUN apt-get update \
&& apt-get -y install \
libxml2-dev \
&& rm -rf /var/lib/apt/lists/*
USER shiny
RUN Rscript -e 'install.packages(c("covr", "roxygen2", "testthat"))'
COPY --chown=shiny:shiny run_tests.R /home/shiny/package/
WORKDIR /home/shiny/package
CMD ["Rscript", "run_tests.R"]
The directory /home/shiny/package
is going to have the source of the package to be tested.
The run_tests.R
script that executes the tests is introduced in the next section.
Note that the CMD
in r-test
will not execute successfully – it needs the source code for a package in /home/shiny/package
.
In the directory of the package to be tested I include the following Dockerfile:
FROM r-test
COPY --chown=shiny:shiny . /home/shiny/package
Since no CMD
is provided it is inherited from r-test
.
If the package has system requirements these are installed just like libxml2-dev
is in r-test
.
I have updated my repository on GitHub with the r-test
Dockerfile.
Testing a package
In the usual “interactive” use of the testthat and covr packages the test results are written directly in the REPL.
As mentioned in my post about testing Python on VSTS we must export the test results in the JUnit format and the code coverage in the Cobertura format for VSTS to utilize them.
The run_tests.R
script I use look like this:
devtools::install()
# Run tests
options("testthat.output_file" = "test-results.xml")
devtools::test(reporter = testthat::JunitReporter$new())
# Compute code coverage
cov <- covr::package_coverage()
covr::to_cobertura(cov, "coverage.xml")
Here we assume that run_tests.R
is located in the package source directory and we carry out three tasks: Install the package; run the tests and export the results to test-results.xml
; compute the code coverage and export the results to coverage.xml
.
The benefit of including run_tests.R
in the r-test
image is that I only have to update this script once to use it in all R packages.
Build in VSTS
My build definition in VSTS is as follows:
As an agent queue (not seen in the picture) I use a “Hosted Linux Preview”.
I use Azure Container Registry to host the images I use.
There is a nice tutorial on Microsoft’s docs on how to to this using Azure CLI.
The first two are built-in tasks: Pull an image from a container registry and build a Docker image based on the Dockerfile included in the repository.
The “Bash Script” executes the image just built and copies the resulting xml files from the container to the host.
docker run test:latest
CONTAINERID=`docker ps -alq`
docker container cp $CONTAINERID:/tmp/VSTS/test-results.xml .
docker container cp $CONTAINERID:/tmp/VSTS/coverage.xml .
In “Publish Test Results”:
- Test result format:
JUnit
- Test results files:
test-results.xml
- Search folder:
$(Build.SourcesDirectory)
In “Publish Code Coverage Results”:
- Code coverage tool:
Cobertura
- Summary file:
$(Build.SourcesDirectory)/coverage.xml
Alternatives
As mentioned in a previous post there is an official Microsoft guide to run tests in containers using Kubenetes.
I have also found an interesting alternative that use Azure Container Instances to provide the test environment.
Just as the author of that article I see the following pros and cons:
- Run a custom container on a hosted solution (as here):
- It is very flexible: In the container I am King. Uhm… root :-)
- It requires downloading the base image and building the custom image which can be quite time consuming.
- I have to include a Dockerfile in the package repository.
- Provide the host environment in a container:
- The environment is quickly up and running, but is quite restricted with respect to system requirements.
- I have to maintain the Azure Functions that start and stop the container instance.
- I have to maintain several small images with different environments or one huge image.