Skip to content

Commit 73774ae

Browse files
committed
Improve custom expectation docs
1 parent 9c93b8f commit 73774ae

File tree

1 file changed

+125
-33
lines changed

1 file changed

+125
-33
lines changed

vignettes/custom-expectation.Rmd

Lines changed: 125 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -16,54 +16,82 @@ snapper <- local_snapshotter()
1616
snapper$start_file("snapshotting.Rmd", "test")
1717
```
1818

19-
This vignette shows you how to write your expectations that work identically to the built-in `expect_` functions.
20-
21-
You can use these either locally by putting them in a helper file, or export them from your package.
19+
This vignette shows you how to write your expectations. You can use within your package by putting them in a helper file, or share them with others by exporting them from your package.
2220

2321
## Expectation basics
2422

25-
There are three main parts to writing an expectation, as illustrated by `expect_length()`:
23+
An expectation has three main parts, as illustrated by `expect_length()`:
2624

2725
```{r}
2826
expect_length <- function(object, n) {
2927
# 1. Capture object and label
3028
act <- quasi_label(rlang::enquo(object), arg = "object")
3129
32-
# 2. Verify the expectations
30+
# 2. Fail when expectations aren't met
3331
act_n <- length(act$val)
3432
if (act_n != n) {
3533
msg <- sprintf("%s has length %i, not length %i.", act$lab, act_n, n)
3634
return(fail(msg))
3735
}
3836
39-
# 3. Pass
37+
# 3. Pass when expectations are
4038
pass(act$val)
4139
}
4240
```
4341

44-
### Capture value and label
42+
The first step in any expectation is to use `quasi_label()` to capture both the value (`$val`) of the first argument and a label (`$lab`) to use failure messages. This is a pattern that exists to support fairly esoteric testthat features; you don't need to understand, just copy and paste it 🙂.
43+
44+
Next, you need to fail, for each way that the `object` violates our expectation. In my experience it's easier to check for problems one by one, because that yields the most informative failure messages. Note that it's really important to `return(fail())` here. You wont see the problem when interactively testing your function because when run outside of `test_that()`, `fail()` throws an error, causing the function to terminate early. When running inside of `test_that()` however, `fail()` does not stop execution because we want to collect all failures in a given test.
45+
46+
Finally, if the object is expected, call `pass()` with the input value (usually `act$val`). Returning the input value is good practice since expectation functions are called primarily for their side-effects (triggering a failure). This allows expectations to be chained:
4547

46-
The first step in any expectation is to capture the actual object, and generate a label for it to use if a failure occur. All testthat expectations support quasiquotation so that you can unquote variables. This makes it easier to generate good labels when the expectation is called from a function or within a for loop.
48+
```{r}
49+
mtcars |>
50+
expect_type("list") |>
51+
expect_s3_class("data.frame") |>
52+
expect_length(11)
53+
```
4754

48-
By convention, the first argument to every `expect_` function is called `object`, and you capture its value (`val`) and label (`lab`) with `act <- quasi_label(enquo(object))`, where `act` is short for actual (in constrast to expected).
55+
## Testing your expectations
4956

50-
### Verify the expectation
57+
testthat comes with three expectations designed specifically to test expectations: `expect_success()` and `expect_failure()`:
5158

52-
Now we can check if our expectation is met and return `fail()` if not. The most challenging job here is typically generating the error message because you want it to be as self-contained as possible. This means it should typically give both the expected and actual value, along with the name of the object passed to the expectation. testthat expectations use `sprintf()`, but if you're familiar with {glue}, you might want to use that instead.
59+
* `expect_success()` checks that your expectation emits exactly one success and zero failures.
60+
* `expect_failure()` checks that your expectation emits exactly one failure and zero successes.
61+
* `expect_failure_snapshot()` captures the failure message in a snapshot, making it easier to review if it's useful or not.
5362

54-
More complicated expectations will have more `if` statements. For example, we might want to make our `expect_length()` function include an assertion that `object` is a vector:
63+
It's important to check that expectations return either one failure or one success because the ensures that reporting is correct. If you
64+
65+
```{r}
66+
test_that("expect_length works as expected", {
67+
x <- 1:10
68+
expect_success(expect_length(x, 10))
69+
expect_failure(expect_length(x, 11))
70+
})
71+
72+
test_that("expect_length gives useful feedback", {
73+
x <- 1:10
74+
expect_snapshot_failure(expect_length(x, 11))
75+
})
76+
```
77+
78+
## Examples
79+
80+
### `expect_vector_length()`
81+
82+
For example, you could imagine a slightly more complex version that first checked if the object was a vector:
5583

5684
```{r}
5785
expect_vector_length <- function(object, n) {
58-
act <- quasi_label(rlang::enquo(object), arg = "object")
86+
act <- quasi_label(rlang::enquo(object))
5987
60-
if (!is.atomic(act$val) || !is.list(act$val)) {
88+
if (!is.atomic(act$val) && !is.list(act$val)) {
6189
msg <- sprintf("%s is a %s, not a vector", act$lab, typeof(act$val))
6290
return(fail(msg))
6391
}
6492
6593
act_n <- length(act$val)
66-
if (act$n != n) {
94+
if (act_n != n) {
6795
msg <- sprintf("%s has length %i, not length %i.", act$lab, act_n, n)
6896
return(fail(msg))
6997
}
@@ -72,33 +100,97 @@ expect_vector_length <- function(object, n) {
72100
}
73101
```
74102

75-
Note that it's really important to `return(fail())` here. You wont see the problem when interactively testing your function because when run outside of `test_that()`, `fail()` throws an error, causing the function to terminate early. When running inside of `test_that()` however, `fail()` does not stop execution because we want to collect all failures in a given test.
103+
To make your failure messages as actionable as possible, state both what the object is and what you expected:
76104

77-
### Pass the test
105+
```{r}
106+
#| error: true
107+
expect_vector_length(mean, 10)
108+
expect_vector_length(mtcars, 15)
109+
```
78110

79-
If no assertions fail, call `pass()` with the input value (usually `act$val`). Returning the input value is good practice since expectation functions are called primarily for their side-effects (triggering a failure). This allows expectations to be chained:
111+
### `expect_s3_class()`
112+
113+
As another example, imagine if you're checking to see if an object inherits from an S3 class. In R, there's no direct way to tell if an object is an S3 object: you can confirm that it's an object, then that it's not an S4 object. So you might organise your test this way:
80114

81115
```{r}
82-
mtcars |>
83-
expect_type("list") |>
84-
expect_s3_class("data.frame") |>
85-
expect_length(11)
116+
expect_s3_class <- function(object, class) {
117+
act <- quasi_label(rlang::enquo(object), arg = "object")
118+
119+
if (!is.object(act$val)) {
120+
return(fail(sprintf("%s is not an object.", act$lab)))
121+
}
122+
123+
if (isS4(act$val)) {
124+
return(fail(sprintf("%s is an S4 object, not an S3 object.", act$lab)))
125+
}
126+
127+
if (!inherits(act$val, class)) {
128+
msg <- sprintf(
129+
"%s inherits from %s not %s.",
130+
act$lab,
131+
paste0(class(object), collapse = "/"),
132+
paste0(class, collapse = "/")
133+
)
134+
return(fail(msg))
135+
}
136+
137+
pass(act$val)
138+
}
86139
```
87140

88-
## Testing your expectations
141+
```{r}
142+
#| error: true
143+
x1 <- 1:10
144+
TestClass <- methods::setClass("Test", contains = "integer")
145+
x2 <- TestClass()
146+
x3 <- factor()
147+
148+
expect_s3_class(x1, "integer")
149+
expect_s3_class(x2, "integer")
150+
expect_s3_class(x3, "integer")
151+
```
89152

90-
testthat comes with three expectations designed specifically to test expectations: `expect_success()` and `expect_failure()`:
153+
## Repeated code
91154

92-
* `expect_success()` checks that your expectation emits exactly one success and zero failures.
93-
* `expect_failure()` checks that your expectation emits exactly one failure and zero successes.
94-
* `expect_failure_snapshot()` captures the failure message in a snapshot, making it easier to review if it's useful or not.
155+
As you write more expectations, you might discover repeated code that you want to extract out in to a helper. For example, testthat has `expect_true()`, `expect_false()`, and `expect_null()` which are special cases of `expect_equal()`
95156

96157
```{r}
97-
test_that("expect_length works as expected", {
98-
x <- 1:10
99-
expect_success(expect_length(x, 10))
100-
expect_failure(expect_length(x, 11))
158+
expect_true <- function(object) {
159+
act <- quasi_label(enquo(object))
160+
expect_constant_(act, TRUE, ignore_attr = TRUE)
161+
}
162+
expect_false <- function(object) {
163+
act <- quasi_label(enquo(object))
164+
expect_constant_(act, FALSE, ignore_attr = TRUE)
165+
}
166+
expect_null <- function(object, label = NULL) {
167+
act <- quasi_label(enquo(object))
168+
expect_constant_(act, NULL)
169+
}
101170
102-
expect_snapshot_failure(expect_length(x, 11))
103-
})
171+
expect_constant_ <- function(
172+
act,
173+
constant,
174+
ignore_attr = TRUE,
175+
trace_env = caller_env()
176+
) {
177+
comp <- waldo::compare(
178+
act$val,
179+
constant,
180+
x_arg = "actual",
181+
y_arg = "expected",
182+
ignore_attr = ignore_attr
183+
)
184+
if (length(comp) != 0) {
185+
msg <- sprintf(
186+
"%s is not %s\n\n%s",
187+
act$lab,
188+
format(constant),
189+
paste0(comp, collapse = "\n\n")
190+
)
191+
return(fail(msg, info = info, trace_env = trace_env))
192+
}
193+
194+
pass(act$val)
195+
}
104196
```

0 commit comments

Comments
 (0)