Ready for a 10-minute crash course on automated testing with GitHub Actions?! β‘οΈ
For an R package, you would typically:
devtools::test()
or devtools::check()
or testthat::test_check()
Harsh reality: you will either skip 2 because youβre too busy, or you will simply forget 2 (and 3) before pushing changes. π¬
For a Shiny app, you would typically:
Harsh reality: your manual testing will become so annoying that you will skip 1 and 2 before pushing changes. π¬
Write tests once, enjoy them forever! π§‘
testthat
usethis
to make your life easierOptionally:
shinytest2
in the case of a Shiny appcovr
for checking test coverageR/take_break.R
#' Should you take a break?
#'
#' This function takes in three inputs and returns a logical value
#' indicating whether you should take a break.
#'
#' @param weather A character string indicating the weather.
#' @param time A numeric value indicating the time of day.
#' @param workload A numeric value indicating the amount of work you have.
#'
#' @examples
#' take_break(weather = "sunny", time = 10, workload = 3)
#'
#'
#' @return A logical value indicating whether you should take a break.
#'
#' @export
take_break <- function(weather, time, workload) {
# check if weather is accepted value
weather <- match.arg(weather, c("sunny", "cloudy", "rainy"))
# time should be between 0 and 24
if (time < 0 | time > 24) {
stop("time should be between 0 and 24")
}
# workload should be numeric
if (!is.numeric(workload)) {
stop("workload should be numeric")
}
# determine whether or not to take a break
if (weather == "sunny" & time > 10 & workload > 5) {
return(TRUE)
} else {
return(FALSE)
}
}
inst/shiny/app.R
library(shiny)
library(RPharmaTest)
ui <- fluidPage(
titlePanel("Should You Take a Break?"),
selectInput("weather", "Weather:", choices = c("sunny", "cloudy", "rainy")),
numericInput("time", "Time of day (0-24):", value = 12, min = 0, max = 24),
numericInput("workload", "Workload (0-10):", value = 5),
textOutput("result")
)
server <- function(input, output) {
output$result <- renderText({
if (take_break(input$weather, input$time, input$workload)) {
"Yes, take a break!"
} else {
"No, keep working."
}
})
}
shinyApp(ui = ui, server = server)
The basic unit tests. You need:
usethis::use_testthat()
to create it all for youProject structure:
tests/testthat/test-take_break.R
test_that("take_break returns correct values", {
# Should return TRUE when it's sunny, after 10 AM, and workload > 5
expect_true(take_break(weather = "sunny", time = 11, workload = 6))
# Should return FALSE for other conditions
expect_false(take_break(weather = "sunny", time = 9, workload = 6))
expect_false(take_break(weather = "cloudy", time = 11, workload = 6))
expect_false(take_break(weather = "sunny", time = 11, workload = 3))
})
test_that("take_break throws errors for invalid inputs", {
expect_error(take_break("stormy", 11, 6))
expect_error(take_break("sunny", -1, 6), "time should be between 0 and 24")
expect_error(take_break("sunny", 11, "a lot"), "workload should be numeric")
})
Project structure:
tests/testthat/test-shiny.R
library(shinytest2)
test_that("Shiny app works as expected", {
# Don't run these tests on the CRAN build servers
skip_on_cran()
example_app <- system.file("shiny", package = "RPharmaTest")
app <- AppDriver$new(app_dir = example_app,
name = "take-break-app")
# Test when break should be taken
app$set_inputs(weather = "sunny", time = 11, workload = 6)
app$expect_text("#result")
# Test when break should not be taken
app$set_inputs(weather = "sunny", time = 9, workload = 6)
app$expect_text("#result")
})
With covr
, you can check the test coverage of your package. You can use the covr
package to generate a test coverage report, which you can then use to improve your tests.
covr::package_coverage()
generates a test coverage reportcovr::function_coverage()
calculates coverage for a specific functionSo we have to run:
devtools::check()
ORtestthat::test_check()
for R CMD checks (including running tests) ANDcovr::package_coverage()
Every time?!
With GitHub Actions, you can automate things β¨. You can create custom workflows that run every time you push changes to your repository. Handy!
To run Actions, you create a .github/workflows
directory in your repo. In here, you create .yaml
files that define the workflows.
You can have one big workflow file, or you can split it up into multiple workflow files.
Two workflow files:
Two workflows files:
usethis::use_github_action("check-standard", badge = TRUE)
usethis::use_github_action("test-coverage", badge = TRUE)
.github/workflows/R-CMD-CHECK.yaml
# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
name: R-CMD-check.yaml
permissions: read-all
jobs:
R-CMD-check:
runs-on: ${{ matrix.config.os }}
name: ${{ matrix.config.os }} (${{ matrix.config.r }})
strategy:
fail-fast: false
matrix:
config:
- {os: macos-latest, r: 'release'}
- {os: windows-latest, r: 'release'}
- {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'}
- {os: ubuntu-latest, r: 'release'}
- {os: ubuntu-latest, r: 'oldrel-1'}
env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
R_KEEP_PKG_SOURCE: yes
steps:
- uses: actions/checkout@v4
- uses: r-lib/actions/setup-pandoc@v2
- uses: r-lib/actions/setup-r@v2
with:
r-version: ${{ matrix.config.r }}
http-user-agent: ${{ matrix.config.http-user-agent }}
use-public-rspm: true
- uses: r-lib/actions/setup-r-dependencies@v2
with:
extra-packages: any::rcmdcheck
needs: check
- uses: r-lib/actions/check-r-package@v2
with:
upload-snapshots: true
build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")'
.github/workflows/test-coverage.yaml
# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
name: test-coverage.yaml
permissions: read-all
jobs:
test-coverage:
runs-on: ubuntu-latest
env:
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
- uses: r-lib/actions/setup-r@v2
with:
use-public-rspm: true
- uses: r-lib/actions/setup-r-dependencies@v2
with:
extra-packages: any::covr, any::xml2
needs: coverage
- name: Test coverage
run: |
cov <- covr::package_coverage(
quiet = FALSE,
clean = FALSE,
install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package")
)
covr::to_cobertura(cov)
shell: Rscript {0}
- uses: codecov/codecov-action@v4
with:
fail_ci_if_error: ${{ github.event_name != 'pull_request' && true || false }}
file: ./cobertura.xml
plugin: noop
disable_search: true
token: ${{ secrets.CODECOV_TOKEN }}
- name: Show testthat output
if: always()
run: |
## --------------------------------------------------------------------
find '${{ runner.temp }}/package' -name 'testthat.Rout*' -exec cat '{}' \; || true
shell: bash
- name: Upload test results
if: failure()
uses: actions/upload-artifact@v4
with:
name: coverage-test-failures
path: ${{ runner.temp }}/package
Every time you push changes to your repository, the workflows will run. You can see the results in the Actions tab in your repository.
And it can be a pass, or a failβ¦ π¦
Badges. Show them off in your README.
Bonus: remember the badge = TRUE
argument in the usethis::use_github_action()
function? This will automatically add the badge to your README!