Skip to main content

Reliable

Pre-learning materials

Making code reliable means ensuring it does what you think it does, and that it keeps doing so even as your data or dependencies change. In this session, you’ll learn about how you can

  1. Write tests
  2. Use enviroment variables & secrets
  3. Manage your environment
  4. Automate your checks
Note

Read through the sections below and try out the 👩‍💻 Your turn sections. Share your answers in the shared learning document for your group. We’ll use your comments as discussion points in the contact session.

Write tests

A test is a small piece of code that checks your function does what you expect. Tests give you confidence that your code works and catch mistakes when you change it later.

A simple test has three parts:

  • Call your function with a known input
  • State the output you expect
  • Check the two match

The {testthat} package is the standard tool for testing in R. If your code is in a package, usethis::use_test("percentage_change") creates tests/testthat/test-percentage_change.R with some boilerplate to get you started.

# tests/testthat/test-percentage_change.R
library(testthat)

percentage_change <- function(new, old) {
  (new - old) / old
}

test_that("percentage_change calculates an increase", {
  expect_equal(percentage_change(new = 120, old = 100), 0.2)
})

test_that("percentage_change calculates a decrease", {
  expect_equal(percentage_change(new = 80, old = 100), -0.2)
})

Run all your tests with devtools::test(), or a single file with testthat::test_file("tests/testthat/test-percentage_change.R").

Note

You don’t need a full R package to use {testthat}. Keep your functions in one script, your tests in another, and source() the functions at the top of the test file.

pytest is the most widely used testing package in Python. Tests are plain functions whose names start with test_, and you use assert to check the result.

# test_percentage_change.py
import pytest

def percentage_change(new, old):
    return (new - old) / old

def test_percentage_change_increase():
    assert percentage_change(new=120, old=100) == pytest.approx(0.2)

def test_percentage_change_decrease():
    assert percentage_change(new=80, old=100) == pytest.approx(-0.2)

Run all your tests by typing pytest in the terminal from your project folder.

Tip

Use pytest.approx() when comparing decimals, as floating point arithmetic can give tiny rounding differences. {testthat}’s expect_equal() already allows for this.

👩‍💻 Your turn

  • Think of a function in your work would you want to be sure is correct. What test could you write for it? What input would you give, and what output do you already know it should produce?
  • What edge cases might break it? Think about zeros, missing values or an empty file.

📖 Resources

Use environment variables and secrets

Ideally, we want to share and commit our code as much as possible. But some things should never end up in a repository:

  • Data, especially personal or sensitive data
  • Credentials, such as passwords and API keys

Instead of writing these into your script, store them in an environment variable: a named value that lives outside your code and is read in when the code runs. Your code can then be shared freely, and each user supplies their own values.

Environment variables aren’t only for secrets. They’re also useful for anything that differs between users or machines, such as the path to a shared folder.

Environment variables are usually stored in a special file (.Renviron or .env) as NAME="value" pairs.

API_KEY="ndfu9034890msd"

In R, environment variables are stored in a file called .Renviron. R reads this file when it starts, and you access the values with Sys.getenv():

api_key <- Sys.getenv("API_KEY")

usethis::edit_r_environ() opens your user-level .Renviron. You can also have one at the project level. If you do, add it to your .gitignore. The differences between levels are explained in Efficient R programming.

In Python, a common approach is a .env file in your project, read with the python-dotenv package.

import os
from dotenv import load_dotenv

load_dotenv()  # reads the .env file

api_key = os.getenv("API_KEY")
Tip

To prevent a file being committed to a repo, list it in a .gitignore file. GitHub has standard templates for R and Python projects. If you commit a secret by accident, deleting it later isn’t enough, because it stays in the Git history. Treat it as compromised: change or revoke it straight away.

👩‍💻 Your turn

  • What is in your project that shouldn’t be shared on GitHub? Think about data, credentials and anything personal.
  • What might you store as an environment variable? For example, the path to a SharePoint or network folder, which will be different for each person.

📖 Resources

Manage your environment

The code you write depends on more than the code itself: the version of R or Python, and the packages installed on your laptop. If you share your code with a colleague, will it run on their computer? What if a package updates and changes a key function? Will your code still run in two years?

A virtual environment solves this. It gives each project its own set of packages, and records the exact versions you used in a lockfile, so anyone can recreate the same setup.

Note

You may have heard of Docker, which also manages environments but for the entire system, not just your R or Python packages. We won’t cover it here, since a virtual environment is all most projects need.

{renv} is a dependency manager for R packages. The general workflow is:

  1. renv::init() sets up a new project-local environment
  2. Work as normal, installing and removing packages as you need them
  3. renv::snapshot() saves the package versions to the lockfile (renv.lock)
  4. renv::restore() recreates the setup recorded in the lockfile, e.g. when a colleague opens your project

Commit renv.lock to Git so others can restore your environment.

Note

{renv} records package versions and the version of R used, but it doesn’t install R for you. If you need to switch between R versions, take a look at rig.

uv manages Python versions, virtual environments and packages in one place. The general workflow is:

  1. uv init creates a new project
  2. uv add <package> installs a package and records it
  3. uv run <command> runs your code in the project’s environment
  4. uv sync recreates the environment from the lockfile (uv.lock), e.g. when a colleague opens your project

Commit pyproject.toml and uv.lock to Git, but not the .venv folder.

Tip

{renv} and uv are just two options. Others include rig and packrat in R, and venv, conda and poetry in Python. They all follow the same idea, so use whichever your team has agreed on.

👩‍💻 Your turn

  • What environment management does your team use, if any?
  • Which of your projects need a strictly controlled environment (for example, published statistics, or anything that has to be reproduced exactly)? Which don’t (for example, quick one-off exploration)?
  • If a colleague picked up your project in six months, what would they need to know to get your code running?

📖 Resources

Automate your checks

So far, you’ve thought about tests, secrets and environments. Running checks by hand every time you change your code is easy to forget. Continuous integration (CI) means those checks run automatically whenever your code changes. Continuous delivery (CD) means your outputs, such as a report or documentation, are also published automatically.

GitHub Actions is GitHub’s built-in tool for this. You describe what should happen in a small text file (a workflow) saved in your repository. For example: “every time someone pushes a change, set up the environment from the lockfile and run all the tests”. GitHub then does it on its own machine and shows you a green tick or a red cross.

Automation also runs your code on a clean machine. If your tests pass on your laptop but fail there, you’ve probably relied on something that only exists on your computer, such as an unrecorded package or a local file path.

Common things people automate include:

  • Running tests
  • Checking code style
  • Rendering a report or document
  • Publishing documentation or a website

👩‍💻 Your turn

  • What jobs do you do by hand that you’d like to be automated? Which checks do you sometimes forget?
  • Would the tests you thought about be able to run on a clean machine, or do they rely on files or settings only you have?
  • Who would you ask to find out whether GitHub Actions is available in your organisation?

📖 Resources

Before the session

Come prepared to discuss:

  • Which function in your project you’d most want to test, and what test you might write
  • What in your project shouldn’t be shared on GitHub, and what you might store as an environment variable
  • What environment management your team uses, and which projects need a strict environment
  • Which jobs you do by hand that you’d like automated