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.ts
const 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.ts
import server from './server'
const port = process.env.PORT || 3000
server.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 () => {
// Arrange
const expected = 'WOMBAT'
// Act
const res = await request(server).get('/example')
// Assert
expect(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 route
AWAIT a response
COMPARE 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.ts
import { within } from '@testing-library/dom'
import { JSDOM } from 'jsdom'
const render = (response) => {
const { document } = new JSDOM(response.text).window
return within(document)
}
export default render

We can then use this within our route test:

// server.test.ts
test('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