Testing Routes
We can test server routes using Vitest, Supertest, and Testing Library.
Setting up
We do this first and foremost by exporting the Express server
object from server.ts
(or wherever it happens to be defined):
// server.tsconst server = express()server.get('/example', (req, res) => {res.send('WOMBAT')})export default server
Importantly, we want to put the server.listen()
call in another file (index.ts
, for example). This allows our testing code to import server from './server.ts'
. If we put the .listen()
call in server.ts
, it will execute when the file is read and we don't actually want the server listening for connections while we're running our tests. This implies that we can tell things about our application without running it at all! Handy.
index.ts
can be pretty simple:
// index.tsimport server from './server'const port = process.env.PORT || 3000server.listen(port, function () {console.log('Server listening on port:', port)})
Writing route tests
By convention, Supertest is usually called from the variable name request
.
import request from 'supertest'import server from '../server.ts'test('/example returns WOMBAT', async () => {// Arrangeconst expected = 'WOMBAT'// Actconst res = await request(server).get('/example')// Assertexpect(res.text).toBe(expected)})
Notice that we await the server request before making our assertions. Supertest will store the server response in res
when it's done requesting the /example
route from our server.
The text/html
content sent back in the response can be found in the res.text
property.
In pseudocode, we might write:
SEND a request to the /example routeAWAIT a responseCOMPARE it with what we expected it to be
We can also check what the HTTP status code was that the server sent back (for example, 200 for OK or 404 for NOT FOUND):
const res = await request(server).get('/example')expect(res.status).toBe(200)
HTML
If we want to test what users will experience in the HTML, we can use testing-library
to query for features we expect to be present in the HTML. Testing library provides queries and assertions that are about the parts of the UI that the users will actually experience, e.g. the text in an element or the label of a form input.
This makes our tests less brittle and more realistic, because we tend to test things that will affect users.
To run these queries, we first need to load the HTML in a simulated DOM environment. We can create a reusable function to help us:
// test-utils.tsimport { within } from '@testing-library/dom'import { JSDOM } from 'jsdom'const render = (response) => {const { document } = new JSDOM(response.text).windowreturn within(document)}export default render
We can then use this within our route test:
// server.test.tstest('contains the word "Welcome"', () => {return request(server).get('/').then((response) => {const screen = render(response)const welcome = screen.getByText(/Welcome/)expect(welcome).toBeInTheDocument()expect(welcome).toBeVisible()})})
Resources
- Testing library intro and guiding principles
- Testing Library queries