-
Notifications
You must be signed in to change notification settings - Fork 0
/
test_suite.qmd
327 lines (223 loc) · 9.58 KB
/
test_suite.qmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# Test suite {#sec-tests-suite}
```{r}
#| eval: true
#| echo: false
#| include: false
source("_common.R")
library(testthat)
library(withr)
library(logger)
```
```{r}
#| label: co_box_tldr
#| echo: false
#| results: asis
#| eval: true
co_box(
color = "b",
look = "default", hsize = "1.15", size = "1.10",
header = "TLDR   ![](images/testthat.png){width='8%'}",
fold = TRUE,
contents = "
<br>
**`testthat` Workflow:**
- `use_testthat()`: setup testing infrastructure in your app-package\n
- Include edition (i.e., `use_testthat(3)`)\n
- `use_test()`: creates new test files (with `test-` prefix)\n
- Each file under `R/` should a corresponding `test-` file\n
- `test_active_file()`: runs tests in the current open test file\n
- `test_coverage_active_file()`: test coverage for the current open test file\n
**Behavior-driven development functions:**
- `describe()`: provides context (user specification or feature) for tests and test code\n
- `it()`: used to test functional requirement (i.e., expectation functions).\n
"
)
```
---
Testing shiny applications poses some unique challenges. Shiny functions are written in the context of its reactive model,[^tests-shiny-reactive] so some standard testing techniques and methods for regular R packages don't directly apply. This chapter covers setting up the `testthat`’s infrastructure for unit tests. I'll also introduce some keyboard shortcuts for commonly used functions while developing tests.
[^tests-shiny-reactive]: The ['Reactivity - An overview'](https://shiny.posit.co/r/articles/build/reactivity-overview/) article gives an excellent description (and mental module) of reactive programming.
:::: {.callout-tip collapse='true' appearance='default'}
## [Accessing the code examples]{style='font-weight: bold; font-size: 1.15em;'}
::: {style='font-size: 0.95em; color: #282b2d;'}
I've created the [`shinypak` R package](https://mjfrigaard.github.io/shinypak/) In an effort to make each section accessible and easy to follow:
Install `shinypak` using `pak` (or `remotes`):
```{r}
#| code-fold: false
#| message: false
#| warning: false
#| eval: false
# install.packages('pak')
pak::pak('mjfrigaard/shinypak')
```
Review the chapters in each section:
```{r}
#| code-fold: false
#| message: false
#| warning: false
#| collapse: true
library(shinypak)
list_apps(regex = 'tests')
```
Launch an app:
```{r}
#| code-fold: false
#| eval: false
launch(app = "11_tests-specs")
```
:::
::::
## [`testthat`]{style="font-size: 1.05em;"} framework
`testthat` is the standard package for testing in R packages and one of the most widely used and supported packages on CRAN. Its widespread adoption is likely due to its ability to simplify the setup, creation, and execution of unit tests.
In our app-packages, we'll use `testthat` unit tests to ensure the underlying logic (i.e., non-reactive utility functions) behaves correctly. Combining Shiny's `testServer()` function and the `shinytest2` package with `testthat` provides a comprehensive testing suite for our app-package.
## [Setting up `testthat` tests]{style="font-size: 0.95em;"} {#sec-tests-suite-use-testthat}
The `testthat` package has been around for over a decade and thus has undergone various changes that require us to specify the edition we intend to use (currently, it's the third):[^tests-testthat-edition]
[^tests-testthat-edition]: Read more about changes to the third edition to `testthat` in [R Packages, 2ed](https://r-pkgs.org/testing-basics.html#introducing-testthat)
```{r}
#| eval: false
#| code-fold: false
usethis::use_testthat(3)
```
Setting up your testing infrastructure with `use_testthat()` does the following (`3` is the edition):
1. In the `DESCRIPTION` file, `testthat (>= 3.0.0)` is listed under `Suggests`
2. `Config/testthat/edition: 3` is also listed in the `DESCRIPTION` to specify the `testthat` edition
3. A new `tests/` folder is created, with a `testthat/` subfolder
4. The `tests/testthat/testthat.R` file is created
We now have a `tests/` folder to store our `testthat` tests.
```{bash}
#| eval: false
#| code-fold: false
tests/
├── testthat/
└── testthat.R #<1>
2 directories, 1 file
```
1. Referred to as the 'test runner,' because it runs all our tests (do not edit this file).
## Creating unit tests {#sec-tests-suite-use-test}
The standard workflow for writing `testthat` unit tests consists of the following:
**New tests** are created with `usethis::use_test()`:
```{r}
#| eval: false
#| code-fold: false
usethis::use_test("scatter_plot")
```
- `testthat` recommends having a corresponding test file in `tests/testthat/` (with the `test-` prefix) for the files in `R/`.
### [`test-`]{style="font-size: 0.95em;"} files
**Test files**: the IDE will automatically create and open the new test file:
```{verbatim}
#| eval: false
#| code-fold: false
✔ Writing 'tests/testthat/test-scatter_plot.R'
• Modify 'tests/testthat/test-scatter_plot.R'
```
### [`test_that()`]{style="font-size: 0.95em;"} tests {#sec-tests-suite-test-that}
Each new test file contains a boilerplate `test_that()` **test**:
```{r}
#| eval: false
#| code-fold: false
test_that(desc = "multiplication works", code = { # <1>
})
```
1. `desc` is the test context (supplied in `"quotes"`), and `code` is the test code (supplied in `{curly brackets}`).
### [`expect_`]{style="font-size: 0.95em;"}ations {#sec-tests-suite-expectations}
**Expectation** typically have two parts: an `observed` object, and an `expected` object:
```{r}
#| eval: false
#| code-fold: false
#| collapse: true
expect_equal( # <1>
object = 2 * 2, # <2>
expected = 4 # <3>
)
```
1. A `testthat` expectation function
2. The output or behavior being tested
3. A predefined output or behavior
The `observed` object is an artifact of some code we've written, and it's being compared against an `expected` result.
### BDD test functions {#sec-tests-suite-bdd-intro}
`testthat` also has two behavior-driven development (BDD) functions for performing tests: `describe()` and `it()`.
> "*Use `describe()` to verify that you implement the right things and use [`it()`] to ensure you do the things right.*" - `testthat` [documentation](https://testthat.r-lib.org/reference/describe.html)
```{r}
#| eval: false
#| code-fold: false
describe("Description of feature or specification", # <1>
code = {
it("Functionality under test", # <2>
code = { # <3>
expect_equal(
object = 2 * 2,
expected = 4
) # <3>
}) # <2>
}) # <1>
```
1. `describe()` the feature or specification
2. Capture `it()` in a test
3. Write expectations
We'll cover BDD more in the [next chapter](test_specs.qmd), but for now just know that each call to `it()` behaves like `test_that()`.
### Running tests {#sec-tests-suite-running-tests}
Another [`devtools`](development.qmd) habit to adopt is regularly writing and **running tests**. If you're using Posit Workbench and have `devtools` installed, you can test your app-package using the **Build** pane or the keyboard shortcut: [<kbd>Ctrl/Cmd</kbd> + <kbd>Shift</kbd> + <kbd>T</kbd>]{style="font-size: 0.90em"}
:::{.column-margin}
![Run all tests](img/11_tests_build_pane_test.png){width='100%'}
:::
### Keyboard shortcuts {#sec-tests-suite-keyboard-shortcuts}
R Packages, 2ed also [suggests](https://r-pkgs.org/testing-basics.html#run-tests) binding `test_active_file()` and `test_coverage_active_file()` to keyboard shortcuts. I **highly** recommend using a shortcut while developing tests because it will improve your ability to iterate quickly.
::: {layout="[54, -1, 45]" layout-valign="bottom"}
#### `devtools` function {.unnumbered}
[`test()`]{style="font-weight: bold; font-size: 0.95em"}
#### Keyboard shortcut {.unnumbered}
[<kbd>Ctrl/Cmd</kbd> + <kbd>Shift</kbd> + <kbd>T</kbd>]{style="font-weight: bold; font-size: 0.80em"}
:::
::: {layout="[54, -1, 45]" layout-valign="bottom"}
[`test_active_file()`]{style="font-weight: bold; font-size: 0.95em"}
[<kbd>Ctrl/Cmd</kbd> + <kbd>T</kbd>]{style="font-weight: bold; font-size: 0.80em"}
:::
::: {layout="[54, -1, 45]" layout-valign="bottom"}
[`test_coverage_active_file()`]{style="font-weight: bold; font-size: 0.95em"}
[<kbd>Ctrl/Cmd</kbd> + <kbd>Shift</kbd> + <kbd>R</kbd>]{style="font-weight: bold; font-size: 0.80em"}
:::
When the test is run, we'll see feedback on whether it passes or fails (and occasionally some encouragement):
```{r}
#| eval: true
#| code-fold: false
#| echo: true
#| collapse: true
test_that("multiplication works", {
expect_equal(
object = 2 * 2,
expected = 4
)
})
```
## Recap {.unnumbered}
```{r}
#| label: co_box_recap
#| echo: false
#| results: asis
#| eval: true
co_box(
color = "g",
look = "default", hsize = "1.15", size = "1.10",
header = "RECAP   ![](images/testthat.png){width='8%'}",
fold = FALSE,
contents = "
<br>
**`testthat` setup**\n
- `use_testthat()`: sets up testing infrastructure in your app-package\n
**Test files**\n
- `use_test()`: creates new test files (with `test-` prefix). The test file names should *generally* match the file names be below` R/`.\n
**BDD test functions**\n
- `describe()`: Feature descriptions and any relevant background information\n
- `it()`: Scenarios and test code with expectations (`Then` statement = functional requirement).\n
**Running tests**\n
- `test_active_file()`: runs tests in the current open test file\n
- `test_coverage_active_file()`: test coverage for the current open test file\n
"
)
```
```{r}
#| label: git_contrib_box
#| echo: false
#| results: asis
#| eval: true
git_contrib_box()
```