-
Notifications
You must be signed in to change notification settings - Fork 32
/
testing.tex
397 lines (322 loc) · 11.7 KB
/
testing.tex
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
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
\chapter{Testing}\label{s:testing}
We are bad people,
because we have been writing code without testing it.
In this lesson we will redeem ourselves (partially)
by correcting that (also partially).
JavaScript uses the same pattern for \gref{g:unit-test}{unit testing} as most other modern languages.
Each test is written as a function,
and each of those functions tests one particular aspect of the code.
A standalone program called a \gref{g:test-runner}{test runner}
finds test functions,
runs them,
and reports the results.
Any setup code that needs to be run before each test
to create the data for the test's input (called its \gref{g:fixture}{fixture})
is put in a function of its own.
Similarly (but less frequently),
if some teardown needs to be done \emph{after} each test,
we put those operations in a function as well.
Each unit test can have one of three results:
\begin{itemize}
\item
pass: everything worked,
\item
fail: the system being tested didn't do what was expected, or
\item
error: something went wrong with the test itself.
\end{itemize}
We can combine tests into \grefdex{g:test-suite}{test suites}{test suite}
(and test suites into larger suites, and so on)
so that we can more easily run related sets of tests.
This makes testing during development faster,
which in turn makes it more likely that we'll actually do it.
Finally,
we write the tests themselves using \grefdex{g:assertion}{assertions}{assertion}:
statements that check whether or not some condition holds
and generate an error if it doesn't.
Node provides an \texttt{assert} library with some useful functions for asserting various things;
we'll explore this as we go along.
\section{Introducing Mocha}\label{s:testing-mocha}
JavaScript has almost as many testing libraries as it has front-end frameworks.
We will use one called \hreffoot{https://mochajs.org/}{Mocha}\index{Mocha}
that is popular and well documented.
Unlike the libraries we have seen before,
we don't import anything to use it;
instead,
\emph{it} imports \emph{our} code and calls our functions.
When we're writing tests for Mocha to run,
we use a function called \texttt{describe} to create a group of tests
and another function called \texttt{it} for each test in that group:
\begin{minted}{js}
describe('first test', () => {
it('should run without errors', (done) => {
done()
})
})
\end{minted}
\noindent
As this example shows,
\texttt{describe}'s arguments are an explanatory string and a callback function.
That callback makes calls to \texttt{it}, which takes:
\begin{itemize}
\item
another explanatory string describing how the system should behave, and
\item
a callback that receives a function (called \texttt{done} by convention).
\end{itemize}
\noindent
(The name \texttt{it} was chosen so that when we read tests aloud,
it sounds like we're saying, ``It should do this or that.'')
At the end of each test we call \texttt{done} to signal that it has finished.
We have to do this because callbacks run out of order,
so Mocha needs to know when each one completes.
We can run our tests from the shell by invoking \texttt{mocha}
and giving it the path to the file that contains the tests:
\begin{minted}{shell}
$ ./node_modules/.bin/mocha path/to/test.js
\end{minted}
\begin{minted}{text}
first test
+ should run without errors
1 passing (12ms)
\end{minted}
As with bundling,
we normally put the \texttt{mocha} command in \texttt{package.json}
so that \texttt{./node\_modules/.bin} is automatically included in the path.
After we add this:
\begin{minted}{js}
{
...
"scripts": {
...
"test": "mocha",
...
}
}
\end{minted}
\noindent
to \texttt{package.json}, our command becomes:
\begin{minted}{shell}
$ npm test -- path/to/test.js
\end{minted}
\noindent
(Again, everything after \texttt{-\/-} is passed to the script itself.)
If we don't specify where to find tests,
Mocha looks for a directory called \texttt{test}
and runs the files it finds there whose names begin with \texttt{test}
(\figref{f:testing-mocha}).
\figpdf{figures/testing-mocha.pdf}{Mocha in Action}{f:testing-mocha}
\section{Refactoring}\label{s:testing-refactoring}
The next step is to \gref{g:refactor}{refactor} our software to make it testable.
In the case of our server,
we have to:
\begin{itemize}
\item
move the code that listens on a port into one file, and
\item
have that import a file that contains the code to do everything else.
\end{itemize}
Once we have done this,
we can run the server code in other contexts.
Here's the file \texttt{standalone.js} that actually launches a server:
\begin{minted}{js}
const server = require('./server')
const PORT = 3418
server.listen(PORT,
() => { console.log(`listening on port ${PORT}...`) })
\end{minted}
\noindent
And here is the application code we've extracted into \texttt{server.js}
so that we can test it:
\begin{minted}{js}
const express = require('express')
// Main server object.
let app = express()
// Root page.
app.get('/', (req, res, next) => {
res.status(200).send('<html><body><h1>Home</h1></body></html>')
})
// ...as before...
module.exports = app
\end{minted}
Before going any further,
we check that we haven't broken anything by running:
\begin{minted}{shell}
$ node standalone.js
\end{minted}
\section{Testing the Server}\label{s:testing-server}
All right:
now that we have extracted the code that's specific to our server,
how do we test it?
The answer is yet another library called \hreffoot{https://github.com/visionmedia/supertest}{supertest}\index{supertest}
that sends fake HTTP requests to an Express server
that has been split in the way we just split ours
and lets us interact with that server's replies.
\figpdf{figures/testing-supertest.pdf}{Supertest versus Reality}{f:testing-supertest}
Here's a test of a simple request that uses Mocha to manage the test,
and supertest's \texttt{request} function to send something to the server
and check the result:
\begin{minted}{js}
const assert = require('assert')
const request = require('supertest')
const server = require('./server')
describe('server', () => {
it('should return HTML with expected title', (done) => {
request(server)
.get('/')
.expect(200)
.expect('Content-Type', /html/)
.end((err, res) => {
assert(res.text.includes('Home'), 'Has expected title')
done()
})
})
})
\end{minted}
Going through this line by line:
\begin{enumerate}
\item
\texttt{request(server)} starts building up a request to send.
\item
\texttt{.get('/')} specifies the path in the URL we are sending.
\item
\texttt{.expect(200)} checks that the return code is 200 (OK).
\item
\texttt{.expect('Content-Type',\ /html/)}
checks the content type in the returned value against
a \gref{g:regular-expression}{regular expression} (\appref{s:regexp}).
\item
\texttt{.end} is called when the whole response has been received,
i.e., when we can start looking at the content of the page or data
that the server has sent.
\item
Inside the callback to \texttt{end},
\texttt{res} is the result data.
We make sure that its text (i.e., \texttt{res.text}) includes the word ``Home''.
We really should check \texttt{err} here to make sure everything worked properly,
but we're not quite that virtuous.
\item
Finally, we call \texttt{done()} to signal the end of the test.
Again,
we have to do this because there's no way Mocha can know
when the enclosing \texttt{.end(...)} will be called
\end{enumerate}
Let's run our code:
\begin{minted}{text}
server
+ should return HTML with expected title (48ms)
1 passing (58ms)
\end{minted}
\noindent
Excellent: let's add some more tests.
\begin{minted}{js}
describe('server', () => {
it('should return HTML with expected title', (done) => {
// ...as before...
})
it('should return page as HTML with expected title', (done) => {
request(server)
.get('/asteroids')
.expect(200)
.expect('Content-Type', /html/)
.end((err, res) => {
assert(res.text.includes('Asteroids'), 'Has expected title')
done()
})
})
it('should 404 for other pages', (done) => {
request(server)
.expect(404)
.get('/other')
.end((err, res) => {
assert(res.text.includes('ERROR'), 'Has expected error message')
done()
})
})
})
\end{minted}
\begin{minted}{text}
server
+ should return HTML with expected title (42ms)
+ should return asteroids page as HTML with expected title
+ should 404 for other pages
3 passing (62ms)
\end{minted}
Notice that we check to make sure that the server sends a 404
when we ask for something that doesn't exist.
Making sure the system fails gracefully is just as important
as making sure that it provides data when asked to.
\section{Checking the HTML}\label{s:testing-html}
It's increasingly common for servers to return data that is rendered by the client
rather than generating and returning HTML,
but some servers still do the latter.
Do \emph{not} try to check this with substrings or regular expressions:
the exceptions have exceptions,
and \hreffoot{https://stackoverflow.com/a/1732454}{that way lies madness}.
Instead,
we should parse the HTML to create a structure in memory and check that;
if parsing fails because the HTML is badly formatted, that's worth knowing too.
The structure in question is our new friend the DOM,
and to get it,
we will use (yet another) library called \hreffoot{https://cheerio.js.org/}{cheerio}.\index{cheerio}
\texttt{cheerio.load} turns HTML text into a DOM tree;
the resulting object can be used like a function,
and we can use the same selectors we met previously to find things in it.
Here's our test:
\begin{minted}{js}
const assert = require('assert')
const request = require('supertest')
const cheerio = require('cheerio')
const server = require('./server')
describe('server', () => {
it('should have the correct headings', (done) => {
request(server)
.get('/')
.expect('Content-Type', /html/)
.expect(200)
.end((err, res) => {
const tree = cheerio.load(res.text)
assert.equal(tree('h1').length, 1, 'Correct number of headings')
assert.equal(tree('h1').text(), 'Home', 'Correct heading text')
done()
})
})
})
\end{minted}
\begin{minted}{text}
server
+ should have the correct headings (67ms)
1 passing (77ms)
\end{minted}
This gets the page as before,
parses it,
then looks for \texttt{h1} elements and checks the text of the first one.
(Note that this \emph{doesn't} check if the title is \texttt{\htmltag{em}H\htmltag{/em}ome}
because \texttt{.text()} concatenates all the text of the children.)
We won't explore this approach further because we're going to serve data for rendering
rather than generating HTML and sending that,
but if you're doing any web scraping at all,
libraries like \texttt{cheerio} are there to help.
\section{Exercises}\label{s:testing-exercises}
\exercise{Not Done}
What happens if we forget to call \texttt{done()} in a test?
\exercise{Adding Tests}
\begin{enumerate}
\item
What is the most useful test you could add for the asteroids application?
Why?
\item
Implement it.
\item
Ask yourself why tutorials like this one don't say ``\emph{please} implement it''.
Reflect on the fact that this question didn't say ``please'' either.
Are you comfortable with the paternalistic power relationship embodied in the absence of that one little word,
and with the somewhat uncomfortable attempt at ironic humor embodied in this question?
\end{enumerate}
\exercise{Lifecycle}
Suppose a JavaScript program contains some JSX expressions that produce HTML
which is then read and displayed by a browser.
Draw a diagram to show the form taken by an H1 heading containing the word ``data''
from start to finish.
\section*{Key Points}
\input{keypoints/testing}