1---
2title: "Snapshot tests"
3output: rmarkdown::html_vignette
4vignette: >
5  %\VignetteIndexEntry{Snapshot tests}
6  %\VignetteEngine{knitr::rmarkdown}
7  %\VignetteEncoding{UTF-8}
8---
9
10```{r, include = FALSE}
11knitr::opts_chunk$set(
12  collapse = TRUE,
13  comment = "#>"
14)
15set.seed(1014)
16```
17
18The goal of a unit test is to record the expected output of a function using code.
19This is a powerful technique because not only does it ensure that code doesn't change unexpectedly, it also expresses the desired behaviour in a way that a human can understand.
20
21However, it's not always convenient to record the expected behaviour with code.
22Some challenges include:
23
24-   Text output that includes many characters like quotes and newlines that require special handling in a string.
25
26-   Output that is large, making it painful to define the reference output, and bloating the size of the test file and making it hard to navigate.
27
28-   Binary formats like plots or images, which are very difficult to describe in code: i.e. the plot looks right, the error message is useful to a human, the print method uses colour effectively.
29
30For these situations, testthat provides an alternative mechanism: snapshot tests.
31Instead of using code to describe expected output, snapshot tests (also known as [golden tests](https://ro-che.info/articles/2017-12-04-golden-tests)) record results in a separate human readable file.
32Snapshot tests in testthat are inspired primarily by [Jest](https://jestjs.io/docs/en/snapshot-testing), thanks to a number of very useful discussions with Joe Cheng.
33
34```{r setup}
35library(testthat)
36```
37
38```{r include = FALSE}
39snapper <- local_snapshotter()
40snapper$start_file("snapshotting.Rmd", "test")
41```
42
43## Basic workflow
44
45We'll illustrate the basic workflow with a simple function that generates an HTML heading.
46It can optionally include an `id` attribute, which allows you to construct a link directly to that heading.
47
48```{r}
49bullets <- function(text, id = NULL) {
50  paste0(
51    "<ul", if (!is.null(id)) paste0(" id=\"", id, "\""), ">\n",
52    paste0("  <li>", text, "</li>\n", collapse = ""),
53    "</ul>\n"
54  )
55}
56cat(bullets("a", id = "x"))
57```
58
59Testing this simple function is relatively painful.
60To write the test you have to carefully escape the newlines and quotes.
61And then when you re-read the test in the future, all that escaping makes it hard to tell exactly what it's supposed to return.
62
63```{r}
64test_that("bullets", {
65  expect_equal(bullets("a"), "<ul>\n  <li>a</li>\n</ul>\n")
66  expect_equal(bullets("a", id = "x"), "<ul id=\"x\">\n  <li>a</li>\n</ul>\n")
67})
68```
69
70This is a great place to use snapshot testing.
71To do this we make two changes to our code:
72
73-   We use `expect_snapshot()` instead of `expect_equal()`
74
75-   We wrap the call in `cat()` (to avoid `[1]` in the output, like in my first interactive example).
76
77This yields the following test:
78
79```{r}
80test_that("bullets", {
81  expect_snapshot(cat(bullets("a")))
82  expect_snapshot(cat(bullets("a", "b")))
83})
84```
85
86```{r, include = FALSE}
87# Reset snapshot test
88snapper$end_file()
89snapper$start_file("snapshotting.Rmd", "test")
90```
91
92When we run the test for the first time, it automatically generates reference output, and prints it, so that you can visually confirm that it's correct.
93The output is automatically saved in `_snaps/{name}.R`.
94The name of the snapshot matches your test file name --- e.g. if your test is `test-pizza.R` then your snapshot will be saved in `test/testthat/_snaps/pizza.md`.
95As the file name suggests, this is a markdown file, which I'll explain shortly.
96
97If you run the test again, it'll succeed:
98
99```{r}
100test_that("bullets", {
101  expect_snapshot(cat(bullets("a")))
102  expect_snapshot(cat(bullets("a", "b")))
103})
104```
105
106```{r, include = FALSE}
107# Reset snapshot test
108snapper$end_file()
109snapper$start_file("snapshotting.Rmd", "test")
110```
111
112But if you change the underlying code, say to tweak the indenting, the test will fail:
113
114```{r, error = TRUE}
115bullets <- function(text, id = NULL) {
116  paste0(
117    "<ul", if (!is.null(id)) paste0(" id=\"", id, "\""), ">\n",
118    paste0("<li>", text, "</li>\n", collapse = ""),
119    "</ul>\n"
120  )
121}
122test_that("bullets", {
123  expect_snapshot(cat(bullets("a")))
124  expect_snapshot(cat(bullets("a", "b")))
125})
126```
127
128If this is a deliberate change, you can follow the advice in the message and update the snapshots for that file by running `snapshot_accept("pizza")`; otherwise you can fix the bug and your tests will pass once more.
129(You can also accept snapshot for all files with `snapshot_accept()`).
130
131### Snapshot format
132
133Snapshots are recorded using a subset of markdown.
134You might wonder why we use markdown?
135It's important that snapshots be readable by humans, because humans have to look at it during code reviews.
136Reviewers often don't run your code but still want to understand the changes.
137
138Here's the snapshot file generated by the test above:
139
140``` md
141# bullets
142
143    <ul>
144      <li>a</li>
145    </ul>
146
147---
148
149    <ul id="x">
150      <li>a</li>
151    </ul>
152```
153
154Each test starts with `# {test name}`, a level 1 heading.
155Within a test, each snapshot expectation is indented by four spaces, i.e. as code, and are separated by `---`, a horizontal rule.
156
157### Interactive usage
158
159Because the snapshot output uses the name of the current test file and the current test, snapshot expectations don't really work when run interactively at the console.
160Since they can't automatically find the reference output, they instead just print the current value for manual inspection.
161
162## Other types of output
163
164So far we've focussed on snapshot tests for output printed to the console.
165But `expect_snapshot()` also captures messages, errors, and warnings.
166The following function generates a some output, a message, and a warning:
167
168```{r}
169f <- function() {
170  print("Hello")
171  message("Hi!")
172  warning("How are you?")
173}
174```
175
176And `expect_snapshot()` captures them all:
177
178```{r}
179test_that("f() makes lots of noice", {
180  expect_snapshot(f())
181})
182```
183
184Capturing errors is *slightly* more difficult because `expect_snapshot()` will fail when there's an error:
185
186```{r, error = TRUE}
187test_that("you can't add a number and a letter", {
188  expect_snapshot(1 + "a")
189})
190```
191
192This is a safety valve that ensures that you don't accidentally write broken code.
193To deliberately snapshot an error, you'll have to specifically request it with `error = TRUE`:
194
195```{r}
196test_that("you can't add a number and a letter", {
197  expect_snapshot(1 + "a", error = TRUE)
198})
199```
200
201When the code gets longer, I like to put `error = TRUE` up front so it's a little more obvious:
202
203```{r}
204test_that("you can't add weird thngs", {
205  expect_snapshot(error = TRUE, {
206    1 + "a"
207    mtcars + iris
208    mean + sum
209  })
210})
211```
212
213## Other types of snapshot
214
215`expect_snapshot()` is the most used snapshot function because it records everything: the code you run, printed output, messages, warnings, and errors.
216But sometimes you just want to capture the output or errors in which you might want to use `expect_snapshot_output()` or `expect_snapshot_error()`.
217
218Or rather than caring about side-effects, you may want to check that the value of an R object stays the same.
219In this case, you can use `expect_snapshot_value()` which offers a number of serialisation approaches that provide a tradeoff between accuracy and human readability.
220
221## Whole file snapshotting
222
223`expect_snapshot()`, `expect_snapshot_output()`, `expect_snapshot_error()`, and `expect_snapshot_value()` all store their snapshots in a single file per test.
224But that doesn't work for all file types --- for example, what happens if you want to snapshot an image?
225`expect_snapshot_file()` provides an alternative workflow that generates one snapshot per expectation, rather than one file per test.
226Assuming you're in `test-burger.R` then the snapshot created by `expect_snapshot_file(code_that_returns_path_to_file(), "toppings.png")` would be saved in `tests/testthat/_snaps/burger/toppings.png`.
227If a future change in the code creates a different file it will be saved in `tests/testthat/_snaps/burger/toppings.new.png`.
228
229Unlike `expect_snapshot()` and friends, `expect_snapshot_file()` can't provide an automatic diff when the test fails.
230Instead you'll need to call `snapshot_review()`.
231This launches a Shiny app that allows you to visually review each change and approve it if it's deliberate:
232
233![](review-image.png) ![](review-text.png)
234
235The display varies based on the file type (currently text files, common image files, and csv files are supported).
236
237Sometimes the failure occurs in a non-interactive environment where you can't run `snapshot_review()`, e.g. in `R CMD check`.
238In this case, the easiest fix is to retrieve the `.new` file, copy it into the appropriate directory, then run `snapshot_review()` locally.
239If your code was run on a CI platform, you'll need to start by downloading the run "artifact", which contains the check folder.
240
241In most cases, we don't expect you to use `expect_snapshot_file()` directly.
242Instead, you'll use it via a wrapper that does its best to gracefully skip tests when differences in platform or package versions make it unlikely to generate perfectly reproducible output.
243
244## Previous work
245
246This is not the first time that testthat has attempted to provide snapshot testing (although it's the first time I knew what other languages called them).
247This section describes some of the previous attempts and why we believe the new approach is better.
248
249-   `verify_output()` has three main drawbacks:
250
251    -   You have to supply a path where the output will be saved.
252        This seems like a small issue, but thinking of a good name, and managing the difference between interactive and test-time paths introduces a suprising amount of friction.
253
254    -   It always overwrites the previous result; automatically assuming that the changes are correct.
255        That means you have to use it with git and it's easy to accidentally accept unwanted changes.
256
257    -   It's relatively coarse grained, which means tests that use it tend to keep growing and growing.
258
259-   `expect_known_output()` is finer grained version of `verify_output()` that captures output from a single function.
260    The requirement to produce a path for each individual expectation makes it even more painful to use.
261
262-   `expect_known_value()` and `expect_known_hash()` have all the disadvantages of `expect_known_output()`, but also produce binary output meaning that you can't easily review test differences in pull requests.
263