@open-draft/test-server
v0.7.1
Published
HTTP/HTTPS testing server for your tests.
Downloads
2,682
Maintainers
Readme
Test Server
Spawns HTTP/HTTPS/WS/WSS local servers for testing.
Motivation
I happened to do a lot of network testing. In my test suites, I often need to perform requests against actual HTTP/WebSocket servers, but need the flexibility to model whatever scenarios I want, including on a per-test basis. Traditional server packages likes express
or fastify
are great but too verbose for this particular use case.
So I've built this utility. I've been using this library for years to help me perfect the test suite for Mock Service Worker. But why build it at all?
- Compact. I can spawn actual servers in tests while keeping the test setup to the minimum.
- Practical. I can configure test servers first, spawn later. This adds a ton of reusability while also keeping my tests clean. I can also use the test servers as disposables, which means they will automatically clean after themselves even if I establish a server within a particular test.
- Universal. This library gives you the means to create test HTTP and WebSocket servers to test the network to your heart's content.
It's also a bit silly that in 2024 some server libraries don't return a connection promise and don't give you any adequate means to get the server's actual URL. Those two are quite essential when testing the network.
This library is built on top of Hono, and you should definitely check it out! For once, it's an incredible environment-agnostic server library. But also, because you can attach existing Hono middleware to your test serves to send them to the moon.
Install
npm install @open-draft/test-server
Usage
HTTP server
import { createTestHttpServer } from '@open-draft/test-server/http'
const server = createTestHttpServer({
defineRoutes(router) {
router.get('/numbers', () => {
return Response.json([1, 2, 3])
})
},
})
beforeAll(async () => {
await server.listen()
})
afterAll(async () => {
await server.close()
})
test('fetches all the numbers', async () => {
const response = await fetch(server.http.url('/numbers'))
expect(response.status).toBe(200)
await expect(response.json()).resolves.toEqual([1, 2, 3])
})
WebSocket server
import { createTestHttpServer } from '@open-draft/test-server/http'
import { createTestWebSocketServer } from '@open-draft/test-server/ws'
const server = createTestHttpServer()
const wss = createTestWebSocketServer({
// Attach the test WebSocket server to an existing HTTP server.
server,
pathname: '/ws',
})
// Running the HTTP server will establish the WebSocket middleware as well.
await server.listen()
console.log('WebSocket server is running at:', wss.ws.url())
wss.ws.on('connect', (socket) => {
console.log('new connection:', socket.id)
})
As disposable utilities
Both the createTestHttpServer()
and createTestWebSocketServer()
utilities are disposable, which means they can be created within function closures (e.g. your test cases) and will be automatically cleaned up once that closure is garbage-collected.
// my.test.js
import { createTestHttpServer } from '@open-draft/test-server/http'
it('fetches a resource by id', async () => {
await using server = createTestHttpServer({
defineRoutes(router) {
router.get('/resource/:id', () => {
return Response.json({ a: 1 })
})
}
})
await fetch(server.http.url('/resource/abc-123'))
})
In the test case above, the test HTTP server will automatically be closed once the test case is done!
The "rooms" you can create in the test HTTP server are also disposable, which allows you to append additional behaviors to an already existing test server instance.
const server = createTestHttpServer()
beforeAll(async () => await server.listen())
afterAll(async () => await server.listen())
it('handles errors when fetching a resource', async () => {
await using room = server.http.createRoom({
defineRoutes(router) {
router.get('/resource', () => {
return new Response(null, { status: 404 })
})
}
})
await fetch(room.url('/resource'))
})
Note that the request references the
/resource
pathname nested under the room (room.url()
).
API
createTestHttpServer([options])
Creates a test HTTP server instance. The server instance is not running until you call server.listen()
options
(optional)protocols
, (optional)Array<'http' | 'htts'
, the list of protocols to spawn corresponding test servers. For example, providing['http', 'https']
will spawn two test servers (HTTP and HTTPS) with the same route handlers.defineRoutes
, (optional)(router: Hono) => void
, a function describing the initial route handlers.
const server = createTestHttpServer({
protocols: ['http', 'https'],
defineRoutes(router) {
router.get('/user', () => {
return Response.json(userFixture)
})
},
})
TestHttpServer
TestHttpServer.listen([options])
options
(optional)hostname
, (optional)string
, the exact hostname to use for this server.
TestHttpServer.close()
Closes the HTTP server, aborting any pending requests.
TestHttpServer.http.url([pathname])
pathname
(optional)string
, a pathname to rebase against the server URL.
const server = createTestHttpServer()
await server.listen()
server.http.url() // "http://localhost:57834"
server.http.url('/resource') // "http://localhost:57834/resource"
TestHttpServer.http.createRoom([options])
Create a new room on this server. Room is a set of route handlers scoped under a random base path.
options
(optional)defineRoutes
, (optional)(router: Hono) => void
, a function describing the routes for this room. Note that all room routes are scoped to the random room base path.
const server = createTestHttpServer()
await server.listen()
const room = server.http.creaetRoom({
defineRouter(router) {
router.get('/resource', () => new Response())
},
})
room.url() // "http://localhost:57834/48e34ef7-13d0-4b6b-862f-4a29475c3396"
await fetch(room.url('/resource'))
Creating rooms with their own route handlers allows you to reuse the same running server instance while imbuing it with different functionality on a per-test basis.
createTestWebSocketServer(options)
options
server
,TestHttpServer
, a reference to the server instance created by calling thecreateTestHttpServer()
utility.pathname
,string
, a pathname of the given HTTPserver
where the WebSocket connection must be established.
TestWebSocketServer.ws.url()
TestWebSocketServer.wss.url()
Returns an absolute URL to this WebSocket connection.
Note that the
.url()
method on the WebSocket server does not accept apathname
. The WebSocket protocol does not define the concept of sub-resources, so there is no point in referencing nested paths that point at nothing.
TestWebSocketServer.on(type, listener)
Adds an event listener to the underlying WebSocket connection.
const wss = createTestWebSocketServer({
server,
pathname: '/ws',
})
wss.ws.on('connection', (socket) => {
// Handle any incoming WebSocket client connections
// to this test WebSocket server under HTTP.
})
TestWebSocketServer.once(type, listener)
Adds a one-time event listener to this WebSocket connection.
TestWebSocketServer.off(type, listener)
Removes the event listener for the given event type from this WebSocket connection.
TestWebSocketServer.close()
Closes the WebSocket server, closing any active clients.
[!IMPORTANT] Note that closing the WebSocket server does not close the parent HTTP server. Closing the parent HTTP server, however, will automatically close any attached WebSocket servers.