Chapter 1 Your first 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:

install.packages(c("devtools", "roxygen2", "testthat", "usethis", "knitr"))

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.

usethis::create_package("~/rstuff/demopckg")

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.

1.3.2 Create a LICENSE

Add a license using one of the following options:

usethis::use_mit_license(name = "YOUR NAME")  # permissive sharing
usethis::use_cc0_license(name = "YOUR NAME")  # public domain - use for data packages
usethis::use_gpl3_license(name = "YOUR NAME") # derivatives must be open

1.3.3 Create a README

Use the following code to set up a README document that will explain your package.

usethis::use_readme_rmd() 

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:

#' My function
#'
#' `myfunction` does something.
#'
#' @param arg1 A value to return
#' @return Returns the value of \code{arg1}
#' @examples
#'
#' myfunction(1) # returns 1
#' 
#' @export

myfunction <- function(arg1 = "Change me") { 
  arg1 
}

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".

report_p <- function(p, digits = 3) {
  if (p < .001) return("p < .001")
}

Once you run 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.

report_p <- function(p, digits = 3) {
  if (p < .001) return("p < .001")
  
  round_p <- 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 need to 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 Imports

You need to "import" any packages you used in your function by running usethis::use_package for each package you want to include.

usethis::use_package("stringr")

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.4.4 Documentation

Now edit the commented part 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 @paramargument Argument description...
  • The Value section is the text after @return
  • The Examples section is the text under @examples
  • This block should end with @export to make sure your function is added to your package

Edit the documentation for your report_p function.

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).

usethis::use_roxygen_md()

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.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

Next, run devtools::build(). 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).

devtools::install("../demopckg")

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.

  if (p < 0) stop("p cannot be less than 0")
  if (p > 1) stop("p cannot be 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.

  if (!(digits %in% 1:5)) {
    warning("digits should probably be an integer between 1 and 5")
    digits = 3
  }

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 expectataions. The first set we're going to make 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:

  testthat::expect_equal(
    report_p(p = 0.0222),
    "p = .022"
  )

1.7.3 Run all tests

Run devtools::test() after you add each test to make sure your tests work as expected.

1.8 Share your package

You can do package development without a GitHub account, but this is one of the easiest ways to share your package.

1.8.1 Git on RStudio

If you don't have git installed on your computer, don't have it integrated with RStudio, and/or don't have a github account, follow the instructions in Appendix @ref(#git).

1.8.2 Set up git for this project

If you aren't already using version control for this project, make sure all of your files are saved and type usethis::use_git() into the console. Choose Yes to commit and Yes to restart R.

1.8.3 GitHub access token

Now set up a github access token with usethis::browse_github_pat(). Your web browser will open and you'll be asked to log into your github account and then asked to authorise a new token. Accept the defaults and click OK at the bottom of the page.

Authorise a github token so you can create new github repositories from RStudio

Figure 1.1: Authorise a github token so you can create new github repositories from RStudio

Copy your token (the blacked-out bit in the image below).

Copy your github token

Figure 1.2: Copy your github token

Type usethis::edit_r_environ() in the RStudio console pane. A new file called .Renviron will appear in the source pane. Add the following line to it (replace <YOUR-TOKEN> with your copied token).

GITHUB_PAT=<YOUR-TOKEN>

Save and close the file, then restart R.

1.8.4 Make a new GitHub repository

Type usethis::use_github(protocol="https") into the console window and check that the suggested title and description are OK.

✔ Setting active project to '/Users/lisad/rproj/demopckg'
● Check title and description
  Name:        demopckg
  Description: Demo Stuff
Are title and description ok?
1: No way
2: Yeah
3: No

If you choose Yeah, you'll see some messages and your web browser will open the github repsitory page.

Your new github repository

Figure 1.3: Your new github repository

1.8.5 Install your package from GitHub

Install your package using the following code (replacing debruine with your github username).

devtools::install_github("debruine/demopckg")

1.9 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.

1.9.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"))
usethis::use_test(funcname)
for (import in imports) usethis::use_package(import)

# building the package
devtools::check() # can take a long time
devtools::build()
devtools::install(paste0("../", pckg))

# use these for specific tasks 
# if check takes too long
devtools::document()
devtools::test()
devtools::run_examples()

1.9.2 The full report_p function

Here's what the full function should look like.

#' Report p-value
#'
#' `report_p` reports a p-value.
#'
#' @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)
}