Background

We saw last time how to create a basic package from a simple function, document its contents, and explore its functionality. In this exercise, we’ll learn how to create formal tests for our functions, write vignettes, and publish our package to the web.


Testing

We spent just a bit of time testing out our cats() function, but we can formalize this process with so-called unit tests. This basically means we’ll establish some expectations about what our function should return when passed certain arguments. As Wickham & Bryan point out, there are four advantages to working with unit tests:

  1. Fewer bugs and errors;

  2. More rigorous code structure;

  3. Easier start/stop points;

  4. Code that’s robust to changes.

Fortunately, the {devtools} package helps us with this process via some built-in functions. To begin, we’ll use use_testthat() to set up our framework for testing, which will

  • add Suggests: testthat to DESCRIPTION;

  • create the directory tests/testthat/; and

  • add the script tests/testthat.R.

Task: Load the {devtools} package.

> library(devtools)
Loading required package: usethis

Task: Set the testing framework with use_testthat().

## set testing framework
> use_testthat()
✔ Setting active project to '/Users/mark/Documents/GitHub/FISH549/pets'
✔ Adding 'testthat' to Suggests field in DESCRIPTION
✔ Setting Config/testthat/edition field in DESCRIPTION to '3'
✔ Creating 'tests/testthat/'
✔ Writing 'tests/testthat.R'
• Call `use_test()` to initialize a basic test file and open it for editing

Write a unit test

Now that we have our testing framework, we need to write some actual test scripts. Here again, we see a suggestion like last time to use use_test() to initialize a basic test file.

Task: Use the use_test() function to create an empty script for testing our cats() function.

> use_test("cats")
✓ Writing 'tests/testthat/test-cats.R'
● Modify 'tests/testthat/test-cats.R'

You should now see a script called test-cats.R with the following example unit test in it.

test_that("multiplication works", {
  expect_equal(2 * 2, 4)
})

Unit tests are hierarchical in nature. So-called expectations are grouped into tests, which are then organized into files. Expectations are informal and intended solely for human eyes—they will never be read or checked by an R function. The test itself is embedded within test_that(), and is based upon the expected result of an operation that we check with an expect_ function. In the example test shown above, the expectation is that "multiplication works". The specific test of the expectation is whether or not 2 * 2 == 4, which is coded as expect_equal(2 * 2, 4).

Tip: You can find a whole battery of {testthat} functions here.

Let’s go ahead and write some unit tests for cats().

Task: Delete the default unit test in test-cats.R and replace it with the following code.

test_that("logical operators work", {
  ## if TRUE
  expect_equal(cats(TRUE), "I love cats!")
  ## if alias for TRUE
  expect_equal(cats(1), "I love cats!")
  ## if FALSE
  expect_equal(cats(FALSE), "I am not a cat person.")
  ## if alias for FALSE
  expect_equal(cats(0), "I am not a cat person.")
})

Task: Save your file when you’re done entering the tests.

Run unit tests

Now we’re set to run these tests with the test() function.

Task: Call test() and note the output, which may differ somewhat from below.

> test()
ℹ Testing pets

Attaching package: ‘testthat’

The following object is masked from ‘package:devtools’:

    test_file

✔ | F W S  OK | Context
⠏ |         0 | cats                                                      
[1] "I love cats!"
[1] "I love cats!"
[1] "I am not a cat person."
[1] "I am not a cat person."
✔ |         4 | cats

══ Results ════════════════════════════════════════════════════════════════════════
[ FAIL 0 | WARN 0 | SKIP 0 | PASS 4 ]

What does this all mean?

The report indicates how many total tests were run (4) and the output of each test. The Results section at the bottom shows how many of the tests failed (FAIL), issued a warning (WARN), were skipped (SKIP), and passed (PASS).

Success: All four of our tests passed!

Tip: Now is a good time to commit your changes and push them to GitHub.


Packages on GitHub

The pets repo we created on GitHub contains our package skeleton in human-readable form. One of the neat things about this is that you (or anyone else) can now install the package directly from GitHub.

Tip: You can use either of the following function calls to install a package diectly from GitHub. To do so, replace GITHUB_USERNAME with the username of the repo’s owner and PACKAGE with the package’s name.

## original call using {devtools} still works
devtools::install_github("GITHUB_USERNAME/PACKAGE")

## but {remotes} is the newer package with the function
remotes::install_github("GITHUB_USERNAME/PACKAGE")

Tip: If you try either of these commands and get the following error, run Sys.unsetenv("GITHUB_PAT") and then try again.

Using github PAT from envvar GITHUB_PAT
Error: Failed to install 'pets' from GitHub:
  HTTP error 401.
  Bad credentials

Editing the README.md

Because your package now lives in a public space on GitHub where others can find and use it, you should include some helpful information in the README.md file to describe the package contents and how it works.

Task: Use the use_readme_rmd() function to create a Markdown file with a skeleton to add information about your package.

> use_readme_rmd()
✓ Writing 'README.Rmd'
✓ Adding '^README\\.Rmd$' to '.Rbuildignore'
● Modify 'README.Rmd'
✓ Writing '.git/hooks/pre-commit'

The output above indicates that the function

  • created README.Rmd

  • added some lines to .Rbuildignore

  • created a Git pre-commit hook to keep README.Rmd and README.md synced with one another

Task: Open up README.Rmd and inspect its contents.

At the top of README.Rmd you’ll see a very simply YAML section with only one entry for the output format.

---
output: github_document
---

Below that you will see a number of sections with prompts asking for more information about the package, including a description of the package, how to install it, and example of its usage.

Tip: Comments in Markdown are denoted by <!-- some comment here -->.

Task: In the first section # pets, go ahead and enter some descriptive text about the package contents (you can delete the comments about badges).

# pets

The goal of pets is to provide a simple means for people to express their feelings about pets. At present, the package only contains one function: `cats()`.

Task: In the next section on ## Installation, add some install instructions like those shown above.

## Installation

You can install the development version of pets like so:

``` r
if(!require("devtools")) {
  install.packages("devtools")
}
devtools::install_github("FISH549/pets")
```

Task: In the ## Example section, fill in some information with example calls to cats().

## Example

Here are two simple examples that allow you to express your feelings about cats.

```{r example}
library(pets)

## if you like cats
cats(TRUE)

## if you don't like cats
cats(FALSE)
```

Task: When you are finished editing README.Rmd, knit it with the Knit button in RStudio or use the build_readme() function at the command line.

> build_readme()
Installing pets in temporary library
Building /Users/mark/Documents/GitHub/pets/README.Rmd

Task: Now commit both README.Rmd and README.md and then push them to GitHub. When you are finished, check out your pets repo on GitHub to see the new changes to your README.md.


Vignettes

Vignettes offer a longer description of a package’s utility. They aren’t a necessary component of a package, but rather one of the “bells and whistles”. Vignettes are written in Markdown and can be accessed via vignette(name) where name is the name of the vignette.

Tip: You can see a list of all of the vignettes within a particular package with browseVignettes("packagename").

Task: To create a vignette, use the use_vignette() function by passing it the name of the vignette file to create. Here use "example-usage".

> use_vignette("example-usage")
✔ Adding 'knitr' to Suggests field in DESCRIPTION
✔ Setting VignetteBuilder field in DESCRIPTION to 'knitr'
✔ Adding 'inst/doc' to '.gitignore'
✔ Creating 'vignettes/'
✔ Adding '*.html', '*.R' to 'vignettes/.gitignore'
✔ Adding 'rmarkdown' to Suggests field in DESCRIPTION
✔ Writing 'vignettes/example-usage.Rmd'
• Modify 'vignettes/example-usage.Rmd'

The call to use_vignette() did the following

  • added the necessary dependencies to DESCRIPTION

  • added some folders and files to .gitignore

  • created a vignettes/ directory

  • created the skeleton file vignettes/example-usage.Rmd

Task: Inspect the contents of example-usage.Rmd. At the top you’ll see the following YAML section.

---
title: "example-usage"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{example-usage}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

Task: Change the title to be a little more informative.

title: "Using the pets package"

Task: Change %\VignetteIndexEntry{example-usage} to match our new title.

%\VignetteIndexEntry{Using the pets package}

Task: Scroll down below the YAML and inspect the first code chunk where two options are being set.

knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)

Tip: Setting collapse = TRUE will if possible, collapse all the source code and output blocks from one code chunk into a single block (by default, they are written to separate blocks).

Tip: The comment = "#> determines the prefix for output from function calls. You can change this to be any character string or leave it empty.

Task: Add some more details to the body of the vignette.

# Background

The `{pets}` package was designed to allow people to express their feelings about pets. At present, the package is rather cat-centric, in that it only contains one function: `cats()`.

# Usage

To use `{pets}`, first load the package and then call the function `cats()` with a logical/boolean argument. For example, 

```{r setup}
## load pets package
library(pets)

## if you love cats
cats(TRUE)

## if you're not a big fan of cats
cats(FALSE)
```

Task: When you are finished, go ahead and click on the Knit button in RStudio to preview the html version of the vignette.

At this point, we’ve created our vignette, but we still need to build it into the package itself.

Task: Use the build_vignettes() function to build the vignette.

> build_vignettes()
ℹ Installing pets in temporary library
ℹ Building vignettes for pets
--- re-building ‘example-usage.Rmd’ using rmarkdown


processing file: example-usage.Rmd
                                                                                                            
output file: example-usage.knit.md

/Applications/RStudio.app/Contents/MacOS/quarto/bin/tools/pandoc +RTS -K512m -RTS example-usage.knit.md --to html4 --from markdown+autolink_bare_uris+tex_math_single_backslash --output /Users/mark/Documents/GitHub/FISH549/pets/vignettes/example-usage.html --lua-filter /Users/mark/R_libs/rmarkdown/rmarkdown/lua/pagebreak.lua --lua-filter /Users/mark/R_libs/rmarkdown/rmarkdown/lua/latex-div.lua --embed-resources --standalone --section-divs --template /Users/mark/R_libs/rmarkdown/rmd/h/default.html --highlight-style pygments --css /Users/mark/R_libs/rmarkdown/rmarkdown/templates/html_vignette/resources/vignette.css --mathjax --variable 'mathjax-url=https://mathjax.rstudio.com/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML' --include-in-header /var/folders/w6/bgxxqxln6nxf9z0wr7fypyfw0000gn/T//Rtmppjt1Se/rmarkdown-stra66d76e727.html 

Output created: example-usage.html
--- finished re-building ‘example-usage.Rmd’

ℹ Copying vignettes
ℹ Moving example-usage.html and example-usage.R to doc/
ℹ Copying example-usage.Rmd to doc/
ℹ Building vignette index

Task: We should now rebuild our package.

> build()
✔  checking for file ‘/Users/mark/Documents/GitHub/FISH549/pets/DESCRIPTION’ ...
─  preparing ‘pets’:
✔  checking DESCRIPTION meta-information
─  installing the package to build vignettes
✔  creating vignettes (1.8s)
─  checking for LF line-endings in source and make files and shell scripts
─  checking for empty or unneeded directories
─  building ‘pets_0.0.0.9000.tar.gz’
   
[1] "/Users/mark/Documents/GitHub/FISH549/pets_0.0.0.9000.tar.gz"

Task: Re-run our package checks (most of the output below has been surpressed).

> check()
── R CMD check results ────────────────────────────── pets 0.0.0.9000 ────
Duration: 8.7s

0 errors ✓ | 0 warnings ✓ | 0 notes ✓

Success: It looks like everything is in working order!

Task: Go ahead and commit your changes and push them to GitHub.


Website

If you’ve spent some time searching the web for information on packages, there’s a good chance you’ve come across a specific form of package website. For example, here is the wesbite for the {usethis} package that we’ve used to build our {pets} package. These websites are created with the {pkgdown} package, which we can use to create a website for {pets}.

Task: Begin by loading the {pkgdown} package.

> library(pkgdown)

Attaching package: ‘pkgdown’

The following object is masked from ‘package:devtools’:

    build_site

Task: Configure your package to use {pkgdown}.

> use_pkgdown()
✔ Adding '^_pkgdown\\.yml$', '^docs$', '^pkgdown$' to '.Rbuildignore'
✔ Adding 'docs' to '.gitignore'
✔ Writing '_pkgdown.yml'
• Modify '_pkgdown.yml'

use_pkgdown() will add a new file called _pkgdown.yml, which contains some YAML and fields for url and template

url: ~
template:
  bootstrap: 5

Now we can use pkgdown::build_site() to create the actual website for the package. When you call this function, you’ll see a verbose response from R followed by a preview of your site opened in your web browser.

Note: Before proceeding, we need to make a small change to our .gitignore file. When we run build_site(), it creates the necessary html and supporting files in the /docs directory, so we need to remove it from .gitignore so that we can commit its contents and push the changes to GitHub.

Task: Open your .gitignore file and look for docs (Mark’s is in a block called # R Environment Variables). Comment it out.

# R Environment Variables
.Renviron
.Rproj.user
inst/doc
/doc/
/Meta/
# docs

Task: Call build_site() to create the necessary html and supporting files in the /docs directory.

> build_site()
-- Installing package into temporary library -------------------------------------------
== Building pkgdown site =======================================================
Reading from: '/Users/mark/Documents/GitHub/FISH549/pets'
Writing to:   '/Users/mark/Documents/GitHub/FISH549/pets/docs'
-- Initialising site -----------------------------------------------------------
-- Building home ---------------------------------------------------------------
Reading 'LICENSE.md'
Writing '404.html'
-- Building function reference -------------------------------------------------
Reading 'man/cats.Rd'
Writing 'reference/cats.html'
-- Building articles -----------------------------------------------------------
Writing 'articles/index.html'
Reading 'vignettes/example-usage.Rmd'
Writing 'articles/example-usage.html'
Writing 'sitemap.xml'
-- Building search index -------------------------------------------------------
== DONE ========================================================================
-- Previewing site ---------------------------------------------------------------------

Task: Commit your changes and push them to GitHub.

Task: Navigate to your pets repo on GitHub and click on the Settings tab.

Task: Under the section on the left titled Code and automation, click on Pages.

Task: Under Branch select main and set the folder to /docs.

Task: Click Save when you’re done.

Task: Navigate to https://USERNAME.github.io/pets/ to view the website for your package (where USERNAME is your GitHub user name).

Tip: If youget a 404 error or your site doesn’t display properly, wait a few minute and force-refresh your browser.


Hex sticker

Let’s be honest—hex stickers are all the rage when it comes to R packages. We can make our own hex sticker for our {pets} package with the {hexSticker} package. The function sticker() will do all of the work for us.

Here we’ll include an image of a silhouette cata and dog on our sticker.

Task: Download the cat_and_dog.png image by navigating here and right-clicking on the image. Choose Save Image As… and save it in the working directory for your {pets} package.

Task: Load {hexSticker} and make the following call to sticker() (you can type ?sticker to see all of the function arguments).

> library(hexSticker)
## create sticker image
sticker("cat_and_dog.png",
        ## package name
        package = "pets",
        ## heights & widths
        p_size = 18, s_x = 0.95, s_y = 0.9, asp = 0.85,
        s_width = 0.65, s_height = 0.65, p_y = 1.6,
        ## text color
        p_color = "#4b2e83",
        ## fill color
        h_fill = "white",
        ## border color
        h_color = "#85754d",
        ## filename to save
        filename = "logo.png")

Task: Tell R to use our new image with usethis::use_logo().

> usethis::use_logo("logo.png")
✓ Creating 'man/figures/'
✓ Resized 'logo.png' to 240x278
● Add logo to your README with the following html:
Warning: pkgdown config does not specify the site's url, which is optional but recommended
  # pets <img src='man/figures/logo.png' align="right" height="139" />
  [Copied to clipboard]

The last thing we need to do is edit our README.Rmd file to include a reference to the sticker.

Task: Add the following line of code to the right of the # pets heading.

# pets <img src="man/figures/logo.png" align="right" alt="" width="120" />

Task: When you are finished, click the Knit button in RStudio to preview your new readme file.

Task: Lastly, we need to rebuild the website with build_site().

> build_site()
-- Installing package into temporary library -------------------------------------------
== Building pkgdown site =======================================================
Reading from: '/Users/mark/Documents/GitHub/FISH549/pets'
Writing to:   '/Users/mark/Documents/GitHub/FISH549/pets/docs'
-- Initialising site -----------------------------------------------------------
-- Building favicons -----------------------------------------------------------
Building favicons with realfavicongenerator.net...
Copying 'pkgdown/favicon/apple-touch-icon-120x120.png' to 'apple-touch-icon-120x120.png'
Copying 'pkgdown/favicon/apple-touch-icon-152x152.png' to 'apple-touch-icon-152x152.png'
Copying 'pkgdown/favicon/apple-touch-icon-180x180.png' to 'apple-touch-icon-180x180.png'
Copying 'pkgdown/favicon/apple-touch-icon-60x60.png' to 'apple-touch-icon-60x60.png'
Copying 'pkgdown/favicon/apple-touch-icon-76x76.png' to 'apple-touch-icon-76x76.png'
Copying 'pkgdown/favicon/apple-touch-icon.png' to 'apple-touch-icon.png'
Copying 'pkgdown/favicon/favicon-16x16.png' to 'favicon-16x16.png'
Copying 'pkgdown/favicon/favicon-32x32.png' to 'favicon-32x32.png'
Copying 'pkgdown/favicon/favicon.ico' to 'favicon.ico'
Copying 'logo.png' to 'logo.png'
-- Building home ---------------------------------------------------------------
Writing 'authors.html'
Reading 'LICENSE.md'
Writing 'LICENSE.html'
Writing 'LICENSE-text.html'
Copying 'man/figures/logo.png' to 'reference/figures/logo.png'
Writing '404.html'
-- Building function reference -------------------------------------------------
Writing 'reference/index.html'
Reading 'man/cats.Rd'
Writing 'reference/cats.html'
-- Building articles -----------------------------------------------------------
Writing 'articles/index.html'
Reading 'vignettes/example-usage.Rmd'
Writing 'articles/example-usage.html'
Writing 'sitemap.xml'
-- Building search index -------------------------------------------------------
== DONE ========================================================================
-- Previewing site ---------------------------------------------------------------------

Task: When it finishes, go ahead and commit your changes and push them to GitHub.

Task: Finally, navigate back to your pets repo on GitHub and preview your changes.