diff --git a/.quarto/_freeze/slides/execute-results/html.json b/.quarto/_freeze/slides/execute-results/html.json index 93b5cc1..6f5231a 100644 --- a/.quarto/_freeze/slides/execute-results/html.json +++ b/.quarto/_freeze/slides/execute-results/html.json @@ -2,7 +2,7 @@ "hash": "f5f23dbaba36d79ec4c065550e20a5b5", "result": { "engine": "knitr", - "markdown": "---\n#title: \"Introduction to Snapshot Testing in R\"\nformat:\n revealjs: \n css: style.css\n theme: simple\n slide-number: true\n preview-links: auto\n code-link: true\n footer: \"Source code for these slides can be found [on GitHub](https://github.com/IndrajeetPatil/intro-to-snapshot-testing/){target='_blank'}.\"\n#author: \"Indrajeet Patil\"\n#affiliation: \nexecute:\n echo: true\n---\n\n\n\n## Introduction to Snapshot Testing in R {style=\"margin-top: 1em;\"}\n\n\n\n::: {style=\"margin-top: 0.5em; margin-bottom: 0.5em; font-size: 1em\"}\n\nIndrajeet Patil\n\n:::\n\n\n::: {style=\"margin-top: 1em; font-size:0.75em\"}\n\n![](media/logos_combined.jpeg){.absolute width=\"750\" height=\"300\"}\n\n:::\n\n## Unit testing {.smaller}\n\nThe goal of a unit test is to capture the *expected* output of a function using *code* and making sure that *actual* output after any changes matches the expected output.\n\n[`{testthat}`](https://testthat.r-lib.org/) is a popular framework for writing unit tests in R. \n\n:::: {.columns}\n\n::: {.column width='60%'}\n\n:::{.callout-important}\n\n## Benefits of unit testing\n\n- insures against unintentionally changing function behaviour\n- prevents re-introducing already fixed bugs\n- acts as the most basic form of developer-focused documentation\n- catches breaking changes coming from upstream dependencies\n- etc.\n\n:::\n\n:::\n\n::: {.column width='40%'}\n\n:::{.callout-tip}\n\n## Test output\n\nTest pass only when actual function behaviour matches expected.\n\n| actual | expected | tests |\n| ------- | ------- | ----- |\n| {{< fa regular file-lines size=2xl >}} | {{< fa regular file-lines size=2xl >}} | {{< fa regular circle-check size=2xl >}} |\n| {{< fa regular file size=2xl >}} | {{< fa regular file-lines size=2xl >}} | {{< fa regular circle-xmark size=2xl >}} |\n\n:::\n\n:::\n\n::::\n\n## Unit testing with `{testthat}`: A recap {.smaller}\n\n:::: {.columns}\n\n::: {.column width='50%'}\n\n::: {.callout-important}\n\n## Test organization\n\nTesting infrastructure for R package has the following hierarchy:\n\n| Component | Role |\n| :------------------------------------------------------- | :---------------------------------------------------------------------------------------------- |\n|
{{< fa regular file-code size=2xl >}} **Test file** | Tests for code situated in `R/foo.R` will typically be in the file `tests/testthat/test-foo.R`. |\n| {{< fa solid flask size=2xl >}} **Tests** | A single file can contain multiple tests.
|\n|
{{< fa solid equals size=2xl >}} **Expectations** | A single test can have multiple expectations that formalize what you expect code to do. |\n\n:::\n\n:::\n\n::: {.column width='50%'}\n\n:::{.callout-tip}\n\n## Example test file\n\n- Every test is a call to `testthat::test_that()` function.\n\n- Every expectation is represented by `testthat::expect_*()` function.\n\n- You can generate a test file using `usethis::use_test()` function.\n\n```{.r}\n# File: tests/testthat/test-op.R\n\n# test-1\ntest_that(\"multiplication works\", {\n expect_equal(2 * 2, 4) # expectation-1\n expect_equal(-2 * 2, -4) # expectation-2\n})\n\n# test-2\ntest_that(\"addition works\", {\n expect_equal(2 + 2, 4) # expectation-1\n expect_equal(-2 + 2, 0) # expectation-2\n})\n\n...\n```\n\n:::\n\n:::\n\n::::\n\n\n## What is different about snapshot testing?\n\n. . .\n\nA **unit test** records the code to describe expected output.\n\n
\n\n\n\n(actual) {{< fa regular file-code size=2xl >}} {{< fa solid arrows-left-right size=2xl >}} {{< fa solid file-code size=2xl >}} (expected)\n\n
\n\n. . .\n\nA **snapshot test** records expected output in a separate, human-readable file.\n\n
\n\n(actual) {{< fa regular file-code size=2xl >}} {{< fa solid arrows-left-right size=2xl >}} {{< fa solid file-lines size=2xl >}} (expected)\n\n## Why do you need snapshot testing?\n\nIf you develop R packages and have struggled to \n\n::: incremental\n\n- test that text output *prints* as expected\n- test that an entire file *is* as expected\n- test that generated graphical output *looks* as expected\n- update such tests *en masse*\n\n::: \n\n. . .\n\nthen you should be excited to know more about *snapshot tests* (aka *golden tests*)! šŸ¤©\n\n# Prerequisites\n\nFamiliarity with writing unit tests using [`{testthat}`](https://testthat.r-lib.org/index.html){target=\"_blank\"}.\n\nIf not, have a look at [this](https://r-pkgs.org/testing-basics.html){target=\"_blank\"} chapter from *R Packages* book.\n\n## \n\n
\n\n:::{.callout-important}\n\n## *Nota bene*\n\nIn the following slides, in all snapshot tests, I include the following line of code:\n\n```r\nlocal_edition(3)\n```\n\n**You don't need to do this in your package tests!**\n\nIt's sufficient to update the `DESCRIPTION` file to use `{testthat}` 3rd edition:\n\n```r\nConfig/testthat/edition: 3\n```\n\nFor more, see [this](https://testthat.r-lib.org/articles/third-edition.html){target=\"_blank\"} article.\n\n:::\n\n# Testing text outputs\n\nSnapshot tests can be used to test that text output *prints* as expected.\n\nImportant for testing functions that pretty-print R objects to the console, create elegant and informative exceptions, etc.\n\n## Example function {.smaller}\n\nLet's say you want to write a unit test for the following function:\n\n:::: {.columns}\n\n::: {.column width='60%'}\n\n**Source code**\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\nprint_movies <- function(keys, values) {\n paste0(\n \"Movie: \\n\",\n paste0(\" \", keys, \": \", values, collapse = \"\\n\")\n )\n}\n```\n:::\n\n\n\n:::\n\n::: {.column width='40%'}\n\n**Output**\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\ncat(print_movies(\n c(\"Title\", \"Director\"),\n c(\"Salaam Bombay!\", \"Mira Nair\")\n))\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nMovie: \n Title: Salaam Bombay!\n Director: Mira Nair\n```\n\n\n:::\n:::\n\n\n\n:::\n\n::::\n\n. . .\n\n
\n\nNote that you want to test that the printed output *looks* as expected. \n\nTherefore, you need to check for all the little bells and whistles in the printed output.\n\n## Example test {.smaller}\n\nEven testing this simple function is a bit painful because you need to keep track of every escape character, every space, etc. \n\n\n\n::: {.cell}\n\n:::\n\n::: {.cell}\n\n```{.r .cell-code}\ntest_that(\"`print_movies()` prints as expected\", {\n expect_equal(\n print_movies(\n c(\"Title\", \"Director\"),\n c(\"Salaam Bombay!\", \"Mira Nair\")\n ),\n \"Movie: \\n Title: Salaam Bombay!\\n Director: Mira Nair\"\n )\n})\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nTest passed šŸ˜€\n```\n\n\n:::\n:::\n\n\n\n. . .\n\nWith a more complex code, it'd be impossible for a human to reason about what the output is supposed to look like.\n\n. . .\n\n:::{.callout-important}\n\nIf this is a utility function used by many other functions, changing its behaviour would entail *manually* changing expected outputs for many tests.\n\nThis is not maintainable! šŸ˜©\n\n:::\n\n## Alternative: Snapshot test {.smaller}\n\nInstead, you can use `expect_snapshot()`, which, when run for the first time, generates a Markdown file with expected/reference output.\n\n\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\ntest_that(\"`print_movies()` prints as expected\", {\n local_edition(3)\n expect_snapshot(cat(print_movies(\n c(\"Title\", \"Director\"),\n c(\"Salaam Bombay!\", \"Mira Nair\")\n )))\n})\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nā”€ā”€ Warning: `print_movies()` prints as expected ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\nAdding new snapshot:\nCode\n cat(print_movies(c(\"Title\", \"Director\"), c(\"Salaam Bombay!\", \"Mira Nair\")))\nOutput\n Movie: \n Title: Salaam Bombay!\n Director: Mira Nair\n```\n\n\n:::\n:::\n\n\n\n\n\n. . .\n\n:::{.callout-warning}\n\nThe first time a snapshot is created, it becomes *the truth* against which future function behaviour will be compared. \n\nThus, it is **crucial** that you carefully check that the output is indeed as expected. šŸ”Ž \n\n:::\n\n## Human-readable Markdown file {.smaller}\n\nCompared to your unit test code representing the expected output\n\n```r\n\"Movie: \\n Title: Salaam Bombay!\\n Director: Mira Nair\"\n```\n\nnotice how much more human-friendly the Markdown output is!\n\n```md\nCode\n cat(print_movies(c(\"Title\", \"Director\"), c(\"Salaam Bombay!\", \"Mira Nair\")))\nOutput\n Movie: \n Title: Salaam Bombay!\n Director: Mira Nair\n```\n\nIt is easy to *see* what the printed text output is *supposed* to look like. In other words, snapshot tests are useful when the *intent* of the code can only be verified by a human.\n\n. . . \n\n:::{.callout-note}\n\n## More about snapshot Markdown files\n\n- If test file is called `test-foo.R`, the snapshot will be saved to `test/testthat/_snaps/foo.md`.\n\n- If there are multiple snapshot tests in a single file, corresponding snapshots will also share the same `.md` file.\n\n- By default, `expect_snapshot()` will capture the code, the object values, and any side-effects.\n\n:::\n\n## What test success looks like {.smaller}\n\nIf you run the test again, it'll succeed:\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\ntest_that(\"`print_movies()` prints as expected\", {\n local_edition(3)\n expect_snapshot(cat(print_movies(\n c(\"Title\", \"Director\"),\n c(\"Salaam Bombay!\", \"Mira Nair\")\n )))\n})\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nTest passed šŸ˜€\n```\n\n\n:::\n:::\n\n\n\n\n\n. . .\n\n
\n\n:::{.callout-note}\n\n### Why does my test fail on a re-run?\n\nIf testing a snapshot you just generated fails on re-running the test, this is most likely because your test is not deterministic. For example, if your function deals with random number generation.\n\nIn such cases, setting a seed (e.g. `set.seed(42)`) should help.\n\n:::\n\n## What test failure looks like {.smaller}\n\nWhen function changes, snapshot doesn't match the reference, and the test fails:\n\n:::: {.columns}\n\n::: {.column width='35%'}\n\n**Changes to function**\n\n```{.r code-line-numbers=\"5\"}\nprint_movies <- function(keys, values) {\n paste0(\n \"Movie: \\n\",\n paste0(\n \" \", keys, \"- \", values,\n collapse = \"\\n\"\n )\n )\n}\n```\n\n\n\n::: {.cell}\n\n:::\n\n\n\n
\n\nFailure message provides expected (`-`) vs observed (`+`) diff.\n\n:::\n\n::: {.column width='65%'}\n\n**Test failure**\n\n```{.r}\ntest_that(\"`print_movies()` prints as expected\", {\n expect_snapshot(cat(print_movies(\n c(\"Title\", \"Director\"),\n c(\"Salaam Bombay!\", \"Mira Nair\")\n )))\n})\n```\n\n\n\n::: {.cell}\n::: {.cell-output .cell-output-stdout}\n\n```\nā”€ā”€ Failure: `print_movies()` prints as expected ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\nSnapshot of code has changed:\nold[2:6] vs new[2:6]\n cat(print_movies(c(\"Title\", \"Director\"), c(\"Salaam Bombay!\", \"Mira Nair\")))\n Output\n Movie: \n- Title: Salaam Bombay!\n+ Title- Salaam Bombay!\n- Director: Mira Nair\n+ Director- Mira Nair\n\n* Run `testthat::snapshot_accept('slides.qmd')` to accept the change.\n* Run `testthat::snapshot_review('slides.qmd')` to interactively review the change.\n```\n\n\n:::\n\n::: {.cell-output .cell-output-error}\n\n```\nError:\n! Test failed\n```\n\n\n:::\n:::\n\n\n\n:::\n\n::::\n\n\n## Fixing tests {.smaller}\n\nMessage accompanying failed tests make it explicit how to fix them.\n\n. . . \n\n- If the change was *deliberate*, you can accept the new snapshot as the current *truth*.\n\n```r\n* Run `snapshot_accept('foo.md')` to accept the change\n```\n\n- If this was *unexpected*, you can review the changes, and decide whether to change the snapshot or to correct the function behaviour instead.\n\n```r\n* Run `snapshot_review('foo.md')` to interactively review the change\n```\n\n. . . \n\n
\n\n:::{.callout-tip}\n\n## Fixing multiple snapshot tests\n\nIf this is a utility function used by many other functions, changing its behaviour would lead to failure of many tests. \n\nYou can update *all* new snapshots with `snapshot_accept()`. And, of course, check the diffs to make sure that the changes are expected.\n\n:::\n\n## Capturing messages and warnings {.smaller}\n\nSo far you have tested text output printed to the console, but you can also use snapshots to capture messages, warnings, and errors.\n\n:::: {.columns}\n\n::: {.column width='50%'}\n\n**message**\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\nf <- function() message(\"Some info for you.\")\n\ntest_that(\"f() messages\", {\n local_edition(3)\n expect_snapshot(f())\n})\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nā”€ā”€ Warning: f() messages ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\nAdding new snapshot:\nCode\n f()\nMessage\n Some info for you.\n```\n\n\n:::\n:::\n\n\n\n:::\n\n::: {.column width='50%'}\n\n**warning**\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\ng <- function() warning(\"Managed to recover.\")\n\ntest_that(\"g() warns\", {\n local_edition(3)\n expect_snapshot(g())\n})\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nā”€ā”€ Warning: g() warns ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\nAdding new snapshot:\nCode\n g()\nCondition\n Warning in `g()`:\n Managed to recover.\n```\n\n\n:::\n:::\n\n\n\n:::\n\n::::\n\n:::{.callout-tip}\n\nSnapshot records both the *condition* and the corresponding *message*.\n\nYou can now rest assured that the users are getting informed the way you want! šŸ˜Œ\n\n:::\n\n## Capturing errors {.smaller}\n\nIn case of an error, the function `expect_snapshot()` itself will produce an error. \nYou have two ways around this:\n\n:::: {.columns}\n\n::: {.column width='50%'}\n\n**Option-1** (recommended)\n\n```{.r code-line-numbers=\"3\"}\ntest_that(\"`log()` errors\", {\n local_edition(3)\n expect_snapshot(log(\"x\"), error = TRUE)\n})\n```\n\n\n\n::: {.cell}\n::: {.cell-output .cell-output-stdout}\n\n```\nā”€ā”€ Warning: `log()` errors ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\nAdding new snapshot:\nCode\n log(\"x\")\nCondition\n Error in `log()`:\n ! non-numeric argument to mathematical function\n```\n\n\n:::\n:::\n\n\n\n:::\n\n::: {.column width='50%'}\n\n**Option-2**\n\n```{.r code-line-numbers=\"3\"}\ntest_that(\"`log()` errors\", {\n local_edition(3)\n expect_snapshot_error(log(\"x\"))\n})\n```\n\n\n\n::: {.cell}\n::: {.cell-output .cell-output-stdout}\n\n```\nā”€ā”€ Warning: `log()` errors ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\nAdding new snapshot:\nnon-numeric argument to mathematical function\n```\n\n\n:::\n:::\n\n\n\n:::\n\n::::\n\n:::{.callout-tip}\n\n### Which option should I use?\n\n- If you want to capture both the code and the error message, use `expect_snapshot(..., error = TRUE)`.\n\n- If you want to capture only the error message, use `expect_snapshot_error()`.\n\n:::\n\n## Further reading {.smaller}\n\n- `{testthat}` article on [snapshot testing](https://testthat.r-lib.org/articles/snapshotting.html){target=\"_blank\"}\n\n- Introduction to [golden testing](https://ro-che.info/articles/2017-12-04-golden-tests){target=\"_blank\"}\n\n- Docs for [Jest](https://jestjs.io/docs/snapshot-testing){target=\"_blank\"} library in JavaScript, which inspired snapshot testing implementation in `{testthat}`\n\n\n# Testing graphical outputs\n\nTo create graphical expectations, you will use `{testthat}` extension package: [`{vdiffr}`](https://vdiffr.r-lib.org/){target=\"_blank\"}.\n\n## How does `{vdiffr}` work? {.smaller}\n\n`{vdiffr}` introduces `expect_doppelganger()` to generate `{testthat}` expectations for graphics. It does this by writing SVG snapshot files for outputs!\n\n. . . \n\nThe figure to test can be:\n\n- a `ggplot` object (from `ggplot2::ggplot()`)\n- a `recordedplot` object (from `grDevices::recordPlot()`)\n- any object with a `print()` method\n\n. . . \n\n:::{.callout-note}\n\n- If test file is called `test-foo.R`, the snapshot will be saved to `test/testthat/_snaps/foo` folder.\n\n- In this folder, there will be one `.svg` file for every test in `test-foo.R`.\n\n- The name for the `.svg` file will be sanitized version of `title` argument to `expect_doppelganger()`.\n\n:::\n\n## Example function {.smaller}\n\nLet's say you want to write a unit test for the following function:\n\n:::: {.columns}\n\n::: {.column width='50%'}\n\n**Source code**\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(ggplot2)\n\ncreate_scatter <- function() {\n ggplot(mtcars, aes(wt, mpg)) +\n geom_point(size = 3, alpha = 0.75) +\n geom_smooth(method = \"lm\")\n}\n```\n:::\n\n\n:::\n\n::: {.column width='50%'}\n\n**Output**\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\ncreate_scatter()\n```\n\n::: {.cell-output-display}\n![](slides_files/figure-revealjs/unnamed-chunk-17-1.png){width=100%}\n:::\n:::\n\n\n\n:::\n\n::::\n\n. . .\n\nNote that you want to test that the graphical output *looks* as expected, and this expectation is difficult to capture with a unit test.\n\n## Graphical snapshot test {.smaller}\n\nYou can use `expect_doppelganger()` from `{vdiffr}` to test this!\n\n. . .\n\nThe *first time* you run the test, it'd generate an `.svg` file with expected output.\n\n\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\ntest_that(\"`create_scatter()` plots as expected\", {\n local_edition(3)\n expect_doppelganger(\n title = \"create scatter\",\n fig = create_scatter(),\n )\n})\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nā”€ā”€ Warning: `create_scatter()` plots as expected ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\nAdding new file snapshot: 'tests/testthat/_snaps/create-scatter.svg'\n```\n\n\n:::\n:::\n\n\n\n\n\n. . .\n\n:::{.callout-warning}\n\nThe first time a snapshot is created, it becomes *the truth* against which future function behaviour will be compared. \n\nThus, it is **crucial** that you carefully check that the output is indeed as expected. šŸ”Ž \n\nYou can open `.svg` snapshot files in a web browser for closer inspection.\n\n:::\n\n## What test success looks like {.smaller}\n\nIf you run the test again, it'll succeed:\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\ntest_that(\"`create_scatter()` plots as expected\", {\n local_edition(3)\n expect_doppelganger(\n title = \"create scatter\",\n fig = create_scatter(),\n )\n})\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nTest passed šŸ„³\n```\n\n\n:::\n:::\n\n\n\n\n\n## What test failure looks like {.smaller}\n\nWhen function changes, snapshot doesn't match the reference, and the test fails:\n\n:::: {.columns}\n\n::: {.column width='40%'}\n\n**Changes to function**\n\n```{.r code-line-numbers=\"3\"}\ncreate_scatter <- function() {\n ggplot(mtcars, aes(wt, mpg)) +\n geom_point(size = 2, alpha = 0.85) +\n geom_smooth(method = \"lm\")\n}\n```\n\n\n\n::: {.cell}\n\n:::\n\n\n\n:::\n\n::: {.column width='60%'}\n\n**Test failure**\n\n\n\n\n\n```{.r}\ntest_that(\"`create_scatter()` plots as expected\", {\n local_edition(3)\n expect_doppelganger(\n title = \"create scatter\",\n fig = create_scatter(),\n )\n})\n\nā”€ā”€ Failure (':3'): `create_scatter()` plots as expected ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\nSnapshot of `testcase` to 'slides.qmd/create-scatter.svg' has changed\nRun `testthat::snapshot_review('slides.qmd/')` to review changes\nBacktrace:\n 1. vdiffr::expect_doppelganger(...)\n 3. testthat::expect_snapshot_file(...)\nError in `reporter$stop_if_needed()`:\n! Test failed\n```\n\n:::\n\n::::\n\n## Fixing tests {.smaller}\n\nRunning `snapshot_review()` launches a Shiny app which can be used to either accept or reject the new output(s).\n\n\n\n::: {.cell}\n::: {.cell-output-display}\n![](media/shiny_app_graphics.mov){width=60%}\n:::\n:::\n\n\n\n##\n\n:::{.callout-tip}\n\n## Why are my snapshots for plots failing?! šŸ˜”\n\nIf tests fail even if the function didn't change, it can be due to any of the following reasons:\n\n- R's graphics engine changed\n- `{ggplot2}` itself changed\n- non-deterministic behaviour\n- changes in system libraries\n\nFor these reasons, snapshot tests for plots tend to be fragile and are not run on CRAN machines by default.\n\n:::\n\n## Further reading\n\n- `{vdiffr}` package [website](https://vdiffr.r-lib.org/){target=\"_blank\"}\n\n# Testing entire files\n\nWhole file snapshot testing makes sure that media, data frames, text files, etc. are as expected.\n\n## Writing test {.smaller}\n\nLet's say you want to test JSON files generated by `jsonlite::write_json()`.\n\n:::: {.columns}\n\n::: {.column width='50%'}\n\n**Test**\n\n\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\n# File: tests/testthat/test-write-json.R\ntest_that(\"json writer works\", {\n local_edition(3)\n \n r_to_json <- function(x) {\n path <- tempfile(fileext = \".json\")\n jsonlite::write_json(x, path)\n path\n }\n\n x <- list(1, list(\"x\" = \"a\"))\n expect_snapshot_file(r_to_json(x), \"demo.json\")\n})\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nā”€ā”€ Warning: json writer works ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\nAdding new file snapshot: 'tests/testthat/_snaps/demo.json'\n```\n\n\n:::\n:::\n\n\n\n\n\n**Snapshot**\n\n```json\n[[1],{\"x\":[\"a\"]}]\n```\n\n:::\n\n::: {.column width='50%'}\n\n:::{.callout-note}\n\n- To snapshot a file, you need to write a helper function that provides its path.\n\n- If a test file is called `test-foo.R`, the snapshot will be saved to `test/testthat/_snaps/foo` folder.\n\n- In this folder, there will be one file (e.g. `.json`) for every `expect_snapshot_file()` expectation in `test-foo.R`.\n\n- The name for snapshot file is taken from `name` argument to `expect_snapshot_file()`.\n\n:::\n\n:::\n\n::::\n\n\n## What test success looks like {.smaller}\n\nIf you run the test again, it'll succeed:\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\n# File: tests/testthat/test-write-json.R\ntest_that(\"json writer works\", {\n local_edition(3)\n \n r_to_json <- function(x) {\n path <- tempfile(fileext = \".json\")\n jsonlite::write_json(x, path)\n path\n }\n\n x <- list(1, list(\"x\" = \"a\"))\n expect_snapshot_file(r_to_json(x), \"demo.json\")\n})\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nTest passed šŸŽ‰\n```\n\n\n:::\n:::\n\n\n\n\n\n## What test failure looks like {.smaller}\n\nIf the new output doesn't match the expected one, the test will fail:\n\n```{.r code-line-numbers=\"11\"}\n# File: tests/testthat/test-write-json.R\ntest_that(\"json writer works\", {\n local_edition(3)\n \n r_to_json <- function(x) {\n path <- tempfile(fileext = \".json\")\n jsonlite::write_json(x, path)\n path\n }\n\n x <- list(1, list(\"x\" = \"b\"))\n expect_snapshot_file(r_to_json(x), \"demo.json\")\n})\n```\n\n\n\n::: {.cell}\n::: {.cell-output .cell-output-stdout}\n\n```\nā”€ā”€ Failure: json writer works ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\nSnapshot of `r_to_json(x)` to 'slides.qmd/demo.json' has changed\nRun `testthat::snapshot_review('slides.qmd/')` to review changes\n```\n\n\n:::\n\n::: {.cell-output .cell-output-error}\n\n```\nError:\n! Test failed\n```\n\n\n:::\n:::\n\n\n\n## Fixing tests {.smaller}\n\nRunning `snapshot_review()` launches a Shiny app which can be used to either accept or reject the new output(s).\n\n\n\n::: {.cell}\n::: {.cell-output-display}\n![](media/json_snapshot.png){width=622}\n:::\n:::\n\n\n\n## Further reading {.smaller}\n\nDocumentation for [`expect_snapshot_file()`](https://testthat.r-lib.org/reference/expect_snapshot_file.html){target=\"_blank\"}\n\n# Testing Shiny applications\n\nTo write formal tests for Shiny applications, you will use `{testthat}` extension package: [`{shinytest2}`](https://rstudio.github.io/shinytest2/){target=\"_blank\"}.\n\n## How does `{shinytest2}` work? {.smaller}\n\n`{shinytest2}` uses a Shiny app (how meta! šŸ˜…) to record user interactions with the app and generate snapshots of the application's state. Future behaviour of the app will be compared against these snapshots to check for any changes.\n\n. . . \n\nExactly how tests for Shiny apps in R package are written depends on how the app is stored.\nThere are two possibilities, and you will discuss them both separately.\n\n. . . \n\n
\n\n:::: {.columns}\n\n::: {.column width='50%'}\n\n**Stored in `/inst` folder**\n\n```\nā”œā”€ā”€ DESCRIPTION\nā”œā”€ā”€ R\nā”œā”€ā”€ inst\nā”‚ ā””ā”€ā”€ sample_app\nā”‚ ā””ā”€ā”€ app.R\n```\n\n:::\n\n::: {.column width='50%'}\n\n**Returned by a function**\n\n```\nā”œā”€ā”€ DESCRIPTION\nā”œā”€ā”€ R\nā”‚ ā””ā”€ā”€ app-function.R\n```\n\n:::\n\n::::\n\n\n\n# Shiny app in subdirectory \n\n
\n\n```\nā”œā”€ā”€ DESCRIPTION\nā”œā”€ā”€ R\nā”œā”€ā”€ inst\nā”‚ ā””ā”€ā”€ sample_app\nā”‚ ā””ā”€ā”€ app.R\n```\n\n## Example app {.smaller}\n\nLet's say this app resides in the `inst/unitConverter/app.R` file.\n\n\n\n:::: {.columns}\n\n::: {.column width='40%'}\n\n**App**\n\n\n\n::: {.cell}\n::: {.cell-output-display}\n![](media/shiny_app.mov)\n:::\n:::\n\n\n\n:::\n\n::: {.column width='60%'}\n\n**Code**\n\n```{.r}\nlibrary(shiny)\n\nui <- fluidPage(\n titlePanel(\"Convert kilograms to grams\"),\n numericInput(\"kg\", \"Weight (in kg)\", value = 0),\n textOutput(\"g\")\n)\n\nserver <- function(input, output, session) {\n output$g <- renderText(\n paste0(\"Weight (in g): \", input$kg * 1000)\n )\n}\n\nshinyApp(ui, server)\n```\n\n:::\n\n::::\n\n## Generating a test {.smaller}\n\nTo create a snapshot test, go to the app directory and run `record_test()`.\n\n\n\n::: {.cell}\n::: {.cell-output-display}\n![](media/shinytest_record.mov){width=60%}\n:::\n:::\n\n\n\n## Auto-generated artifacts {.smaller}\n\n:::: {.columns}\n\n::: {.column width='60%'}\n\n**Test**\n\n```{.r}\nlibrary(shinytest2)\n\ntest_that(\"{shinytest2} recording: unitConverter\", {\n app <- AppDriver$new(\n name = \"unitConverter\", height = 543, width = 426)\n app$set_inputs(kg = 1)\n app$set_inputs(kg = 10)\n app$expect_values()\n})\n\n```\n\n:::\n\n::: {.column width='40%'}\n\n**Snapshot**\n\n\n\n::: {.cell}\n::: {.cell-output-display}\n![](media/unitConverter-001_.png){width=100%}\n:::\n:::\n\n\n\n:::\n\n::::\n\n:::{.callout-note}\n\n- `record_test()` will auto-generate a test file in the app directory. The test script will be saved in a *subdirectory* of the app (`inst/my-app/tests/testthat/test-shinytest2.R`).\n\n- There will be one `/tests` folder inside *every* app folder.\n\n- The snapshots are saved as `.png` file in `tests/testthat/test-shinytest2/_snaps/{.variant}/shinytest2`. The `{.variant}` here corresponds to operating system and R version used to record tests. For example, `_snaps/windows-4.1/shinytest2`.\n\n:::\n\n## Creating a driver script {.smaller}\n\nNote that currently your test scripts and results are in the `/inst` folder, but you'd also want to run these tests automatically using `{testthat}`.\n\nFor this, you will need to write a driver script like the following:\n\n```{.r}\nlibrary(shinytest2)\n\ntest_that(\"`unitConverter` app works\", {\n appdir <- system.file(package = \"package_name\", \"unitConverter\")\n test_app(appdir)\n})\n```\n\nNow the Shiny apps will be tested with the rest of the source code in the package! šŸŽŠ\t\n\n:::{.callout-tip}\n\nYou save the driver test in the `/tests` folder (`tests/testthat/test-inst-apps.R`), alongside other tests.\n\n:::\n\n## What test failure looks like {.smaller}\n\nLet's say, while updating the app, you make a mistake, which leads to a failed test.\n\n**Changed code with mistake**\n\n```{.r code-line-numbers=\"9\"}\nui <- fluidPage(\n titlePanel(\"Convert kilograms to grams\"),\n numericInput(\"kg\", \"Weight (in kg)\", value = 0),\n textOutput(\"g\")\n)\n\nserver <- function(input, output, session) {\n output$g <- renderText(\n paste0(\"Weight (in kg): \", input$kg * 1000) # should be `\"Weight (in g): \"`\n )\n}\n\nshinyApp(ui, server)\n```\n\n**Test failure JSON diff**\n\n```{.json code-line-number=\"6\"}\nDiff in snapshot file `shinytest2unitConverter-001.json`\n< before > after \n@@ 4,5 @@ @@ 4,5 @@ \n }, }, \n \"output\": { \"output\": { \n< \"g\": \"Weight (in g): 10000\" > \"g\": \"Weight (in kg): 10000\"\n }, }, \n \"export\": { \"export\": { \n```\n\n\n## Updating snapshots {.smaller}\n\nFixing this test will be similar to fixing any other snapshot test you've seen thus far.\n\n`{testthat2}` provides a Shiny app for comparing the old and new snapshots.\n\n\n\n::: {.cell}\n::: {.cell-output-display}\n![](media/shinytest_failure.mov){width=60%}\n:::\n:::\n\n\n\n# Function returns Shiny app\n\n
\n\n```\nā”œā”€ā”€ DESCRIPTION\nā”œā”€ā”€ R\nā”‚ ā””ā”€ā”€ app-function.R\n```\n\n## Example app and test {.smaller}\n\nThe only difference in testing workflow when Shiny app objects are created by functions is that you will write the test ourselves, instead of `{shinytest2}` auto-generating it.\n\n:::: {.columns}\n\n::: {.column width='55%'}\n\n**Source code**\n\n```{.r}\n# File: R/unit-converter.R\nunitConverter <- function() {\n ui <- fluidPage(\n titlePanel(\"Convert kilograms to grams\"),\n numericInput(\"kg\", \"Weight (in kg)\", value = 0),\n textOutput(\"g\")\n )\n\n server <- function(input, output, session) {\n output$g <- renderText(\n paste0(\"Weight (in g): \", input$kg * 1000)\n )\n }\n\n shinyApp(ui, server)\n}\n```\n\n\n:::\n\n::: {.column width='45%'}\n\n**Test file to modify**\n\n```{.r}\n# File: tests/testthat/test-unit-converter.R\ntest_that(\"unitConverter app works\", {\n shiny_app <- unitConverter()\n app <- AppDriver$new(shiny_app)\n})\n```\n\n\n:::\n\n::::\n\n## Generating test and snapshots {.smaller}\n\nyou call `record_test()` directly on a Shiny app object, copy-paste commands to the test script, and run `devtools::test_active_file()` to generate snapshots.\n\n\n\n::: {.cell}\n::: {.cell-output-display}\n![](media/shiny_app_function_return.mov){width=60%}\n:::\n:::\n\n\n\n## Testing apps from frameworks {.smaller}\n\nThis testing workflow is also relevant for app frameworks (e.g. [`{golem}`](https://thinkr-open.github.io/golem/index.html){target=\"_blank\"}, [`{rhino}`](https://appsilon.github.io/rhino/){target=\"_blank\"}, etc.).\n\n:::: {.columns}\n\n::: {.column width='50%'}\n\n**`{golem}`**\n\nFunction in `run_app.R`\nreturns app.\n\n```\nā”œā”€ā”€ DESCRIPTION \nā”œā”€ā”€ NAMESPACE \nā”œā”€ā”€ R \nā”‚ ā”œā”€ā”€ app_config.R \nā”‚ ā”œā”€ā”€ app_server.R \nā”‚ ā”œā”€ā”€ app_ui.R \nā”‚ ā””ā”€ā”€ run_app.R \n```\n\n:::\n\n::: {.column width='50%'}\n\n**`{rhino}`**\n\nFunction in `app.R`\nreturns app.\n\n```\nā”œā”€ā”€ app\nā”‚ ā”œā”€ā”€ js\nā”‚ ā”‚ ā””ā”€ā”€ index.js\nā”‚ ā”œā”€ā”€ logic\nā”‚ ā”‚ ā””ā”€ā”€ __init__.R\nā”‚ ā”œā”€ā”€ static\nā”‚ ā”‚ ā””ā”€ā”€ favicon.ico\nā”‚ ā”œā”€ā”€ styles\nā”‚ ā”‚ ā””ā”€ā”€ main.scss\nā”‚ ā”œā”€ā”€ view\nā”‚ ā”‚ ā””ā”€ā”€ __init__.R\nā”‚ ā””ā”€ā”€ main.R\nā”œā”€ā”€ tests\nā”‚ ā”œā”€ā”€ ...\nā”œā”€ā”€ app.R\nā”œā”€ā”€ RhinoApplication.Rproj\nā”œā”€ā”€ dependencies.R\nā”œā”€ā”€ renv.lock\nā””ā”€ā”€ rhino.yml\n```\n\n:::\n\n::::\n\n\n## Final directory structure {.smaller}\n\nThe final location of the tests and snapshots should look like the following for the two possible ways Shiny apps are included in R packages.\n\n
\n\n:::: {.columns}\n\n::: {.column width='50%'}\n\n**Stored in `/inst` folder**\n\n```\nā”œā”€ā”€ DESCRIPTION\nā”œā”€ā”€ R\nā”œā”€ā”€ inst\nā”‚ ā””ā”€ā”€ sample_app\nā”‚ ā”œā”€ā”€ app.R\nā”‚ ā””ā”€ā”€ tests\nā”‚ ā”œā”€ā”€ testthat\nā”‚ ā”‚ ā”œā”€ā”€ _snaps\nā”‚ ā”‚ ā”‚ ā””ā”€ā”€ shinytest2\nā”‚ ā”‚ ā”‚ ā””ā”€ā”€ 001.json\nā”‚ ā”‚ ā””ā”€ā”€ test-shinytest2.R\nā”‚ ā””ā”€ā”€ testthat.R\nā””ā”€ā”€ tests\n ā”œā”€ā”€ testthat\n ā”‚ ā””ā”€ā”€ test-inst-apps.R\n ā””ā”€ā”€ testthat.R\n```\n\n:::\n\n::: {.column width='50%'}\n\n**Returned by a function**\n\n```\nā”œā”€ā”€ DESCRIPTION\nā”œā”€ā”€ R\nā”‚ ā””ā”€ā”€ app-function.R\nā””ā”€ā”€ tests\n ā”œā”€ā”€ testthat\n ā”‚ ā”œā”€ā”€ _snaps\n ā”‚ ā”‚ ā””ā”€ā”€ app-function\n ā”‚ ā”‚ ā””ā”€ā”€ 001.json\n ā”‚ ā””ā”€ā”€ test-app-function.R\n ā””ā”€ā”€ testthat.R\n```\n\n:::\n\n::::\n\n## Testing multiple apps {.smaller}\n\nFor the sake of completeness, here is what the test directory structure would like when there are multiple apps in a single package.\n\n:::: {.columns}\n\n::: {.column width='50%'}\n\n**Stored in `/inst` folder**\n\n```\nā”œā”€ā”€ DESCRIPTION\nā”œā”€ā”€ R\nā”œā”€ā”€ inst\nā”‚ ā””ā”€ā”€ sample_app1\nā”‚ ā”œā”€ā”€ app.R\nā”‚ ā””ā”€ā”€ tests\nā”‚ ā”œā”€ā”€ testthat\nā”‚ ā”‚ ā”œā”€ā”€ _snaps\nā”‚ ā”‚ ā”‚ ā””ā”€ā”€ shinytest2\nā”‚ ā”‚ ā”‚ ā””ā”€ā”€ 001.json\nā”‚ ā”‚ ā””ā”€ā”€ test-shinytest2.R\nā”‚ ā””ā”€ā”€ testthat.R\nā”‚ ā””ā”€ā”€ sample_app2\nā”‚ ā”œā”€ā”€ app.R\nā”‚ ā””ā”€ā”€ tests\nā”‚ ā”œā”€ā”€ testthat\nā”‚ ā”‚ ā”œā”€ā”€ _snaps\nā”‚ ā”‚ ā”‚ ā””ā”€ā”€ shinytest2\nā”‚ ā”‚ ā”‚ ā””ā”€ā”€ 001.json\nā”‚ ā”‚ ā””ā”€ā”€ test-shinytest2.R\nā”‚ ā””ā”€ā”€ testthat.R\nā””ā”€ā”€ tests\n ā”œā”€ā”€ testthat\n ā”‚ ā””ā”€ā”€ test-inst-apps.R\n ā””ā”€ā”€ testthat.R\n```\n\n:::\n\n::: {.column width='50%'}\n\n**Returned by a function**\n\n```\nā”œā”€ā”€ DESCRIPTION\nā”œā”€ā”€ R\nā”‚ ā””ā”€ā”€ app-function1.R\nā”‚ ā””ā”€ā”€ app-function2.R\nā””ā”€ā”€ tests\n ā”œā”€ā”€ testthat\n ā”‚ ā”œā”€ā”€ _snaps\n ā”‚ ā”‚ ā””ā”€ā”€ app-function1\n ā”‚ ā”‚ ā””ā”€ā”€ 001.json\n ā”‚ ā”‚ ā””ā”€ā”€ app-function2\n ā”‚ ā”‚ ā””ā”€ā”€ 001.json\n ā”‚ ā””ā”€ā”€ test-app-function1.R\n ā”‚ ā””ā”€ā”€ test-app-function2.R\n ā””ā”€ā”€ testthat.R\n```\n\n:::\n\n::::\n\n## Advanced topics {.smaller}\n\nThe following are some advanced topics that are beyond the scope of the current presentation, but you may wish to know more about.\n\n:::{.callout-tip}\n\n## Extra\n\n- If you want to test Shiny apps with continuous integration using `{shinytest2}`, read [this](https://rstudio.github.io/shinytest2/articles/use-ci.html){target=\"_blank\"} article.\n\n- `{shinytest2}` is a successor to `{shinytest}` package. If you want to migrate from the latter to the former, have a look at [this](https://rstudio.github.io/shinytest2/articles/z-migration.html){target=\"_blank\"}.\n\n:::\n\n\n\n## Further reading {.smaller}\n\n- [Testing](https://mastering-shiny.org/scaling-testing.html){target=\"_blank\"} chapter from *Mastering Shiny* book\n\n- `{shinytest2}` article introducing its [workflow](https://rstudio.github.io/shinytest2/articles/shinytest2.html){target=\"_blank\"}\n\n- `{shinytest2}` article on how to test apps in [R packages](https://rstudio.github.io/shinytest2/articles/use-package.html){target=\"_blank\"}\n\n# Headaches \n\nIt's not all kittens and roses when it comes to snapshot testing. \n\nLet's see some issues you might run into while using them. šŸ¤•\n\n## Testing behavior that you don't own {.smaller}\n\nLet's say you write a graphical snapshot test for a function that produces a `ggplot` object. If `{ggplot2}` authors make some modifications to this object, your tests will fail, even though your function works as expected!\n\nIn other words, *your* tests are now at the mercy of *other package authors* because snapshots are capturing things beyond your package's control.\n\n:::{.callout-caution}\n\nTests that fail for reasons other than what they are testing for are problematic. Thus, be careful about *what* you snapshot and keep in mind the maintenance burden that comes with dependencies with volatile APIs.\n\n:::\n\n:::{.callout-note}\n\n*A* way to reduce the burden of keeping snapshots up-to-date is to [automate this process](https://github.com/krlmlr/actions-sync/tree/base/update-snapshots){target=\"_blank\"}. But there is no free lunch in this universe, and now you need to maintain this automation! šŸ¤·\n\n:::\n\n## Failures in non-interactive environments {.smaller}\n\nIf snapshots fail locally, you can just run `snapshot_review()`, but what if they fail in non-interactive environments (on CI/CD platforms, during `R CMD Check`, etc.)?\n\nThe easiest solution is to copy the new snapshots to the local folder and run `snapshot_review()`.\n\n:::{.callout-tip}\n\nIf expected snapshot is called (e.g.) `foo.svg`, there will be a new snapshot file `foo.new.svg` in the same folder when the test fails.\n\n`snapshot_review()` compares these files to reveal how the outputs have changed.\n\n:::\n\nBut where can you find the new snapshots?\n\n## Accessing new snapshots {.smaller}\n\nIn local `R CMD Check`, you can find new snapshots in `.Rcheck` folder:\n\n```r\npackage_name.Rcheck/tests/testthat/_snaps/\n```\n\nOn CI/CD platforms, you can find snapshots in artifacts folder:\n\n- [AppVeyor](https://www.appveyor.com/docs/packaging-artifacts/){target=\"_blank\"}\n\n\n\n::: {.cell}\n::: {.cell-output-display}\n![](media/AppVeyor.png){width=484}\n:::\n:::\n\n\n\n- [GitHub actions](https://github.com/r-lib/actions/tree/v2-branch/check-r-package){target=\"_blank\"}\n\n```yaml\n - uses: r-lib/actions/check-r-package@v2\n with:\n upload-snapshots: true\n```\n\n## Code review with snapshot tests {.smaller}\n\nDespite snapshot tests making the expected outputs more human-readable, given a big enough change and complex enough output, sometimes it can be challenging to review changes to snapshots. \n\nHow do you review pull requests with complex snapshots changes? \n\n## {.smaller}\n\n:::panel-tabset\n\n### Option-1\n\nUse tools provided for code review by hosting platforms (like GitHub).\nFor example, to review changes in SVG snapshots:\n\n\n\n::: {.cell}\n::: {.cell-output-display}\n![](media/github_pr.mov){width=70%}\n:::\n:::\n\n\n\n### Option-2\n\nLocally open the PR branch and play around with the changes to see if the new behaviour makes sense. šŸ¤· \n\n:::\n\n## Danger of silent failures {.smaller}\n\nGiven their fragile nature, snapshot tests are skipped on CRAN by default. \n\nAlthough this makes sense, it means that you miss out on anything but a breaking change from upstream dependency. E.g., if `{ggplot2}` (hypothetically) changes how the points look, you won't know about this change until you *happen to* run your snapshot tests again locally or on CI/CD.\n\nUnit tests run on CRAN, on the other hand, will fail and you will be immediately informed about it.\n\n:::{.callout-tip collapse=false appearance='default' icon=true}\n\nA way to insure against such silent failures is to run tests **daily** on CI/CD platforms (e.g. AppVeyor nightly builds).\n\n:::\n\n# Parting wisdom\n\nWhat *not* to do\n\n## Don't use snapshot tests for *everything* {.smaller}\n\nIt is tempting to use them everywhere out of laziness. But they are sometimes inappropriate (e.g. when testing requires external benchmarking).\n\nLet's say you write a function to extract estimates from a regression model.\n\n```{.r}\nextract_est <- function(m) m$coefficients\n```\n\nIts test should compare results against an external benchmark, and not a snapshot.\n\n:::: {.columns}\n\n::: {.column width='50%'}\n\n**Good**\n\n```{.r}\ntest_that(\"`extract_est()` works\", {\n m <- lm(wt ~ mpg, mtcars)\n expect_equal(\n extract_est(m)[[1]],\n m$coefficients[[1]]\n )\n})\n```\n\n\n:::\n\n::: {.column width='50%'}\n\n**Bad**\n\n```{.r}\ntest_that(\"`extract_est()` works\", {\n m <- lm(wt ~ mpg, mtcars)\n expect_snapshot(extract_est(m))\n})\n```\n\n:::\n\n::::\n\n## Snapshot for humans, not machines {.smaller}\n\nSnapshot testing is appropriate when the human needs to be in the loop to make sure that things are working as expected. Therefore, the snapshots should be human readable.\n\nE.g. if you write a function that plots something:\n\n```{.r}\npoint_plotter <- function() ggplot(mtcars, aes(wt, mpg)) + geom_point()\n```\n\nTo test it, you should snapshot the *plot*, and not the underlying *data*, which is hard to make sense of for a human.\n\n:::: {.columns}\n\n::: {.column width='50%'}\n\n**Good**\n\n```{.r}\ntest_that(\"`point_plotter()` works\", {\n expect_doppelganger(\n \"point-plotter-mtcars\",\n point_plotter()\n )\n})\n```\n\n:::\n\n::: {.column width='50%'}\n\n**Bad**\n\n```{.r}\ntest_that(\"`point_plotter()` works\", {\n p <- point_plotter()\n pb <- ggplot_build(p)\n expect_snapshot(pb$data)\n})\n```\n\n:::\n\n::::\n\n## Don't **blindly accept** snapshot changes {.smaller}\n\nResist formation of such a habit.\n\n`{testthat}` provides tools to make it very easy to review changes, so no excuses! \n\n\n# Self-study \n\nIn this presentation, you deliberately kept the examples and the tests simple. \n\nTo see a more realistic usage of snapshot tests, you can study open-source test suites.\n\n## Suggested repositories {.smaller}\n\n::: columns\n\n\n::: {.column width=\"33%\"}\n### Print outputs\n\n- [`{cli}`](https://github.com/r-lib/cli/tree/main/tests/testthat){target=\"_blank\"} (for testing command line interfaces)\n\n- [`{pkgdown}`](https://github.com/r-lib/pkgdown/tree/main/tests/testthat){target=\"_blank\"} (for testing generated HTML documents)\n\n- [`{dbplyr}`](https://github.com/tidyverse/dbplyr/tree/main/tests/testthat){target=\"_blank\"} (for testing printing of generated SQL queries)\n\n- [`{gt}`](https://github.com/rstudio/gt/tree/master/tests/testthat){target=\"_blank\"} (for testing table printing)\n:::\n\n\n::: {.column width=\"33%\"}\n\n### Visualizations\n\n- [`{ggplot2}`](https://github.com/tidyverse/ggplot2/tree/main/tests/testthat){target=\"_blank\"}\n\n- [`{ggstatsplot}`](https://github.com/IndrajeetPatil/ggstatsplot/tree/main/tests/testthat){target=\"_blank\"}\n\n:::\n\n\n::: {.column width=\"33%\"}\n\n### Shiny apps \n\n- [`{shinytest2}`](https://github.com/rstudio/shinytest2/tree/main/tests/testthat){target=\"_blank\"}\n\n- [`{designer}`](https://github.com/ashbaldry/designer/tree/dev/tests/testthat){target=\"_blank\"}\n\n:::\n\n:::\n\n# Activities\n\nIf you feel confident enough to contribute to open-source projects to practice these skills, here are some options.\n\n## Practice makes it perfect {.smaller}\n\nThese are only suggestions. Feel free to contribute to any project you like! šŸ¤\n\n:::{.callout-tip}\n\n## Suggestions \n\n- See if `{ggplot2}` [extensions](https://exts.ggplot2.tidyverse.org/gallery/){target=\"_blank\"} you like use snapshot tests for graphics. If not, you can add them for key functions.\n\n- Check out hard reverse dependencies of [`{shiny}`](https://cran.r-project.org/web/packages/shiny/index.html){target=\"_blank\"}, and add snapshot tests using `{shinytest2}` to an app of your liking.\n\n- Add more `{vdiffr}` snapshot tests to plotting functions in [`{see}`](https://github.com/easystats/see), a library for statistical visualizations (I can chaperone your PRs here).\n\n- `{shinytest2}` is the successor to `{shinytest}` package. Check out which packages currently use it for [testing](https://cran.r-project.org/web/packages/shinytest/index.html){target=\"_blank\"} Shiny apps, and see if you can use `{shinytest2}` instead (see how-to [here](https://rstudio.github.io/shinytest2/articles/z-migration.html){target=\"_blank\"}).\n\n:::\n\n## General reading {.smaller}\n\nAlthough current presentation is focused only on snapshot testing, here is reading material on automated testing in general.\n\n- McConnell, S. (2004). *Code Complete*. Microsoft Press. (**pp. 499-533**)\n\n- Boswell, D., & Foucher, T. (2011). *The Art of Readable Code*. O'Reilly Media, Inc. (**pp. 149-162**)\n\n- Riccomini, C., & Ryaboy D. (2021). *The Missing Readme*. No Starch Press. (**pp. 89-108**)\n\n- Martin, R. C. (2009). *Clean Code*. Pearson Education. (**pp. 121-133**)\n\n- Fowler, M. (2018). *Refactoring.* Addison-Wesley Professional. (**pp. 85-100**)\n\n- Beck, K. (2003). *Test-Driven Development*. Addison-Wesley Professional.\n\n## Additional resources \n\nFor a comprehensive collection of packages for unit testing in R, see [this](https://indrajeetpatil.github.io/awesome-r-pkgtools/#unit-testing){target=\"_blank\"} page.\n\n# For more\n\nIf you are interested in good programming and software development practices, check out my other [slide decks](https://sites.google.com/site/indrajeetspatilmorality/presentations){target=\"_blank\"}.\n\n# Find me at...\n\n{{< fa brands twitter >}} [Twitter](http://twitter.com/patilindrajeets){target=\"_blank\"}\n\n{{< fa brands linkedin >}} [LikedIn](https://www.linkedin.com/in/indrajeet-patil-397865174/){target=\"_blank\"}\n\n{{< fa brands github >}} [GitHub](http://github.com/IndrajeetPatil){target=\"_blank\"}\n\n{{< fa solid link >}} [Website](https://sites.google.com/site/indrajeetspatilmorality/){target=\"_blank\"}\n\n{{< fa solid envelope >}} [E-mail](mailto:patilindrajeet.science@gmail.com){target=\"_blank\"}\n\n# Thank You \n\nAnd Happy Snapshotting! šŸ˜Š\n\n## Session information {.smaller}\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\nquarto::quarto_version()\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n[1] '1.5.26'\n```\n\n\n:::\n\n```{.r .cell-code}\nsessioninfo::session_info(include_base = TRUE)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nā”€ Session info ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\n setting value\n version R version 4.3.3 (2024-02-29)\n os Ubuntu 22.04.4 LTS\n system x86_64, linux-gnu\n ui X11\n language (EN)\n collate C.UTF-8\n ctype C.UTF-8\n tz UTC\n date 2024-03-24\n pandoc 3.1.11 @ /opt/hostedtoolcache/pandoc/3.1.11/x64/ (via rmarkdown)\n\nā”€ Packages ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\n package * version date (UTC) lib source\n base * 4.3.3 2024-03-04 [3] local\n brio 1.1.4 2023-12-10 [1] RSPM\n cli 3.6.2 2023-12-11 [1] RSPM\n colorspace 2.1-0 2023-01-23 [1] RSPM\n compiler 4.3.3 2024-03-04 [3] local\n crayon 1.5.2 2022-09-29 [1] RSPM\n datasets * 4.3.3 2024-03-04 [3] local\n desc 1.4.3 2023-12-10 [1] RSPM\n diffobj 0.3.5 2021-10-05 [1] RSPM\n digest 0.6.35 2024-03-11 [1] RSPM\n evaluate 0.23 2023-11-01 [1] RSPM\n fansi 1.0.6 2023-12-08 [1] RSPM\n farver 2.1.1 2022-07-06 [1] RSPM\n fastmap 1.1.1 2023-02-24 [1] RSPM\n ggplot2 * 3.5.0 2024-02-23 [1] RSPM\n glue 1.7.0 2024-01-09 [1] RSPM\n graphics * 4.3.3 2024-03-04 [3] local\n grDevices * 4.3.3 2024-03-04 [3] local\n grid 4.3.3 2024-03-04 [3] local\n gtable 0.3.4 2023-08-21 [1] RSPM\n htmltools 0.5.7 2023-11-03 [1] RSPM\n jsonlite 1.8.8 2023-12-04 [1] RSPM\n knitr 1.45 2023-10-30 [1] RSPM\n labeling 0.4.3 2023-08-29 [1] RSPM\n later 1.3.2 2023-12-06 [1] RSPM\n lattice 0.22-5 2023-10-24 [3] CRAN (R 4.3.3)\n lifecycle 1.0.4 2023-11-07 [1] RSPM\n magrittr 2.0.3 2022-03-30 [1] RSPM\n Matrix 1.6-5 2024-01-11 [3] CRAN (R 4.3.3)\n methods * 4.3.3 2024-03-04 [3] local\n mgcv 1.9-1 2023-12-21 [3] CRAN (R 4.3.3)\n munsell 0.5.0 2018-06-12 [1] RSPM\n nlme 3.1-164 2023-11-27 [3] CRAN (R 4.3.3)\n pillar 1.9.0 2023-03-22 [1] RSPM\n pkgconfig 2.0.3 2019-09-22 [1] RSPM\n pkgload 1.3.4 2024-01-16 [1] RSPM\n png 0.1-8 2022-11-29 [1] RSPM\n processx 3.8.4 2024-03-16 [1] RSPM\n ps 1.7.6 2024-01-18 [1] RSPM\n quarto 1.4.1 2024-03-10 [1] Github (quarto-dev/quarto-r@ba8485a)\n R6 2.5.1 2021-08-19 [1] RSPM\n Rcpp 1.0.12 2024-01-09 [1] RSPM\n rematch2 2.1.2 2020-05-01 [1] RSPM\n rlang 1.1.3 2024-01-10 [1] RSPM\n rmarkdown 2.26 2024-03-05 [1] RSPM\n rprojroot 2.0.4 2023-11-05 [1] RSPM\n rstudioapi 0.15.0 2023-07-07 [1] RSPM\n scales 1.3.0 2023-11-28 [1] RSPM\n sessioninfo 1.2.2 2021-12-06 [1] any (@1.2.2)\n splines 4.3.3 2024-03-04 [3] local\n stats * 4.3.3 2024-03-04 [3] local\n testthat * 3.2.1 2023-12-02 [1] RSPM\n tibble 3.2.1 2023-03-20 [1] RSPM\n tools 4.3.3 2024-03-04 [3] local\n utf8 1.2.4 2023-10-22 [1] RSPM\n utils * 4.3.3 2024-03-04 [3] local\n vctrs 0.6.5 2023-12-01 [1] RSPM\n vdiffr * 1.0.7 2023-09-22 [1] RSPM\n waldo 0.5.2 2023-11-02 [1] RSPM\n withr 3.0.0 2024-01-16 [1] RSPM\n xfun 0.42 2024-02-08 [1] RSPM\n yaml 2.3.8 2023-12-11 [1] RSPM\n\n [1] /home/runner/work/_temp/Library\n [2] /opt/R/4.3.3/lib/R/site-library\n [3] /opt/R/4.3.3/lib/R/library\n\nā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\n```\n\n\n:::\n:::\n", + "markdown": "---\n#title: \"Introduction to Snapshot Testing in R\"\nformat:\n revealjs: \n css: style.css\n theme: simple\n slide-number: true\n preview-links: auto\n code-link: true\n footer: \"Source code for these slides can be found [on GitHub](https://github.com/IndrajeetPatil/intro-to-snapshot-testing/){target='_blank'}.\"\n#author: \"Indrajeet Patil\"\n#affiliation: \nexecute:\n echo: true\n---\n\n\n\n## Introduction to Snapshot Testing in R {style=\"margin-top: 1em;\"}\n\n\n\n::: {style=\"margin-top: 0.5em; margin-bottom: 0.5em; font-size: 1em\"}\n\nIndrajeet Patil\n\n:::\n\n\n::: {style=\"margin-top: 1em; font-size:0.75em\"}\n\n![](media/logos_combined.jpeg){.absolute width=\"750\" height=\"300\"}\n\n:::\n\n## Unit testing {.smaller}\n\nThe goal of a unit test is to capture the *expected* output of a function using *code* and making sure that *actual* output after any changes matches the expected output.\n\n[`{testthat}`](https://testthat.r-lib.org/) is a popular framework for writing unit tests in R. \n\n:::: {.columns}\n\n::: {.column width='60%'}\n\n:::{.callout-important}\n\n## Benefits of unit testing\n\n- insures against unintentionally changing function behaviour\n- prevents re-introducing already fixed bugs\n- acts as the most basic form of developer-focused documentation\n- catches breaking changes coming from upstream dependencies\n- etc.\n\n:::\n\n:::\n\n::: {.column width='40%'}\n\n:::{.callout-tip}\n\n## Test output\n\nTest pass only when actual function behaviour matches expected.\n\n| actual | expected | tests |\n| ------- | ------- | ----- |\n| {{< fa regular file-lines size=2xl >}} | {{< fa regular file-lines size=2xl >}} | {{< fa regular circle-check size=2xl >}} |\n| {{< fa regular file size=2xl >}} | {{< fa regular file-lines size=2xl >}} | {{< fa regular circle-xmark size=2xl >}} |\n\n:::\n\n:::\n\n::::\n\n## Unit testing with `{testthat}`: A recap {.smaller}\n\n:::: {.columns}\n\n::: {.column width='50%'}\n\n::: {.callout-important}\n\n## Test organization\n\nTesting infrastructure for R package has the following hierarchy:\n\n| Component | Role |\n| :------------------------------------------------------- | :---------------------------------------------------------------------------------------------- |\n|
{{< fa regular file-code size=2xl >}} **Test file** | Tests for code situated in `R/foo.R` will typically be in the file `tests/testthat/test-foo.R`. |\n| {{< fa solid flask size=2xl >}} **Tests** | A single file can contain multiple tests.
|\n|
{{< fa solid equals size=2xl >}} **Expectations** | A single test can have multiple expectations that formalize what you expect code to do. |\n\n:::\n\n:::\n\n::: {.column width='50%'}\n\n:::{.callout-tip}\n\n## Example test file\n\n- Every test is a call to `testthat::test_that()` function.\n\n- Every expectation is represented by `testthat::expect_*()` function.\n\n- You can generate a test file using `usethis::use_test()` function.\n\n```{.r}\n# File: tests/testthat/test-op.R\n\n# test-1\ntest_that(\"multiplication works\", {\n expect_equal(2 * 2, 4) # expectation-1\n expect_equal(-2 * 2, -4) # expectation-2\n})\n\n# test-2\ntest_that(\"addition works\", {\n expect_equal(2 + 2, 4) # expectation-1\n expect_equal(-2 + 2, 0) # expectation-2\n})\n\n...\n```\n\n:::\n\n:::\n\n::::\n\n\n## What is different about snapshot testing?\n\n. . .\n\nA **unit test** records the code to describe expected output.\n\n
\n\n\n\n(actual) {{< fa regular file-code size=2xl >}} {{< fa solid arrows-left-right size=2xl >}} {{< fa solid file-code size=2xl >}} (expected)\n\n
\n\n. . .\n\nA **snapshot test** records expected output in a separate, human-readable file.\n\n
\n\n(actual) {{< fa regular file-code size=2xl >}} {{< fa solid arrows-left-right size=2xl >}} {{< fa solid file-lines size=2xl >}} (expected)\n\n## Why do you need snapshot testing?\n\nIf you develop R packages and have struggled to \n\n::: incremental\n\n- test that text output *prints* as expected\n- test that an entire file *is* as expected\n- test that generated graphical output *looks* as expected\n- update such tests *en masse*\n\n::: \n\n. . .\n\nthen you should be excited to know more about *snapshot tests* (aka *golden tests*)! šŸ¤©\n\n# Prerequisites\n\nFamiliarity with writing unit tests using [`{testthat}`](https://testthat.r-lib.org/index.html){target=\"_blank\"}.\n\nIf not, have a look at [this](https://r-pkgs.org/testing-basics.html){target=\"_blank\"} chapter from *R Packages* book.\n\n## \n\n
\n\n:::{.callout-important}\n\n## *Nota bene*\n\nIn the following slides, in all snapshot tests, I include the following line of code:\n\n```r\nlocal_edition(3)\n```\n\n**You don't need to do this in your package tests!**\n\nIt's sufficient to update the `DESCRIPTION` file to use `{testthat}` 3rd edition:\n\n```r\nConfig/testthat/edition: 3\n```\n\nFor more, see [this](https://testthat.r-lib.org/articles/third-edition.html){target=\"_blank\"} article.\n\n:::\n\n# Testing text outputs\n\nSnapshot tests can be used to test that text output *prints* as expected.\n\nImportant for testing functions that pretty-print R objects to the console, create elegant and informative exceptions, etc.\n\n## Example function {.smaller}\n\nLet's say you want to write a unit test for the following function:\n\n:::: {.columns}\n\n::: {.column width='60%'}\n\n**Source code**\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\nprint_movies <- function(keys, values) {\n paste0(\n \"Movie: \\n\",\n paste0(\" \", keys, \": \", values, collapse = \"\\n\")\n )\n}\n```\n:::\n\n\n\n:::\n\n::: {.column width='40%'}\n\n**Output**\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\ncat(print_movies(\n c(\"Title\", \"Director\"),\n c(\"Salaam Bombay!\", \"Mira Nair\")\n))\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nMovie: \n Title: Salaam Bombay!\n Director: Mira Nair\n```\n\n\n:::\n:::\n\n\n\n:::\n\n::::\n\n. . .\n\n
\n\nNote that you want to test that the printed output *looks* as expected. \n\nTherefore, you need to check for all the little bells and whistles in the printed output.\n\n## Example test {.smaller}\n\nEven testing this simple function is a bit painful because you need to keep track of every escape character, every space, etc. \n\n\n\n::: {.cell}\n\n:::\n\n::: {.cell}\n\n```{.r .cell-code}\ntest_that(\"`print_movies()` prints as expected\", {\n expect_equal(\n print_movies(\n c(\"Title\", \"Director\"),\n c(\"Salaam Bombay!\", \"Mira Nair\")\n ),\n \"Movie: \\n Title: Salaam Bombay!\\n Director: Mira Nair\"\n )\n})\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nTest passed šŸŽŠ\n```\n\n\n:::\n:::\n\n\n\n. . .\n\nWith a more complex code, it'd be impossible for a human to reason about what the output is supposed to look like.\n\n. . .\n\n:::{.callout-important}\n\nIf this is a utility function used by many other functions, changing its behaviour would entail *manually* changing expected outputs for many tests.\n\nThis is not maintainable! šŸ˜©\n\n:::\n\n## Alternative: Snapshot test {.smaller}\n\nInstead, you can use `expect_snapshot()`, which, when run for the first time, generates a Markdown file with expected/reference output.\n\n\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\ntest_that(\"`print_movies()` prints as expected\", {\n local_edition(3)\n expect_snapshot(cat(print_movies(\n c(\"Title\", \"Director\"),\n c(\"Salaam Bombay!\", \"Mira Nair\")\n )))\n})\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nā”€ā”€ Warning: `print_movies()` prints as expected ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\nAdding new snapshot:\nCode\n cat(print_movies(c(\"Title\", \"Director\"), c(\"Salaam Bombay!\", \"Mira Nair\")))\nOutput\n Movie: \n Title: Salaam Bombay!\n Director: Mira Nair\n```\n\n\n:::\n:::\n\n\n\n\n\n. . .\n\n:::{.callout-warning}\n\nThe first time a snapshot is created, it becomes *the truth* against which future function behaviour will be compared. \n\nThus, it is **crucial** that you carefully check that the output is indeed as expected. šŸ”Ž \n\n:::\n\n## Human-readable Markdown file {.smaller}\n\nCompared to your unit test code representing the expected output\n\n```r\n\"Movie: \\n Title: Salaam Bombay!\\n Director: Mira Nair\"\n```\n\nnotice how much more human-friendly the Markdown output is!\n\n```md\nCode\n cat(print_movies(c(\"Title\", \"Director\"), c(\"Salaam Bombay!\", \"Mira Nair\")))\nOutput\n Movie: \n Title: Salaam Bombay!\n Director: Mira Nair\n```\n\nIt is easy to *see* what the printed text output is *supposed* to look like. In other words, snapshot tests are useful when the *intent* of the code can only be verified by a human.\n\n. . . \n\n:::{.callout-note}\n\n## More about snapshot Markdown files\n\n- If test file is called `test-foo.R`, the snapshot will be saved to `test/testthat/_snaps/foo.md`.\n\n- If there are multiple snapshot tests in a single file, corresponding snapshots will also share the same `.md` file.\n\n- By default, `expect_snapshot()` will capture the code, the object values, and any side-effects.\n\n:::\n\n## What test success looks like {.smaller}\n\nIf you run the test again, it'll succeed:\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\ntest_that(\"`print_movies()` prints as expected\", {\n local_edition(3)\n expect_snapshot(cat(print_movies(\n c(\"Title\", \"Director\"),\n c(\"Salaam Bombay!\", \"Mira Nair\")\n )))\n})\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nTest passed šŸ˜ø\n```\n\n\n:::\n:::\n\n\n\n\n\n. . .\n\n
\n\n:::{.callout-note}\n\n### Why does my test fail on a re-run?\n\nIf testing a snapshot you just generated fails on re-running the test, this is most likely because your test is not deterministic. For example, if your function deals with random number generation.\n\nIn such cases, setting a seed (e.g. `set.seed(42)`) should help.\n\n:::\n\n## What test failure looks like {.smaller}\n\nWhen function changes, snapshot doesn't match the reference, and the test fails:\n\n:::: {.columns}\n\n::: {.column width='35%'}\n\n**Changes to function**\n\n```{.r code-line-numbers=\"5\"}\nprint_movies <- function(keys, values) {\n paste0(\n \"Movie: \\n\",\n paste0(\n \" \", keys, \"- \", values,\n collapse = \"\\n\"\n )\n )\n}\n```\n\n\n\n::: {.cell}\n\n:::\n\n\n\n
\n\nFailure message provides expected (`-`) vs observed (`+`) diff.\n\n:::\n\n::: {.column width='65%'}\n\n**Test failure**\n\n```{.r}\ntest_that(\"`print_movies()` prints as expected\", {\n expect_snapshot(cat(print_movies(\n c(\"Title\", \"Director\"),\n c(\"Salaam Bombay!\", \"Mira Nair\")\n )))\n})\n```\n\n\n\n::: {.cell}\n::: {.cell-output .cell-output-stdout}\n\n```\nā”€ā”€ Failure: `print_movies()` prints as expected ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\nSnapshot of code has changed:\nold[2:6] vs new[2:6]\n cat(print_movies(c(\"Title\", \"Director\"), c(\"Salaam Bombay!\", \"Mira Nair\")))\n Output\n Movie: \n- Title: Salaam Bombay!\n+ Title- Salaam Bombay!\n- Director: Mira Nair\n+ Director- Mira Nair\n\n* Run `testthat::snapshot_accept('slides.qmd')` to accept the change.\n* Run `testthat::snapshot_review('slides.qmd')` to interactively review the change.\n```\n\n\n:::\n\n::: {.cell-output .cell-output-error}\n\n```\nError:\n! Test failed\n```\n\n\n:::\n:::\n\n\n\n:::\n\n::::\n\n\n## Fixing tests {.smaller}\n\nMessage accompanying failed tests make it explicit how to fix them.\n\n. . . \n\n- If the change was *deliberate*, you can accept the new snapshot as the current *truth*.\n\n```r\n* Run `snapshot_accept('foo.md')` to accept the change\n```\n\n- If this was *unexpected*, you can review the changes, and decide whether to change the snapshot or to correct the function behaviour instead.\n\n```r\n* Run `snapshot_review('foo.md')` to interactively review the change\n```\n\n. . . \n\n
\n\n:::{.callout-tip}\n\n## Fixing multiple snapshot tests\n\nIf this is a utility function used by many other functions, changing its behaviour would lead to failure of many tests. \n\nYou can update *all* new snapshots with `snapshot_accept()`. And, of course, check the diffs to make sure that the changes are expected.\n\n:::\n\n## Capturing messages and warnings {.smaller}\n\nSo far you have tested text output printed to the console, but you can also use snapshots to capture messages, warnings, and errors.\n\n:::: {.columns}\n\n::: {.column width='50%'}\n\n**message**\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\nf <- function() message(\"Some info for you.\")\n\ntest_that(\"f() messages\", {\n local_edition(3)\n expect_snapshot(f())\n})\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nā”€ā”€ Warning: f() messages ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\nAdding new snapshot:\nCode\n f()\nMessage\n Some info for you.\n```\n\n\n:::\n:::\n\n\n\n:::\n\n::: {.column width='50%'}\n\n**warning**\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\ng <- function() warning(\"Managed to recover.\")\n\ntest_that(\"g() warns\", {\n local_edition(3)\n expect_snapshot(g())\n})\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nā”€ā”€ Warning: g() warns ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\nAdding new snapshot:\nCode\n g()\nCondition\n Warning in `g()`:\n Managed to recover.\n```\n\n\n:::\n:::\n\n\n\n:::\n\n::::\n\n:::{.callout-tip}\n\nSnapshot records both the *condition* and the corresponding *message*.\n\nYou can now rest assured that the users are getting informed the way you want! šŸ˜Œ\n\n:::\n\n## Capturing errors {.smaller}\n\nIn case of an error, the function `expect_snapshot()` itself will produce an error. \nYou have two ways around this:\n\n:::: {.columns}\n\n::: {.column width='50%'}\n\n**Option-1** (recommended)\n\n```{.r code-line-numbers=\"3\"}\ntest_that(\"`log()` errors\", {\n local_edition(3)\n expect_snapshot(log(\"x\"), error = TRUE)\n})\n```\n\n\n\n::: {.cell}\n::: {.cell-output .cell-output-stdout}\n\n```\nā”€ā”€ Warning: `log()` errors ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\nAdding new snapshot:\nCode\n log(\"x\")\nCondition\n Error in `log()`:\n ! non-numeric argument to mathematical function\n```\n\n\n:::\n:::\n\n\n\n:::\n\n::: {.column width='50%'}\n\n**Option-2**\n\n```{.r code-line-numbers=\"3\"}\ntest_that(\"`log()` errors\", {\n local_edition(3)\n expect_snapshot_error(log(\"x\"))\n})\n```\n\n\n\n::: {.cell}\n::: {.cell-output .cell-output-stdout}\n\n```\nā”€ā”€ Warning: `log()` errors ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\nAdding new snapshot:\nnon-numeric argument to mathematical function\n```\n\n\n:::\n:::\n\n\n\n:::\n\n::::\n\n:::{.callout-tip}\n\n### Which option should I use?\n\n- If you want to capture both the code and the error message, use `expect_snapshot(..., error = TRUE)`.\n\n- If you want to capture only the error message, use `expect_snapshot_error()`.\n\n:::\n\n## Further reading {.smaller}\n\n- `{testthat}` article on [snapshot testing](https://testthat.r-lib.org/articles/snapshotting.html){target=\"_blank\"}\n\n- Introduction to [golden testing](https://ro-che.info/articles/2017-12-04-golden-tests){target=\"_blank\"}\n\n- Docs for [Jest](https://jestjs.io/docs/snapshot-testing){target=\"_blank\"} library in JavaScript, which inspired snapshot testing implementation in `{testthat}`\n\n\n# Testing graphical outputs\n\nTo create graphical expectations, you will use `{testthat}` extension package: [`{vdiffr}`](https://vdiffr.r-lib.org/){target=\"_blank\"}.\n\n## How does `{vdiffr}` work? {.smaller}\n\n`{vdiffr}` introduces `expect_doppelganger()` to generate `{testthat}` expectations for graphics. It does this by writing SVG snapshot files for outputs!\n\n. . . \n\nThe figure to test can be:\n\n- a `ggplot` object (from `ggplot2::ggplot()`)\n- a `recordedplot` object (from `grDevices::recordPlot()`)\n- any object with a `print()` method\n\n. . . \n\n:::{.callout-note}\n\n- If test file is called `test-foo.R`, the snapshot will be saved to `test/testthat/_snaps/foo` folder.\n\n- In this folder, there will be one `.svg` file for every test in `test-foo.R`.\n\n- The name for the `.svg` file will be sanitized version of `title` argument to `expect_doppelganger()`.\n\n:::\n\n## Example function {.smaller}\n\nLet's say you want to write a unit test for the following function:\n\n:::: {.columns}\n\n::: {.column width='50%'}\n\n**Source code**\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\nlibrary(ggplot2)\n\ncreate_scatter <- function() {\n ggplot(mtcars, aes(wt, mpg)) +\n geom_point(size = 3, alpha = 0.75) +\n geom_smooth(method = \"lm\")\n}\n```\n:::\n\n\n:::\n\n::: {.column width='50%'}\n\n**Output**\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\ncreate_scatter()\n```\n\n::: {.cell-output-display}\n![](slides_files/figure-revealjs/unnamed-chunk-17-1.png){width=100%}\n:::\n:::\n\n\n\n:::\n\n::::\n\n. . .\n\nNote that you want to test that the graphical output *looks* as expected, and this expectation is difficult to capture with a unit test.\n\n## Graphical snapshot test {.smaller}\n\nYou can use `expect_doppelganger()` from `{vdiffr}` to test this!\n\n. . .\n\nThe *first time* you run the test, it'd generate an `.svg` file with expected output.\n\n\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\ntest_that(\"`create_scatter()` plots as expected\", {\n local_edition(3)\n expect_doppelganger(\n title = \"create scatter\",\n fig = create_scatter(),\n )\n})\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nā”€ā”€ Warning: `create_scatter()` plots as expected ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\nAdding new file snapshot: 'tests/testthat/_snaps/create-scatter.svg'\n```\n\n\n:::\n:::\n\n\n\n\n\n. . .\n\n:::{.callout-warning}\n\nThe first time a snapshot is created, it becomes *the truth* against which future function behaviour will be compared. \n\nThus, it is **crucial** that you carefully check that the output is indeed as expected. šŸ”Ž \n\nYou can open `.svg` snapshot files in a web browser for closer inspection.\n\n:::\n\n## What test success looks like {.smaller}\n\nIf you run the test again, it'll succeed:\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\ntest_that(\"`create_scatter()` plots as expected\", {\n local_edition(3)\n expect_doppelganger(\n title = \"create scatter\",\n fig = create_scatter(),\n )\n})\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nTest passed šŸŽ‰\n```\n\n\n:::\n:::\n\n\n\n\n\n## What test failure looks like {.smaller}\n\nWhen function changes, snapshot doesn't match the reference, and the test fails:\n\n:::: {.columns}\n\n::: {.column width='40%'}\n\n**Changes to function**\n\n```{.r code-line-numbers=\"3\"}\ncreate_scatter <- function() {\n ggplot(mtcars, aes(wt, mpg)) +\n geom_point(size = 2, alpha = 0.85) +\n geom_smooth(method = \"lm\")\n}\n```\n\n\n\n::: {.cell}\n\n:::\n\n\n\n:::\n\n::: {.column width='60%'}\n\n**Test failure**\n\n\n\n\n\n```{.r}\ntest_that(\"`create_scatter()` plots as expected\", {\n local_edition(3)\n expect_doppelganger(\n title = \"create scatter\",\n fig = create_scatter(),\n )\n})\n\nā”€ā”€ Failure (':3'): `create_scatter()` plots as expected ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\nSnapshot of `testcase` to 'slides.qmd/create-scatter.svg' has changed\nRun `testthat::snapshot_review('slides.qmd/')` to review changes\nBacktrace:\n 1. vdiffr::expect_doppelganger(...)\n 3. testthat::expect_snapshot_file(...)\nError in `reporter$stop_if_needed()`:\n! Test failed\n```\n\n:::\n\n::::\n\n## Fixing tests {.smaller}\n\nRunning `snapshot_review()` launches a Shiny app which can be used to either accept or reject the new output(s).\n\n\n\n::: {.cell}\n::: {.cell-output-display}\n![](media/shiny_app_graphics.mov){width=60%}\n:::\n:::\n\n\n\n##\n\n:::{.callout-tip}\n\n## Why are my snapshots for plots failing?! šŸ˜”\n\nIf tests fail even if the function didn't change, it can be due to any of the following reasons:\n\n- R's graphics engine changed\n- `{ggplot2}` itself changed\n- non-deterministic behaviour\n- changes in system libraries\n\nFor these reasons, snapshot tests for plots tend to be fragile and are not run on CRAN machines by default.\n\n:::\n\n## Further reading\n\n- `{vdiffr}` package [website](https://vdiffr.r-lib.org/){target=\"_blank\"}\n\n# Testing entire files\n\nWhole file snapshot testing makes sure that media, data frames, text files, etc. are as expected.\n\n## Writing test {.smaller}\n\nLet's say you want to test JSON files generated by `jsonlite::write_json()`.\n\n:::: {.columns}\n\n::: {.column width='50%'}\n\n**Test**\n\n\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\n# File: tests/testthat/test-write-json.R\ntest_that(\"json writer works\", {\n local_edition(3)\n \n r_to_json <- function(x) {\n path <- tempfile(fileext = \".json\")\n jsonlite::write_json(x, path)\n path\n }\n\n x <- list(1, list(\"x\" = \"a\"))\n expect_snapshot_file(r_to_json(x), \"demo.json\")\n})\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nā”€ā”€ Warning: json writer works ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\nAdding new file snapshot: 'tests/testthat/_snaps/demo.json'\n```\n\n\n:::\n:::\n\n\n\n\n\n**Snapshot**\n\n```json\n[[1],{\"x\":[\"a\"]}]\n```\n\n:::\n\n::: {.column width='50%'}\n\n:::{.callout-note}\n\n- To snapshot a file, you need to write a helper function that provides its path.\n\n- If a test file is called `test-foo.R`, the snapshot will be saved to `test/testthat/_snaps/foo` folder.\n\n- In this folder, there will be one file (e.g. `.json`) for every `expect_snapshot_file()` expectation in `test-foo.R`.\n\n- The name for snapshot file is taken from `name` argument to `expect_snapshot_file()`.\n\n:::\n\n:::\n\n::::\n\n\n## What test success looks like {.smaller}\n\nIf you run the test again, it'll succeed:\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\n# File: tests/testthat/test-write-json.R\ntest_that(\"json writer works\", {\n local_edition(3)\n \n r_to_json <- function(x) {\n path <- tempfile(fileext = \".json\")\n jsonlite::write_json(x, path)\n path\n }\n\n x <- list(1, list(\"x\" = \"a\"))\n expect_snapshot_file(r_to_json(x), \"demo.json\")\n})\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nTest passed šŸ˜€\n```\n\n\n:::\n:::\n\n\n\n\n\n## What test failure looks like {.smaller}\n\nIf the new output doesn't match the expected one, the test will fail:\n\n```{.r code-line-numbers=\"11\"}\n# File: tests/testthat/test-write-json.R\ntest_that(\"json writer works\", {\n local_edition(3)\n \n r_to_json <- function(x) {\n path <- tempfile(fileext = \".json\")\n jsonlite::write_json(x, path)\n path\n }\n\n x <- list(1, list(\"x\" = \"b\"))\n expect_snapshot_file(r_to_json(x), \"demo.json\")\n})\n```\n\n\n\n::: {.cell}\n::: {.cell-output .cell-output-stdout}\n\n```\nā”€ā”€ Failure: json writer works ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\nSnapshot of `r_to_json(x)` to 'slides.qmd/demo.json' has changed\nRun `testthat::snapshot_review('slides.qmd/')` to review changes\n```\n\n\n:::\n\n::: {.cell-output .cell-output-error}\n\n```\nError:\n! Test failed\n```\n\n\n:::\n:::\n\n\n\n## Fixing tests {.smaller}\n\nRunning `snapshot_review()` launches a Shiny app which can be used to either accept or reject the new output(s).\n\n\n\n::: {.cell}\n::: {.cell-output-display}\n![](media/json_snapshot.png){width=622}\n:::\n:::\n\n\n\n## Further reading {.smaller}\n\nDocumentation for [`expect_snapshot_file()`](https://testthat.r-lib.org/reference/expect_snapshot_file.html){target=\"_blank\"}\n\n# Testing Shiny applications\n\nTo write formal tests for Shiny applications, you will use `{testthat}` extension package: [`{shinytest2}`](https://rstudio.github.io/shinytest2/){target=\"_blank\"}.\n\n## How does `{shinytest2}` work? {.smaller}\n\n`{shinytest2}` uses a Shiny app (how meta! šŸ˜…) to record user interactions with the app and generate snapshots of the application's state. Future behaviour of the app will be compared against these snapshots to check for any changes.\n\n. . . \n\nExactly how tests for Shiny apps in R package are written depends on how the app is stored.\nThere are two possibilities, and you will discuss them both separately.\n\n. . . \n\n
\n\n:::: {.columns}\n\n::: {.column width='50%'}\n\n**Stored in `/inst` folder**\n\n```\nā”œā”€ā”€ DESCRIPTION\nā”œā”€ā”€ R\nā”œā”€ā”€ inst\nā”‚ ā””ā”€ā”€ sample_app\nā”‚ ā””ā”€ā”€ app.R\n```\n\n:::\n\n::: {.column width='50%'}\n\n**Returned by a function**\n\n```\nā”œā”€ā”€ DESCRIPTION\nā”œā”€ā”€ R\nā”‚ ā””ā”€ā”€ app-function.R\n```\n\n:::\n\n::::\n\n\n\n# Shiny app in subdirectory \n\n
\n\n```\nā”œā”€ā”€ DESCRIPTION\nā”œā”€ā”€ R\nā”œā”€ā”€ inst\nā”‚ ā””ā”€ā”€ sample_app\nā”‚ ā””ā”€ā”€ app.R\n```\n\n## Example app {.smaller}\n\nLet's say this app resides in the `inst/unitConverter/app.R` file.\n\n\n\n:::: {.columns}\n\n::: {.column width='40%'}\n\n**App**\n\n\n\n::: {.cell}\n::: {.cell-output-display}\n![](media/shiny_app.mov)\n:::\n:::\n\n\n\n:::\n\n::: {.column width='60%'}\n\n**Code**\n\n```{.r}\nlibrary(shiny)\n\nui <- fluidPage(\n titlePanel(\"Convert kilograms to grams\"),\n numericInput(\"kg\", \"Weight (in kg)\", value = 0),\n textOutput(\"g\")\n)\n\nserver <- function(input, output, session) {\n output$g <- renderText(\n paste0(\"Weight (in g): \", input$kg * 1000)\n )\n}\n\nshinyApp(ui, server)\n```\n\n:::\n\n::::\n\n## Generating a test {.smaller}\n\nTo create a snapshot test, go to the app directory and run `record_test()`.\n\n\n\n::: {.cell}\n::: {.cell-output-display}\n![](media/shinytest_record.mov){width=60%}\n:::\n:::\n\n\n\n## Auto-generated artifacts {.smaller}\n\n:::: {.columns}\n\n::: {.column width='60%'}\n\n**Test**\n\n```{.r}\nlibrary(shinytest2)\n\ntest_that(\"{shinytest2} recording: unitConverter\", {\n app <- AppDriver$new(\n name = \"unitConverter\", height = 543, width = 426)\n app$set_inputs(kg = 1)\n app$set_inputs(kg = 10)\n app$expect_values()\n})\n\n```\n\n:::\n\n::: {.column width='40%'}\n\n**Snapshot**\n\n\n\n::: {.cell}\n::: {.cell-output-display}\n![](media/unitConverter-001_.png){width=100%}\n:::\n:::\n\n\n\n:::\n\n::::\n\n:::{.callout-note}\n\n- `record_test()` will auto-generate a test file in the app directory. The test script will be saved in a *subdirectory* of the app (`inst/my-app/tests/testthat/test-shinytest2.R`).\n\n- There will be one `/tests` folder inside *every* app folder.\n\n- The snapshots are saved as `.png` file in `tests/testthat/test-shinytest2/_snaps/{.variant}/shinytest2`. The `{.variant}` here corresponds to operating system and R version used to record tests. For example, `_snaps/windows-4.1/shinytest2`.\n\n:::\n\n## Creating a driver script {.smaller}\n\nNote that currently your test scripts and results are in the `/inst` folder, but you'd also want to run these tests automatically using `{testthat}`.\n\nFor this, you will need to write a driver script like the following:\n\n```{.r}\nlibrary(shinytest2)\n\ntest_that(\"`unitConverter` app works\", {\n appdir <- system.file(package = \"package_name\", \"unitConverter\")\n test_app(appdir)\n})\n```\n\nNow the Shiny apps will be tested with the rest of the source code in the package! šŸŽŠ\t\n\n:::{.callout-tip}\n\nYou save the driver test in the `/tests` folder (`tests/testthat/test-inst-apps.R`), alongside other tests.\n\n:::\n\n## What test failure looks like {.smaller}\n\nLet's say, while updating the app, you make a mistake, which leads to a failed test.\n\n**Changed code with mistake**\n\n```{.r code-line-numbers=\"9\"}\nui <- fluidPage(\n titlePanel(\"Convert kilograms to grams\"),\n numericInput(\"kg\", \"Weight (in kg)\", value = 0),\n textOutput(\"g\")\n)\n\nserver <- function(input, output, session) {\n output$g <- renderText(\n paste0(\"Weight (in kg): \", input$kg * 1000) # should be `\"Weight (in g): \"`\n )\n}\n\nshinyApp(ui, server)\n```\n\n**Test failure JSON diff**\n\n```{.json code-line-number=\"6\"}\nDiff in snapshot file `shinytest2unitConverter-001.json`\n< before > after \n@@ 4,5 @@ @@ 4,5 @@ \n }, }, \n \"output\": { \"output\": { \n< \"g\": \"Weight (in g): 10000\" > \"g\": \"Weight (in kg): 10000\"\n }, }, \n \"export\": { \"export\": { \n```\n\n\n## Updating snapshots {.smaller}\n\nFixing this test will be similar to fixing any other snapshot test you've seen thus far.\n\n`{testthat2}` provides a Shiny app for comparing the old and new snapshots.\n\n\n\n::: {.cell}\n::: {.cell-output-display}\n![](media/shinytest_failure.mov){width=60%}\n:::\n:::\n\n\n\n# Function returns Shiny app\n\n
\n\n```\nā”œā”€ā”€ DESCRIPTION\nā”œā”€ā”€ R\nā”‚ ā””ā”€ā”€ app-function.R\n```\n\n## Example app and test {.smaller}\n\nThe only difference in testing workflow when Shiny app objects are created by functions is that you will write the test ourselves, instead of `{shinytest2}` auto-generating it.\n\n:::: {.columns}\n\n::: {.column width='55%'}\n\n**Source code**\n\n```{.r}\n# File: R/unit-converter.R\nunitConverter <- function() {\n ui <- fluidPage(\n titlePanel(\"Convert kilograms to grams\"),\n numericInput(\"kg\", \"Weight (in kg)\", value = 0),\n textOutput(\"g\")\n )\n\n server <- function(input, output, session) {\n output$g <- renderText(\n paste0(\"Weight (in g): \", input$kg * 1000)\n )\n }\n\n shinyApp(ui, server)\n}\n```\n\n\n:::\n\n::: {.column width='45%'}\n\n**Test file to modify**\n\n```{.r}\n# File: tests/testthat/test-unit-converter.R\ntest_that(\"unitConverter app works\", {\n shiny_app <- unitConverter()\n app <- AppDriver$new(shiny_app)\n})\n```\n\n\n:::\n\n::::\n\n## Generating test and snapshots {.smaller}\n\nyou call `record_test()` directly on a Shiny app object, copy-paste commands to the test script, and run `devtools::test_active_file()` to generate snapshots.\n\n\n\n::: {.cell}\n::: {.cell-output-display}\n![](media/shiny_app_function_return.mov){width=60%}\n:::\n:::\n\n\n\n## Testing apps from frameworks {.smaller}\n\nThis testing workflow is also relevant for app frameworks (e.g. [`{golem}`](https://thinkr-open.github.io/golem/index.html){target=\"_blank\"}, [`{rhino}`](https://appsilon.github.io/rhino/){target=\"_blank\"}, etc.).\n\n:::: {.columns}\n\n::: {.column width='50%'}\n\n**`{golem}`**\n\nFunction in `run_app.R`\nreturns app.\n\n```\nā”œā”€ā”€ DESCRIPTION \nā”œā”€ā”€ NAMESPACE \nā”œā”€ā”€ R \nā”‚ ā”œā”€ā”€ app_config.R \nā”‚ ā”œā”€ā”€ app_server.R \nā”‚ ā”œā”€ā”€ app_ui.R \nā”‚ ā””ā”€ā”€ run_app.R \n```\n\n:::\n\n::: {.column width='50%'}\n\n**`{rhino}`**\n\nFunction in `app.R`\nreturns app.\n\n```\nā”œā”€ā”€ app\nā”‚ ā”œā”€ā”€ js\nā”‚ ā”‚ ā””ā”€ā”€ index.js\nā”‚ ā”œā”€ā”€ logic\nā”‚ ā”‚ ā””ā”€ā”€ __init__.R\nā”‚ ā”œā”€ā”€ static\nā”‚ ā”‚ ā””ā”€ā”€ favicon.ico\nā”‚ ā”œā”€ā”€ styles\nā”‚ ā”‚ ā””ā”€ā”€ main.scss\nā”‚ ā”œā”€ā”€ view\nā”‚ ā”‚ ā””ā”€ā”€ __init__.R\nā”‚ ā””ā”€ā”€ main.R\nā”œā”€ā”€ tests\nā”‚ ā”œā”€ā”€ ...\nā”œā”€ā”€ app.R\nā”œā”€ā”€ RhinoApplication.Rproj\nā”œā”€ā”€ dependencies.R\nā”œā”€ā”€ renv.lock\nā””ā”€ā”€ rhino.yml\n```\n\n:::\n\n::::\n\n\n## Final directory structure {.smaller}\n\nThe final location of the tests and snapshots should look like the following for the two possible ways Shiny apps are included in R packages.\n\n
\n\n:::: {.columns}\n\n::: {.column width='50%'}\n\n**Stored in `/inst` folder**\n\n```\nā”œā”€ā”€ DESCRIPTION\nā”œā”€ā”€ R\nā”œā”€ā”€ inst\nā”‚ ā””ā”€ā”€ sample_app\nā”‚ ā”œā”€ā”€ app.R\nā”‚ ā””ā”€ā”€ tests\nā”‚ ā”œā”€ā”€ testthat\nā”‚ ā”‚ ā”œā”€ā”€ _snaps\nā”‚ ā”‚ ā”‚ ā””ā”€ā”€ shinytest2\nā”‚ ā”‚ ā”‚ ā””ā”€ā”€ 001.json\nā”‚ ā”‚ ā””ā”€ā”€ test-shinytest2.R\nā”‚ ā””ā”€ā”€ testthat.R\nā””ā”€ā”€ tests\n ā”œā”€ā”€ testthat\n ā”‚ ā””ā”€ā”€ test-inst-apps.R\n ā””ā”€ā”€ testthat.R\n```\n\n:::\n\n::: {.column width='50%'}\n\n**Returned by a function**\n\n```\nā”œā”€ā”€ DESCRIPTION\nā”œā”€ā”€ R\nā”‚ ā””ā”€ā”€ app-function.R\nā””ā”€ā”€ tests\n ā”œā”€ā”€ testthat\n ā”‚ ā”œā”€ā”€ _snaps\n ā”‚ ā”‚ ā””ā”€ā”€ app-function\n ā”‚ ā”‚ ā””ā”€ā”€ 001.json\n ā”‚ ā””ā”€ā”€ test-app-function.R\n ā””ā”€ā”€ testthat.R\n```\n\n:::\n\n::::\n\n## Testing multiple apps {.smaller}\n\nFor the sake of completeness, here is what the test directory structure would like when there are multiple apps in a single package.\n\n:::: {.columns}\n\n::: {.column width='50%'}\n\n**Stored in `/inst` folder**\n\n```\nā”œā”€ā”€ DESCRIPTION\nā”œā”€ā”€ R\nā”œā”€ā”€ inst\nā”‚ ā””ā”€ā”€ sample_app1\nā”‚ ā”œā”€ā”€ app.R\nā”‚ ā””ā”€ā”€ tests\nā”‚ ā”œā”€ā”€ testthat\nā”‚ ā”‚ ā”œā”€ā”€ _snaps\nā”‚ ā”‚ ā”‚ ā””ā”€ā”€ shinytest2\nā”‚ ā”‚ ā”‚ ā””ā”€ā”€ 001.json\nā”‚ ā”‚ ā””ā”€ā”€ test-shinytest2.R\nā”‚ ā””ā”€ā”€ testthat.R\nā”‚ ā””ā”€ā”€ sample_app2\nā”‚ ā”œā”€ā”€ app.R\nā”‚ ā””ā”€ā”€ tests\nā”‚ ā”œā”€ā”€ testthat\nā”‚ ā”‚ ā”œā”€ā”€ _snaps\nā”‚ ā”‚ ā”‚ ā””ā”€ā”€ shinytest2\nā”‚ ā”‚ ā”‚ ā””ā”€ā”€ 001.json\nā”‚ ā”‚ ā””ā”€ā”€ test-shinytest2.R\nā”‚ ā””ā”€ā”€ testthat.R\nā””ā”€ā”€ tests\n ā”œā”€ā”€ testthat\n ā”‚ ā””ā”€ā”€ test-inst-apps.R\n ā””ā”€ā”€ testthat.R\n```\n\n:::\n\n::: {.column width='50%'}\n\n**Returned by a function**\n\n```\nā”œā”€ā”€ DESCRIPTION\nā”œā”€ā”€ R\nā”‚ ā””ā”€ā”€ app-function1.R\nā”‚ ā””ā”€ā”€ app-function2.R\nā””ā”€ā”€ tests\n ā”œā”€ā”€ testthat\n ā”‚ ā”œā”€ā”€ _snaps\n ā”‚ ā”‚ ā””ā”€ā”€ app-function1\n ā”‚ ā”‚ ā””ā”€ā”€ 001.json\n ā”‚ ā”‚ ā””ā”€ā”€ app-function2\n ā”‚ ā”‚ ā””ā”€ā”€ 001.json\n ā”‚ ā””ā”€ā”€ test-app-function1.R\n ā”‚ ā””ā”€ā”€ test-app-function2.R\n ā””ā”€ā”€ testthat.R\n```\n\n:::\n\n::::\n\n## Advanced topics {.smaller}\n\nThe following are some advanced topics that are beyond the scope of the current presentation, but you may wish to know more about.\n\n:::{.callout-tip}\n\n## Extra\n\n- If you want to test Shiny apps with continuous integration using `{shinytest2}`, read [this](https://rstudio.github.io/shinytest2/articles/use-ci.html){target=\"_blank\"} article.\n\n- `{shinytest2}` is a successor to `{shinytest}` package. If you want to migrate from the latter to the former, have a look at [this](https://rstudio.github.io/shinytest2/articles/z-migration.html){target=\"_blank\"}.\n\n:::\n\n\n\n## Further reading {.smaller}\n\n- [Testing](https://mastering-shiny.org/scaling-testing.html){target=\"_blank\"} chapter from *Mastering Shiny* book\n\n- `{shinytest2}` article introducing its [workflow](https://rstudio.github.io/shinytest2/articles/shinytest2.html){target=\"_blank\"}\n\n- `{shinytest2}` article on how to test apps in [R packages](https://rstudio.github.io/shinytest2/articles/use-package.html){target=\"_blank\"}\n\n# Headaches \n\nIt's not all kittens and roses when it comes to snapshot testing. \n\nLet's see some issues you might run into while using them. šŸ¤•\n\n## Testing behavior that you don't own {.smaller}\n\nLet's say you write a graphical snapshot test for a function that produces a `ggplot` object. If `{ggplot2}` authors make some modifications to this object, your tests will fail, even though your function works as expected!\n\nIn other words, *your* tests are now at the mercy of *other package authors* because snapshots are capturing things beyond your package's control.\n\n:::{.callout-caution}\n\nTests that fail for reasons other than what they are testing for are problematic. Thus, be careful about *what* you snapshot and keep in mind the maintenance burden that comes with dependencies with volatile APIs.\n\n:::\n\n:::{.callout-note}\n\n*A* way to reduce the burden of keeping snapshots up-to-date is to [automate this process](https://github.com/krlmlr/actions-sync/tree/base/update-snapshots){target=\"_blank\"}. But there is no free lunch in this universe, and now you need to maintain this automation! šŸ¤·\n\n:::\n\n## Failures in non-interactive environments {.smaller}\n\nIf snapshots fail locally, you can just run `snapshot_review()`, but what if they fail in non-interactive environments (on CI/CD platforms, during `R CMD Check`, etc.)?\n\nThe easiest solution is to copy the new snapshots to the local folder and run `snapshot_review()`.\n\n:::{.callout-tip}\n\nIf expected snapshot is called (e.g.) `foo.svg`, there will be a new snapshot file `foo.new.svg` in the same folder when the test fails.\n\n`snapshot_review()` compares these files to reveal how the outputs have changed.\n\n:::\n\nBut where can you find the new snapshots?\n\n## Accessing new snapshots {.smaller}\n\nIn local `R CMD Check`, you can find new snapshots in `.Rcheck` folder:\n\n```r\npackage_name.Rcheck/tests/testthat/_snaps/\n```\n\nOn CI/CD platforms, you can find snapshots in artifacts folder:\n\n- [AppVeyor](https://www.appveyor.com/docs/packaging-artifacts/){target=\"_blank\"}\n\n\n\n::: {.cell}\n::: {.cell-output-display}\n![](media/AppVeyor.png){width=484}\n:::\n:::\n\n\n\n- [GitHub actions](https://github.com/r-lib/actions/tree/v2-branch/check-r-package){target=\"_blank\"}\n\n```yaml\n - uses: r-lib/actions/check-r-package@v2\n with:\n upload-snapshots: true\n```\n\n## Code review with snapshot tests {.smaller}\n\nDespite snapshot tests making the expected outputs more human-readable, given a big enough change and complex enough output, sometimes it can be challenging to review changes to snapshots. \n\nHow do you review pull requests with complex snapshots changes? \n\n## {.smaller}\n\n:::panel-tabset\n\n### Option-1\n\nUse tools provided for code review by hosting platforms (like GitHub).\nFor example, to review changes in SVG snapshots:\n\n\n\n::: {.cell}\n::: {.cell-output-display}\n![](media/github_pr.mov){width=70%}\n:::\n:::\n\n\n\n### Option-2\n\nLocally open the PR branch and play around with the changes to see if the new behaviour makes sense. šŸ¤· \n\n:::\n\n## Danger of silent failures {.smaller}\n\nGiven their fragile nature, snapshot tests are skipped on CRAN by default. \n\nAlthough this makes sense, it means that you miss out on anything but a breaking change from upstream dependency. E.g., if `{ggplot2}` (hypothetically) changes how the points look, you won't know about this change until you *happen to* run your snapshot tests again locally or on CI/CD.\n\nUnit tests run on CRAN, on the other hand, will fail and you will be immediately informed about it.\n\n:::{.callout-tip collapse=false appearance='default' icon=true}\n\nA way to insure against such silent failures is to run tests **daily** on CI/CD platforms (e.g. AppVeyor nightly builds).\n\n:::\n\n# Parting wisdom\n\nWhat *not* to do\n\n## Don't use snapshot tests for *everything* {.smaller}\n\nIt is tempting to use them everywhere out of laziness. But they are sometimes inappropriate (e.g. when testing requires external benchmarking).\n\nLet's say you write a function to extract estimates from a regression model.\n\n```{.r}\nextract_est <- function(m) m$coefficients\n```\n\nIts test should compare results against an external benchmark, and not a snapshot.\n\n:::: {.columns}\n\n::: {.column width='50%'}\n\n**Good**\n\n```{.r}\ntest_that(\"`extract_est()` works\", {\n m <- lm(wt ~ mpg, mtcars)\n expect_equal(\n extract_est(m)[[1]],\n m$coefficients[[1]]\n )\n})\n```\n\n\n:::\n\n::: {.column width='50%'}\n\n**Bad**\n\n```{.r}\ntest_that(\"`extract_est()` works\", {\n m <- lm(wt ~ mpg, mtcars)\n expect_snapshot(extract_est(m))\n})\n```\n\n:::\n\n::::\n\n## Snapshot for humans, not machines {.smaller}\n\nSnapshot testing is appropriate when the human needs to be in the loop to make sure that things are working as expected. Therefore, the snapshots should be human readable.\n\nE.g. if you write a function that plots something:\n\n```{.r}\npoint_plotter <- function() ggplot(mtcars, aes(wt, mpg)) + geom_point()\n```\n\nTo test it, you should snapshot the *plot*, and not the underlying *data*, which is hard to make sense of for a human.\n\n:::: {.columns}\n\n::: {.column width='50%'}\n\n**Good**\n\n```{.r}\ntest_that(\"`point_plotter()` works\", {\n expect_doppelganger(\n \"point-plotter-mtcars\",\n point_plotter()\n )\n})\n```\n\n:::\n\n::: {.column width='50%'}\n\n**Bad**\n\n```{.r}\ntest_that(\"`point_plotter()` works\", {\n p <- point_plotter()\n pb <- ggplot_build(p)\n expect_snapshot(pb$data)\n})\n```\n\n:::\n\n::::\n\n## Don't **blindly accept** snapshot changes {.smaller}\n\nResist formation of such a habit.\n\n`{testthat}` provides tools to make it very easy to review changes, so no excuses! \n\n\n# Self-study \n\nIn this presentation, you deliberately kept the examples and the tests simple. \n\nTo see a more realistic usage of snapshot tests, you can study open-source test suites.\n\n## Suggested repositories {.smaller}\n\n::: columns\n\n\n::: {.column width=\"33%\"}\n### Print outputs\n\n- [`{cli}`](https://github.com/r-lib/cli/tree/main/tests/testthat){target=\"_blank\"} (for testing command line interfaces)\n\n- [`{pkgdown}`](https://github.com/r-lib/pkgdown/tree/main/tests/testthat){target=\"_blank\"} (for testing generated HTML documents)\n\n- [`{dbplyr}`](https://github.com/tidyverse/dbplyr/tree/main/tests/testthat){target=\"_blank\"} (for testing printing of generated SQL queries)\n\n- [`{gt}`](https://github.com/rstudio/gt/tree/master/tests/testthat){target=\"_blank\"} (for testing table printing)\n:::\n\n\n::: {.column width=\"33%\"}\n\n### Visualizations\n\n- [`{ggplot2}`](https://github.com/tidyverse/ggplot2/tree/main/tests/testthat){target=\"_blank\"}\n\n- [`{ggstatsplot}`](https://github.com/IndrajeetPatil/ggstatsplot/tree/main/tests/testthat){target=\"_blank\"}\n\n:::\n\n\n::: {.column width=\"33%\"}\n\n### Shiny apps \n\n- [`{shinytest2}`](https://github.com/rstudio/shinytest2/tree/main/tests/testthat){target=\"_blank\"}\n\n- [`{designer}`](https://github.com/ashbaldry/designer/tree/dev/tests/testthat){target=\"_blank\"}\n\n:::\n\n:::\n\n# Activities\n\nIf you feel confident enough to contribute to open-source projects to practice these skills, here are some options.\n\n## Practice makes it perfect {.smaller}\n\nThese are only suggestions. Feel free to contribute to any project you like! šŸ¤\n\n:::{.callout-tip}\n\n## Suggestions \n\n- See if `{ggplot2}` [extensions](https://exts.ggplot2.tidyverse.org/gallery/){target=\"_blank\"} you like use snapshot tests for graphics. If not, you can add them for key functions.\n\n- Check out hard reverse dependencies of [`{shiny}`](https://cran.r-project.org/web/packages/shiny/index.html){target=\"_blank\"}, and add snapshot tests using `{shinytest2}` to an app of your liking.\n\n- Add more `{vdiffr}` snapshot tests to plotting functions in [`{see}`](https://github.com/easystats/see), a library for statistical visualizations (I can chaperone your PRs here).\n\n- `{shinytest2}` is the successor to `{shinytest}` package. Check out which packages currently use it for [testing](https://cran.r-project.org/web/packages/shinytest/index.html){target=\"_blank\"} Shiny apps, and see if you can use `{shinytest2}` instead (see how-to [here](https://rstudio.github.io/shinytest2/articles/z-migration.html){target=\"_blank\"}).\n\n:::\n\n## General reading {.smaller}\n\nAlthough current presentation is focused only on snapshot testing, here is reading material on automated testing in general.\n\n- McConnell, S. (2004). *Code Complete*. Microsoft Press. (**pp. 499-533**)\n\n- Boswell, D., & Foucher, T. (2011). *The Art of Readable Code*. O'Reilly Media, Inc. (**pp. 149-162**)\n\n- Riccomini, C., & Ryaboy D. (2021). *The Missing Readme*. No Starch Press. (**pp. 89-108**)\n\n- Martin, R. C. (2009). *Clean Code*. Pearson Education. (**pp. 121-133**)\n\n- Fowler, M. (2018). *Refactoring.* Addison-Wesley Professional. (**pp. 85-100**)\n\n- Beck, K. (2003). *Test-Driven Development*. Addison-Wesley Professional.\n\n## Additional resources \n\nFor a comprehensive collection of packages for unit testing in R, see [this](https://indrajeetpatil.github.io/awesome-r-pkgtools/#unit-testing){target=\"_blank\"} page.\n\n# For more\n\nIf you are interested in good programming and software development practices, check out my other [slide decks](https://sites.google.com/site/indrajeetspatilmorality/presentations){target=\"_blank\"}.\n\n# Find me at...\n\n{{< fa brands twitter >}} [Twitter](http://twitter.com/patilindrajeets){target=\"_blank\"}\n\n{{< fa brands linkedin >}} [LikedIn](https://www.linkedin.com/in/indrajeet-patil-397865174/){target=\"_blank\"}\n\n{{< fa brands github >}} [GitHub](http://github.com/IndrajeetPatil){target=\"_blank\"}\n\n{{< fa solid link >}} [Website](https://sites.google.com/site/indrajeetspatilmorality/){target=\"_blank\"}\n\n{{< fa solid envelope >}} [E-mail](mailto:patilindrajeet.science@gmail.com){target=\"_blank\"}\n\n# Thank You \n\nAnd Happy Snapshotting! šŸ˜Š\n\n## Session information {.smaller}\n\n\n\n::: {.cell}\n\n```{.r .cell-code}\nquarto::quarto_version()\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\n[1] '1.5.27'\n```\n\n\n:::\n\n```{.r .cell-code}\nsessioninfo::session_info(include_base = TRUE)\n```\n\n::: {.cell-output .cell-output-stdout}\n\n```\nā”€ Session info ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\n setting value\n version R version 4.3.3 (2024-02-29)\n os Ubuntu 22.04.4 LTS\n system x86_64, linux-gnu\n ui X11\n language (EN)\n collate C.UTF-8\n ctype C.UTF-8\n tz UTC\n date 2024-03-31\n pandoc 3.1.11 @ /opt/hostedtoolcache/pandoc/3.1.11/x64/ (via rmarkdown)\n\nā”€ Packages ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\n package * version date (UTC) lib source\n base * 4.3.3 2024-03-04 [3] local\n brio 1.1.4 2023-12-10 [1] RSPM\n cli 3.6.2 2023-12-11 [1] RSPM\n colorspace 2.1-0 2023-01-23 [1] RSPM\n compiler 4.3.3 2024-03-04 [3] local\n crayon 1.5.2 2022-09-29 [1] RSPM\n datasets * 4.3.3 2024-03-04 [3] local\n desc 1.4.3 2023-12-10 [1] RSPM\n diffobj 0.3.5 2021-10-05 [1] RSPM\n digest 0.6.35 2024-03-11 [1] RSPM\n evaluate 0.23 2023-11-01 [1] RSPM\n fansi 1.0.6 2023-12-08 [1] RSPM\n farver 2.1.1 2022-07-06 [1] RSPM\n fastmap 1.1.1 2023-02-24 [1] RSPM\n ggplot2 * 3.5.0 2024-02-23 [1] RSPM\n glue 1.7.0 2024-01-09 [1] RSPM\n graphics * 4.3.3 2024-03-04 [3] local\n grDevices * 4.3.3 2024-03-04 [3] local\n grid 4.3.3 2024-03-04 [3] local\n gtable 0.3.4 2023-08-21 [1] RSPM\n htmltools 0.5.8 2024-03-25 [1] RSPM\n jsonlite 1.8.8 2023-12-04 [1] RSPM\n knitr 1.45 2023-10-30 [1] RSPM\n labeling 0.4.3 2023-08-29 [1] RSPM\n later 1.3.2 2023-12-06 [1] RSPM\n lattice 0.22-5 2023-10-24 [3] CRAN (R 4.3.3)\n lifecycle 1.0.4 2023-11-07 [1] RSPM\n magrittr 2.0.3 2022-03-30 [1] RSPM\n Matrix 1.6-5 2024-01-11 [3] CRAN (R 4.3.3)\n methods * 4.3.3 2024-03-04 [3] local\n mgcv 1.9-1 2023-12-21 [3] CRAN (R 4.3.3)\n munsell 0.5.0 2018-06-12 [1] RSPM\n nlme 3.1-164 2023-11-27 [3] CRAN (R 4.3.3)\n pillar 1.9.0 2023-03-22 [1] RSPM\n pkgconfig 2.0.3 2019-09-22 [1] RSPM\n pkgload 1.3.4 2024-01-16 [1] RSPM\n png 0.1-8 2022-11-29 [1] RSPM\n processx 3.8.4 2024-03-16 [1] RSPM\n ps 1.7.6 2024-01-18 [1] RSPM\n quarto 1.4.1 2024-03-10 [1] Github (quarto-dev/quarto-r@ba8485a)\n R6 2.5.1 2021-08-19 [1] RSPM\n Rcpp 1.0.12 2024-01-09 [1] RSPM\n rematch2 2.1.2 2020-05-01 [1] RSPM\n rlang 1.1.3 2024-01-10 [1] RSPM\n rmarkdown 2.26 2024-03-05 [1] RSPM\n rprojroot 2.0.4 2023-11-05 [1] RSPM\n rstudioapi 0.16.0 2024-03-24 [1] RSPM\n scales 1.3.0 2023-11-28 [1] RSPM\n sessioninfo 1.2.2 2021-12-06 [1] any (@1.2.2)\n splines 4.3.3 2024-03-04 [3] local\n stats * 4.3.3 2024-03-04 [3] local\n testthat * 3.2.1 2023-12-02 [1] RSPM\n tibble 3.2.1 2023-03-20 [1] RSPM\n tools 4.3.3 2024-03-04 [3] local\n utf8 1.2.4 2023-10-22 [1] RSPM\n utils * 4.3.3 2024-03-04 [3] local\n vctrs 0.6.5 2023-12-01 [1] RSPM\n vdiffr * 1.0.7 2023-09-22 [1] RSPM\n waldo 0.5.2 2023-11-02 [1] RSPM\n withr 3.0.0 2024-01-16 [1] RSPM\n xfun 0.43 2024-03-25 [1] RSPM\n yaml 2.3.8 2023-12-11 [1] RSPM\n\n [1] /home/runner/work/_temp/Library\n [2] /opt/R/4.3.3/lib/R/site-library\n [3] /opt/R/4.3.3/lib/R/library\n\nā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€\n```\n\n\n:::\n:::\n", "supporting": [ "slides_files" ], diff --git a/index.html b/index.html index a14c13a..8778b01 100644 --- a/index.html +++ b/index.html @@ -11,7 +11,7 @@ - + slides @@ -647,7 +647,7 @@ ) })
-
Test passed šŸ˜€
+
Test passed šŸŽŠ
@@ -747,7 +747,7 @@ ))) })
-
Test passed šŸ˜€
+
Test passed šŸ˜ø
@@ -1062,7 +1062,7 @@ ) })
-
Test passed šŸ„³
+
Test passed šŸŽ‰

What test failure looks like

@@ -1201,7 +1201,7 @@ expect_snapshot_file(r_to_json(x), "demo.json") })
-
Test passed šŸŽ‰
+
Test passed šŸ˜€

What test failure looks like

@@ -1840,7 +1840,7 @@

Shiny apps

quarto::quarto_version()
-
[1] '1.5.26'
+
[1] '1.5.27'
sessioninfo::session_info(include_base = TRUE)
@@ -1854,7 +1854,7 @@

Shiny apps

collate C.UTF-8 ctype C.UTF-8 tz UTC - date 2024-03-24 + date 2024-03-31 pandoc 3.1.11 @ /opt/hostedtoolcache/pandoc/3.1.11/x64/ (via rmarkdown) ā”€ Packages ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ @@ -1879,7 +1879,7 @@

Shiny apps

grDevices * 4.3.3 2024-03-04 [3] local grid 4.3.3 2024-03-04 [3] local gtable 0.3.4 2023-08-21 [1] RSPM - htmltools 0.5.7 2023-11-03 [1] RSPM + htmltools 0.5.8 2024-03-25 [1] RSPM jsonlite 1.8.8 2023-12-04 [1] RSPM knitr 1.45 2023-10-30 [1] RSPM labeling 0.4.3 2023-08-29 [1] RSPM @@ -1905,7 +1905,7 @@

Shiny apps

rlang 1.1.3 2024-01-10 [1] RSPM rmarkdown 2.26 2024-03-05 [1] RSPM rprojroot 2.0.4 2023-11-05 [1] RSPM - rstudioapi 0.15.0 2023-07-07 [1] RSPM + rstudioapi 0.16.0 2024-03-24 [1] RSPM scales 1.3.0 2023-11-28 [1] RSPM sessioninfo 1.2.2 2021-12-06 [1] any (@1.2.2) splines 4.3.3 2024-03-04 [3] local @@ -1919,7 +1919,7 @@

Shiny apps

vdiffr * 1.0.7 2023-09-22 [1] RSPM waldo 0.5.2 2023-11-02 [1] RSPM withr 3.0.0 2024-01-16 [1] RSPM - xfun 0.42 2024-02-08 [1] RSPM + xfun 0.43 2024-03-25 [1] RSPM yaml 2.3.8 2023-12-11 [1] RSPM [1] /home/runner/work/_temp/Library diff --git a/slides.html b/slides.html index a14c13a..8778b01 100644 --- a/slides.html +++ b/slides.html @@ -11,7 +11,7 @@ - + slides @@ -647,7 +647,7 @@ ) })
-
Test passed šŸ˜€
+
Test passed šŸŽŠ
@@ -747,7 +747,7 @@ ))) })
-
Test passed šŸ˜€
+
Test passed šŸ˜ø
@@ -1062,7 +1062,7 @@ ) })
-
Test passed šŸ„³
+
Test passed šŸŽ‰

What test failure looks like

@@ -1201,7 +1201,7 @@ expect_snapshot_file(r_to_json(x), "demo.json") })
-
Test passed šŸŽ‰
+
Test passed šŸ˜€

What test failure looks like

@@ -1840,7 +1840,7 @@

Shiny apps

quarto::quarto_version()
-
[1] '1.5.26'
+
[1] '1.5.27'
sessioninfo::session_info(include_base = TRUE)
@@ -1854,7 +1854,7 @@

Shiny apps

collate C.UTF-8 ctype C.UTF-8 tz UTC - date 2024-03-24 + date 2024-03-31 pandoc 3.1.11 @ /opt/hostedtoolcache/pandoc/3.1.11/x64/ (via rmarkdown) ā”€ Packages ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ @@ -1879,7 +1879,7 @@

Shiny apps

grDevices * 4.3.3 2024-03-04 [3] local grid 4.3.3 2024-03-04 [3] local gtable 0.3.4 2023-08-21 [1] RSPM - htmltools 0.5.7 2023-11-03 [1] RSPM + htmltools 0.5.8 2024-03-25 [1] RSPM jsonlite 1.8.8 2023-12-04 [1] RSPM knitr 1.45 2023-10-30 [1] RSPM labeling 0.4.3 2023-08-29 [1] RSPM @@ -1905,7 +1905,7 @@

Shiny apps

rlang 1.1.3 2024-01-10 [1] RSPM rmarkdown 2.26 2024-03-05 [1] RSPM rprojroot 2.0.4 2023-11-05 [1] RSPM - rstudioapi 0.15.0 2023-07-07 [1] RSPM + rstudioapi 0.16.0 2024-03-24 [1] RSPM scales 1.3.0 2023-11-28 [1] RSPM sessioninfo 1.2.2 2021-12-06 [1] any (@1.2.2) splines 4.3.3 2024-03-04 [3] local @@ -1919,7 +1919,7 @@

Shiny apps

vdiffr * 1.0.7 2023-09-22 [1] RSPM waldo 0.5.2 2023-11-02 [1] RSPM withr 3.0.0 2024-01-16 [1] RSPM - xfun 0.42 2024-02-08 [1] RSPM + xfun 0.43 2024-03-25 [1] RSPM yaml 2.3.8 2023-12-11 [1] RSPM [1] /home/runner/work/_temp/Library