-
Notifications
You must be signed in to change notification settings - Fork 17
/
wholegames-httptest.Rmd
152 lines (106 loc) · 7.71 KB
/
wholegames-httptest.Rmd
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
# Use httptest {#httptest}
In this chapter we aim at adding HTTP testing infrastructure to exemplighratia using httptest.
For this, we start from the initial state of exemplighratia again. Back to square one!
::: {.alert .alert-dismissible .alert-warning}
Note that the `httptest::with_mock_dir()` function is only available in httptest version >= 4.0.0 (released on CRAN on 2021-02-01).
:::
::: {.alert .alert-dismissible .alert-info}
[Corresponding pull request to exemplighratia](https://github.com/ropensci-books/exemplighratia/pull/9/files) Feel free to fork the repository to experiment yourself!
:::
## Setup
Before working on all this, we need to install `{httptest}`.
First, we need to run `httptest::use_httptest()` which has a few effects:
* Adding httptest as a dependency to `DESCRIPTION`, under Suggests just like testthat.
* Creating a setup file under `tests/testthat/setup`,
```r
library(httptest)
```
When testthat runs tests, [files whose name starts with "setup" are always run first](https://testthat.r-lib.org/reference/test_dir.html#special-files).
The setup file added by httptest loads httptest.
We shall tweak it a bit to fool our package into believing there is an API token around in contexts where there is not. Since tests will use recorded responses when we are not recording, we do not need an actual API token when not recording, but we need `gh_organizations()` to not stop because `Sys.getenv("GITHUB_PAT")` returns nothing.
```r
library(httptest)
# for contexts where the package needs to be fooled
# (CRAN, forks)
# this is ok because the package will used recorded responses
# so no need for a real secret
if (!nzchar(Sys.getenv("GITHUB_PAT"))) {
Sys.setenv(GITHUB_PAT = "foobar")
}
```
So this was just setup, now on to adapting our tests!
## Actual testing
The key function will be `httptest::with_mock_dir("dir", {code-block})` which tells httptest to create mock files under `tests/testthat/dir` to store all API responses for API calls occurring in the code block.
We are allowed to tweak the mock files by hand, and we will do that in some cases.
Let's tweak the test file for `gh_status_api`, it becomes
```r
with_mock_dir("gh_api_status", {
test_that("gh_api_status() works", {
testthat::expect_type(gh_api_status(), "character")
testthat::expect_equal(gh_api_status(), "operational")
})
})
```
We only had to wrap the whole test in `httptest::with_mock_dir()`.
If we run this test (in RStudio clicking on "Run test"),
* the first time, httptest creates a mock file under `tests/testthat/gh_api_status/kctbh9vrtdwd.statuspage.io/api/v2/components.json.json` where it stores the API response.
We however dumbed it down by hand, to
```json
{"components":[{"name":"API Requests","status":"operational"}]}
```
* all the times after that, httptest simply uses the mock file instead of actually calling the API.
Let's tweak our other test, of `gh_organizations()`.
Here things get more exciting or complicated, as we also set out to adding a test of the error behavior.
This inspired us to change error behavior a bit with a slightly more specific error message i.e. `httr::stop_for_status(response)` became `httr::stop_for_status(response, task = "get data from the API, oops")`.
The test file ` tests/testthat/test-organizations.R` is now:
```r
with_mock_dir("gh_organizations", {
test_that("gh_organizations works", {
testthat::expect_type(gh_organizations(), "character")
})
})
with_mock_dir("gh_organizations_error", {
test_that("gh_organizations errors if the API doesn't behave", {
testthat::expect_error(gh_organizations())
})
},
simplify = FALSE)
```
The first test is similar to what we did for `gh_api_status()` except we didn't touch the mock file this time, out of laziness.
In the second test there is more to unpack: how do we get a mock file corresponding to an error?
* We first run the test as is. It fails because there is no error, which we expected. Note the `simplify = FALSE` that means the mock file also contains headers for the response.
* We replaced `200L` with `502L` and removed the body, to end up with a very simple mock file under ` tests/testthat/gh_organizations_error/api.github.com/organizations-5377e8.R`
```r
structure(list(url = "https://api.github.com/organizations?since=1",
status_code = 502L, headers = NULL), class = "response")
```
* We re-run the tests. We got the expected error message.
Without the HTTP testing infrastructure, testing for behavior of the package in case of API errors would be more difficult.
Regarding our secret API token, since httptest doesn't save the requests, and since the responses don't contain the token, it is safe without our making any effort.
::: {.alert .alert-dismissible .alert-primary}
In this demo we used `httptest::with_mock_dir()` but there are other ways to use httptest, e.g. using `httptest::with_mock_api()` that does not require naming a directory (you'd still need to use a separate directory for mocking the error response).
Find out more in the [main httptest vignette](https://enpiar.com/r/httptest/articles/httptest.html).
:::
## Also testing for real interactions {#httptest-real}
What if the API responses change?
Hopefully we'd notice that thanks to following API news.
However, sometimes web APIs change without any notice.
Therefore it is important to run tests against the real web service once in a while.
As with vcr we setup a [GitHub Actions workflow](https://github.com/ropensci-books/exemplighratia/blob/otherhttptestapproach/.github/workflows/R-CMD-check-schedule.yaml) that runs once a week with tests against the real web service.
The difference is what and where these tests are.
As some tests with custom made mock files can be more specific (e.g. testing for actual values, whereas the latest responses from the API will have different values), instead of turning off mock files usage, we use our old original tests that we put in a folder called `real-tests`.
Most of the time `real-tests` is .Rbuildignored but in the scheduled run, before checking the package we replace the content of `tests` with `real-tests`.
An alternative would be to use `testthat::test_dir()` on that directory but in case of failures we would not get artifacts as we do with `R CMD check` (at least not without further effort).
Again, one could imagine other strategies, but in all cases it is important to keep checking the package against the real web service fairly regularly.
## Summary
* We set up httptest usage in our package exemplighratia by running `use_httptest()` and tweaking the setup file to fool our own package that needs an API token.
* We wrapped `test_that()` into `httptest::with_mock_dir()` and ran the tests a first time to generate mock files that hold all information about the API responses. In some cases we modified these mock files to make them smaller or to make them correspond to an API error.
Now, how do we make sure this works?
* Turn off wifi, run the tests again. It works! Turn on wifi again.
* Open .Renviron (`usethis::edit_r_environ()`), edit "GITHUB_PAT" into "byeGITHUB_PAT", re-start R, run the tests again. It works! Fix your "GITHUB_PAT" token in .Renviron.
So we now have tests that no longer rely on an internet connection nor on having API credentials.
We also added a continuous integration workflow for having a build using real interactions once every week, as it is important to regularly make sure the package still works against the latest API responses.
For the full list of changes applied to exemplighratia in this chapter, see [the pull request diff on GitHub](https://github.com/ropensci-books/exemplighratia/pull/9/files).
::: {.alert .alert-dismissible .alert-primary}
How do we get there with yet another package? We'll try webfakes but first let's compare vcr and httptest as they both use mocking.
:::