Chapter 1 R package with unit tests
1.1 Learning objectives
- Create an R package
- Create a function
- Document the function
- Include error checking in the function
- Write unit tests for the function
- Use your package in a script
- Share your package through GitHub
1.2 Setting up
You will need to install the following packages:
1.3 Create your R package
Use the following command to create the framework for a new package called demopckg
. Set the argment to the path where you want to save your package. The last section of the path should be the name of the package.
Package names can only be letters, numbers, and full stops.
You’ll see the following output, and a new RStudio project will open up. You can close the old window now and just work in this project.
✔ Setting active project to '~/rstuff/demopckg'
✔ Creating 'R/'
✔ Creating 'man/'
✔ Writing 'DESCRIPTION'
✔ Writing 'NAMESPACE'
✔ Writing 'demopckg.Rproj'
✔ Adding '.Rproj.user' to '.gitignore'
✔ Adding '^demopckg\\.Rproj$', '^\\.Rproj\\.user$' to '.Rbuildignore'
✔ Opening new project 'demopckg' in RStudio
1.3.1 Edit the DESCRIPTION file
Open the DESCRIPTION
file. It should look like this:
Package: demopckg
Title: What the Package Does (One Line, Title Case)
Version: 0.0.0.9000
Authors@R:
person(given = "First",
family = "Last",
role = c("aut", "cre"),
email = "first.last@example.com")
Description: What the package does (one paragraph).
License: What license it uses
Encoding: UTF-8
LazyData: true
Change the title, authors, and description to your own information. If your package has multiple authors, add them as a vector like this. You can also add your ORCiD.
Authors@R: c(
person(given = "Lisa",
family = "DeBruine",
role = c("aut", "cre"),
email = "debruine@gmail.com",
comment = c(ORCID = "0000-0002-7523-5539")),
person(given = "Firstname",
family = "Lastname",
role = c("aut"),
email = "person@gmail.com",
comment = c(ORCID = "0000-0000-0000-000X")))
1.3.2 Create a LICENSE
Add a license using one of the following options:
1.3.3 Create a README
Use the following code to set up a README document that will explain your package.
We’ll eventually put this on github, so change the installation instructions to the following (change yourusername
to your github username).
You can install the released version of demopckg from [GitHub](https://github.com) with:
``` r
devtools::install_github("yourusername/demopckg")
```
Delete the example for now.
Make sure you knit your README.Rmd file when you update it and never edit the README.md file (that’s just for github).
1.4 Creating a function
Function definitions are saved in the R
folder. You don’t have to, but I like to save each function in its own file and name the file after the function.
1.4.1 Template function
Create a new R script from the File menu (New File > R Script
).
Paste the following template into your file:
1.4.2 Edit the function
We’re going to create a function that reports a p-value in APA style, named report_p
. It will take two arguments, the p-value (p
) and the number of digits to round to (digits
).
Replace myfunction
with report_p
and change the arguments. Should p
have a default value? Should digits
?
The first thing we should do in the function is check whether p
is less than 0.001, and if it is, return the value “p < .001”.
Once you call the return()
function, your function stops running.
If p
is greater than 0.001, then we should round it to the specified number of digits
, paste it after the string “p =”, and return it. So add an argument called digits
and set the default value to 3.
report_p <- function(p, digits = 3) {
if (p < .001) return("p < .001")
p_round <- round(p, digits)
p_string <- paste("p =", p_round)
return(p_string)
}
Run your function and test it with a few different p-values and digits. Try report_p(0.01034)
. Does this look exactly like you expect?
APA style omits the leading zero and pads the number out to three digits. We can do this by converting our rounded p-value into a character string, replacing the string “0.” with “.”, and making sure to pad the right side with enough zeros. The stringr
package has useful functions for this.
When you use R functions from a package (not base R), you normally load the package using the library()
function. When you’re developing your own package, you should preface every function with its package name and two colons instead, so in the code below we’ll use stringr::str_replace()
and stringr::str_pad()
, not str_replace()
and str_pad()
.
One function you can’t preface with the package name is the pipe. While you’re testing your function, load the pipe by typing library(magrittr)
in the console.
report_p <- function(p, digits = 3) {
if (p < .001) return("p < .001")
p_round <- round(p, digits) %>%
as.character() %>%
# omit leading zero for APA-style
stringr::str_replace("0.", ".") %>%
# pad right with zeros
stringr::str_pad(digits+1, "right", 0)
p_string <- paste("p =", p_round)
return(p_string)
}
1.4.3 Documentation
Put your cursor somewhere on the first line of your function and choose Insert Roxygen Skeleton
from the Code
menu.
It will insert the following documentation before your function.
The #'
is special to roxygen2
documentation, which we’ll enable below. This generates what you see in the Help viewer. Type ?mean
into the console pane and have a look at the Help pane.
- The first line is the name of the function in title case
- The Description is the lines between the title and first
@param
- The Useage is automatically generated
- The Arguments section is generated from the list of
@param
argument Argument description… - The Value section is the text after
@return
- The Examples section is the text under
@examples
- This block should include
@export
to make sure your function is available for users of your package (replace@export
with@keywords internal
The documentation for your report_p
function should look something like below:
#' Report p-value
#'
#' Reports a p-value in APA style.
#'
#' @param p The p-value
#' @param digits The number of digits to round to (default = 3)
#'
#' @return A string with the format "p = .040" or "p < .001"
#' @examples
#'
#' report_p(0.02018) # returns "p = .020"
#' report_p(0.00028) # returns "p < .001"
#'
#' @export
Save your file in the R
directory with the name report_p.R
. For now, we’ll make a separate file for each function and give it the name of the function.
Roxygen creates automatic documentation. You enable it with the following command (you only need to run this once per package).
Now you can automatically update the documentation for your package by running devtools::document()
, after which you should see the following text.
Updating demopckg documentation
Writing NAMESPACE
Loading demopckg
Writing report_p.Rd
You don’t need to worry about these files, they’ll be added to your package to show Help documentation.
1.4.4 Imports
You need to “import” any packages you used in your function by running usethis::use_package
for each package you want to include.
You can’t import the whole tidyverse
, but you can import each package separately (i.e., ggplot2, purrr, tibble, dplyr, tidyr, stringr, readr, forcats). Import just the packages you actually need.
If you use pipes (even if you’ve imported dplyr
), you also need to run usethis::use_pipe()
. It will add a file called utils-pipe.R
to your R
directory and add magrittr
to your Imports.
1.5 Build your package
Now you’re ready to check and build your package for installation.
1.5.1 Check
First, check everything by running devtools::check()
. You’ll get a lot of output, but don’t worry unless you have an error message. Hopefully, you’ll get this message:
0 errors ✔ | 0 warnings ✔ | 0 notes ✔
1.5.2 Build
Once you’ve fixed any errors, you can build your package.
You’ll get a message that looks like this:
✔ checking for file ‘/Users/lisad/rproj/demopckg/DESCRIPTION’ ...
─ preparing ‘demopckg’:
✔ checking DESCRIPTION meta-information ...
─ checking for LF line-endings in source and make files and shell scripts
─ checking for empty or unneeded directories
─ building ‘demopckg_0.0.0.9000.tar.gz’
[1] "/Users/lisad/rproj/demopckg_0.0.0.9000.tar.gz"
1.5.3 Install
Next, install your new package with the following code (../
means to go up one directory to look for the demopckg
install file).
You’ll see a bunch of output that should end in:
* DONE (demopckg)
1.5.4 Test
To make sure it’s all gone well, restart R and try to use the function report_p(0.00039)
. You should get an error message.
Then load your new package with library(demopckg)
are retry the example above.
Type ?report_p
in the console and look at your Help documentation.
Restart R and open a new .R or .Rmd file. Load your new package at the top of the file and try using your function in a paragraph that reports the p-value for a test.
1.6 Error checking
Try running your function with different values for p
. What happens if you use invalid values, like 1.07, -0.05, or “A”?
We can add error checking to a function to quit and give a message if the error is fatal, or just warn them if the error is recoverable, but probably wrong.
P-values can’t ever be less than 0 or greater than 1, so we can just quit and give an error message if that happens. Add the following code to the beginning of your function, rerun the code to update the function, and test it on some values of p that are less than 0 or greater than 1.
What other errors do you think people might make? You can add checks to the beginning of your function to warn people if they don’t enter reasonable numbers for the digits argument and set digits to the default value so that the code can continue.
1.7 Unit tests
Up until now, we’ve just tested our function in an ad hoc way every time we made changes. We can make this process more formal by using unit tests. These will make sure that your function is working properly if you make any changes. This seems like overkill for simple functions, but is absolutely essential for big projects, so best to get into good habits now.
1.7.1 Setup
When you set up a new package, you need to set up the testing structure using usethis::use_testthat()
. You only need to do this once for each package and you will know it has been done if a new directory called tests
is made.
1.7.2 New unit tests
Create a new test file for the report_p
function using usethis::use_test("report_p")
. It will create a new file called test-report_p.R
in the tests/testhat/
directory.
Replace the text in that file with the text below.
context("report_p")
testthat::test_that("errors", {
testthat::expect_error(
report_p(-1),
"p cannot be less than 0"
)
testthat::expect_error(
report_p(2),
"p cannot be greater than 1"
)
})
The context
function lets you know what function you’re testing when you run all the unit tests in a package. The test_that
function checks a groups of expectations. The first set checks if we get the error messages we expect, so we’ve called it “errors”.
We’re going to check two expectations, that we’ll get the error message “p cannot be less than 0” if p = -1, and that we’ll get the error message “p cannot be greater than 1” id p = 2. You can test more values than these, but we’ll start with just these two.
After you save this file, run devtools::test()
. You should see output like:
Loading demopckg
Testing demopckg
✔ | OK F W S | Context
✔ | 2 | report_p
══ Results ═══════════════════════════════════════════════════════════
OK: 2
Failed: 0
Warnings: 0
Skipped: 0
Now add another testthat::test_that
block called “default values”. Use the function testthat::expect_equal
to check if the output of the report_p()
function with different p
values and the default digits
value gives you the expected output. For example:
1.7.3 Run all tests
Run devtools::test()
after you add each test to make sure your tests work as expected.
1.8 Documentation
Now it’s time to edit your README to explain how to use your package.
1.8.1 Vignettes
A vignette can also help users understand how to use your package. Create a new vignette with the function below.
Change the YAML header to your “Vignette Title” and “Vignette Author” and edit the text below using R Markdown.
1.8.2 pkgdown
The package pkgdown creates a website for your package. Run the following function to set it up.
This will create a directory called docs
and a file called _pkgdown.yml
.
Whenever you edit the documentation for new function, edit the README, or edit a vignette, you need to re-build the site.
There will be a lot of output, then your site should automatically open up in your default web browser.
1.10 Further resources
There is a lot more to learn about package development, including writing vignettes to help users understand your functions and getting your package ready to submit to CRAN.
- R Packages by Hadley Wickham
- usethis
- Workflow for package development by Emil Hvitfeldt
1.10.1 Workflow
The following script has all of the functions you’ll need to start a new package.
pckg <- "mynewpackage"
pckgdir <- "~/rproj/"
me <- "Lisa DeBruine"
# run once at start of package
usethis::create_package(paste0(pckgdir, pckg))
usethis::use_mit_license(name = me)
usethis::use_readme_rmd()
usethis::use_testthat()
usethis::use_roxygen_md()
usethis::use_pipe() # everyone needs pipes
# code for new functions
funcname <- "newfunction"
imports <- c("dplyr", "tidyr")
usethis::edit_file(paste0("R/", funcname, ".R"))
for (import in imports) usethis::use_package(import)
# testing a function
usethis::use_test(funcname)
devtools::test(filter = funcname)
# documentation
usethis::use_vignette("example")
usethis::use_pkgdown()
pkgdown::build_site()
# building the package
devtools::check() # can take a long time
devtools::build()
devtools::install(paste0("../", pckg))
# use these for specific tasks
# if check() or build_site() take too long
devtools::document() # updates from roxygen function documentation
devtools::test() # runs all your unit tests, or use filter
devtools::run_examples() # checks all your function examples work
pkgdown::build_home() # from README and DESCRIPTION
pkgdown::build_article() # from vignettes
pkgdown::build_reference() # from roxygen function documentation
1.10.2 The full report_p
function
Here’s what the full function should look like.
#' Report p-value
#'
#' Reports a p-value in APA style.
#'
#' @param p The p-value
#' @param digits The number of digits to round to (default = 3)
#'
#' @return A string with the format "p = .040" or "p < .001"
#' @examples
#'
#' report_p(0.02018) # returns "p = .020"
#' report_p(0.00028) # returns "p < .001"
#'
#' @export
report_p <- function(p, digits = 3) {
if (p < 0) stop("p cannot be less than 0")
if (p > 1) stop("p cannot be greater than 1")
if (digits < 1) {
warning("digits should probably be an integer between 1 and 5")
digits = 3
}
if (p < .001) return("p < .001")
p_round <- round(p, digits) %>%
as.character() %>%
# omit leading zero for APA-style
stringr::str_replace("0.", ".") %>%
# pad right with zeros
stringr::str_pad(digits+1, "right", 0)
p_string <- paste("p =", p_round)
return(p_string)
}
1.11 Glossary
term | definition |
---|---|
argument | A variable that provides input to a function. |
character | A data type representing strings of text. |
function | A named section of code that can be reused. |
git | One type of version control software. |
github | A cloud-based service for storing and sharing your version controlled files. |
package | A group of R functions. |
project | A way to organise related files in RStudio |
version control | A way to save a record of changes to your files. |