Package Exports
- superwstest
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (superwstest) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
SuperWSTest
Extends supertest with WebSocket capabilities. This is intended for testing servers which support both HTTP and WebSocket requests.
Install dependency
npm install --save-dev superwstestUsage
Example server implementation
import http from 'http';
import WebSocket from 'ws';
const server = http.createServer();
const wss = new WebSocket.Server({ server });
wss.on('connection', (ws) => {
ws.on('message', (message) => { ws.send(`echo ${message}`); });
ws.send('hello');
});
export default server;Tests for example server
import request from 'superwstest';
import server from './myServer';
describe('My Server', () => {
beforeEach((done) => {
server.listen(0, 'localhost', done);
});
afterEach((done) => {
server.close(done);
});
it('communicates via websockets', async () => {
await request(server)
.ws('/path/ws')
.expectText('hello')
.sendText('foo')
.expectText('echo foo')
.sendText('abc')
.expectText('echo abc')
.close()
.expectClosed();
});
});Since this builds on supertest, all the HTTP checks are also available.
As long as you add server.close in an afterEach, all connections
will be closed automatically, so you do not need to close connections
in every test.
Testing a remote webserver
You can also test against a remote webserver by specifying the URL of the server:
import request from 'superwstest';
describe('My Remote Server', () => {
afterEach(() => {
request.closeAll(); // recommended when using remote servers
});
it('communicates via websockets', async () => {
await request('https://example.com')
.ws('/path/ws')
.expectText('hello')
.close();
});
});Note that adding request.closeAll() to an afterEach will
ensure connections are closed in all situations (including test
timeouts, etc.). This is not needed when testing against a local
server because the server will close connections when closed.
The server URL given should be http(s) rather than ws(s); this will
provide compatibility with native supertest requests such as post,
get, etc. and will be converted automatically as needed.
API
- request(server[, options])
- request(...).ws(path[, protocols][, options])
- .set(header, value)
- .unset(header)
- [.expectText([expected[, options]])](#expecttextexpected-options)
- [.expectJson([expected[, options]])](#expectjsonexpected-options)
- [.expectBinary([expected[, options]])](#expectbinaryexpected-options)
- .sendText(text)
- .sendJson(json)
- .sendBinary(data)
- .send(data[, options])
- [.close([code[, reason]]](#closecode-reason)
- [.expectClosed([expectedCode[, expectedReason]])](#expectclosedexpectedcode-expectedreason)
- .expectConnectionError([expectedStatusCode])
- .expectUpgrade(test)
- .wait(milliseconds)
- .exec(fn)
request(server[, options])
The beginning of a superwstest
(or supertest) test chain.
Typically this is immediately followed by .ws(...) or .get(...) etc.
options can contain additional configuration:
shutdownDelay: wait up to the given number of milliseconds for connections to close by themselves before forcing a shutdown whencloseis called on the server. By default this is 0 (i.e. all connections are closed immediately). Has no effect when testing remote servers.request(server, { shutdownDelay: 500 }).ws(path)
defaultExpectOptions: a set of options which are passed to allexpect*calls in the current chain (e.g. allows setting a timeout for all expectations in the chain):request(server, { defaultExpectOptions: { timeout: 5000 } }) .ws(path) .expectText('hello') // implicit { timeout: 5000 } .expectText('hi', { timeout: 9000 }) // overrides default
request(server).ws(path[, protocols][, options])
Returns a Promise (eventually returning the WebSocket) with
additional fluent API methods attached (described below).
Internally, this uses ws, and the
protocols and options given are passed directly to the
WebSocket constructor.
For example, one way to set a cookie:
request(myServer)
.ws('/path/ws', { headers: { cookie: 'foo=bar' } })(you can also use .set('Cookie', 'foo=bar') to set cookies)
.set(header, value)
Sets the header-value pair on the initial WebSocket connection. This can also be called with an object to set multiple headers at once.
request(server).ws('...')
.set('Cookie', 'foo=bar')
.set({ 'Authorization': 'bearer foo', 'X-Foo': 'bar' })This function cannot be called after the connection has been established
(i.e. after calling send or expect*).
.unset(header)
Removes the header from the initial WebSocket connection.
request(server).ws('...')
.unset('Cookie')This function cannot be called after the connection has been established
(i.e. after calling send or expect*).
.expectText([expected[, options]])
Waits for the next message to arrive then checks that it matches the given text (exact match), regular expression, or function. If no parameter is given, this only checks that the message is text (not binary).
request(server).ws('...')
.expectText('hello') // exact text
.expectText(/^hel*o$/) // RegExp matching
.expectText((actual) => actual.includes('lo')) // function
.expectText() // just check message is textWhen using a function, the check will be considered a failure if it
returns false. Any other value (including undefined and null)
is considered a pass. This means you can use (e.g.) Jest expectations
(returning no value):
request(server).ws('...')
.expectText((actual) => {
expect(actual).toContain('foo');
})A second parameter can be given with additional options:
timeout: wait up to the given number of milliseconds for a message to arrive before failing the test (defaults to infinity).request(server).ws('...') .expectText('hello', { timeout: 1000 }) .expectText(undefined, { timeout: 1000 })
Note that for the most reliable tests, it is recommended to stick with the default (infinite) timeout. This option is provided as an escape hatch when writing long flow tests where the test timeout is unreasonably large for detecting an early failure.
These options can also be configured for the whole chain in the request call.
.expectJson([expected[, options]])
Waits for the next message to arrive, deserialises it using JSON.parse,
then checks that it matches the given data
(deep equality)
or function.
If no parameter is given, this only checks that the message is valid JSON.
request(server).ws('...')
.expectJson({ foo: 'bar', zig: ['zag'] }) // exact match
.expectJson((actual) => (actual.foo === 'bar')) // function
.expectJson() // just check message is valid JSONWhen using a function, the check will be considered a failure if it
returns false. Any other value (including undefined and null)
is considered a pass. This means you can use (e.g.) Jest expectations
(returning no value):
request(server).ws('...')
.expectJson((actual) => {
expect(actual.bar).toBeGreaterThan(2);
})A second parameter can be given with additional options:
timeout: wait up to the given number of milliseconds for a message to arrive before failing the test (defaults to infinity).request(server).ws('...') .expectJson({ foo: 'bar' }, { timeout: 1000 }) .expectJson(undefined, { timeout: 1000 })
Note that for the most reliable tests, it is recommended to stick with the default (infinite) timeout. This option is provided as an escape hatch when writing long flow tests where the test timeout is unreasonably large for detecting an early failure.
These options can also be configured for the whole chain in the request call.
.expectBinary([expected[, options]])
Waits for the next message to arrive then checks that it matches the given array / buffer (exact match) or function. If no parameter is given, this only checks that the message is binary (not text).
When providing a function, the data will always be a Uint8Array.
request(server).ws('...')
.expectBinary([10, 20, 30])
.expectBinary(new Uint8Array([10, 20, 30]))
.expectBinary((actual) => (actual[0] === 10)) // function
.expectBinary() // just check message is binaryWhen using a function, the check will be considered a failure if it
returns false. Any other value (including undefined and null)
is considered a pass. This means you can use (e.g.) Jest expectations
(returning no value):
request(server).ws('...')
.expectBinary((actual) => {
expect(actual[0]).toBeGreaterThan(2);
})A second parameter can be given with additional options:
timeout: wait up to the given number of milliseconds for a message to arrive before failing the test (defaults to infinity).request(server).ws('...') .expectBinary([10, 20, 30], { timeout: 1000 }) .expectBinary(undefined, { timeout: 1000 })
Note that for the most reliable tests, it is recommended to stick with the default (infinite) timeout. This option is provided as an escape hatch when writing long flow tests where the test timeout is unreasonably large for detecting an early failure.
These options can also be configured for the whole chain in the request call.
.sendText(text)
Sends the given text. Non-strings are converted using String before
sending.
request(server).ws('...')
.sendText('yo').sendJson(json)
Sends the given JSON as text using JSON.stringify.
request(server).ws('...')
.sendJson({ foo: 'bar' }).sendBinary(data)
Sends the given data as a binary message.
request(server).ws('...')
.sendBinary([10, 20, 30])
.sendBinary(new Uint8Array([10, 20, 30])).send(data[, options])
Sends a raw message (accepts any types accepted by
WebSocket.send,
and options is passed through unchanged).
request(server).ws('...')
.send(new Uint8Array([5, 20, 100])) // binary message
// multipart message
.send('this is a fragm', { fin: false })
.send('ented message', { fin: true }).close([code[, reason]])
Closes the socket. Arguments are passed directly to
WebSocket.close.
request(server).ws('...')
.close() // close with default code and reason
request(server).ws('...')
.close(1001) // custom code
request(server).ws('...')
.close(1001, 'getting a cup of tea') // custom code and reason.expectClosed([expectedCode[, expectedReason]])
Waits for the socket to be closed. Optionally checks if it was closed with the expected code and reason.
request(server).ws('...')
.expectClosed()
request(server).ws('...')
.expectClosed(1001) // expected code
request(server).ws('...')
.expectClosed(1001, 'bye') // expected code and reason.expectConnectionError([expectedStatusCode])
Expect the initial connection handshake to fail. Optionally checks for a specific HTTP status code.
note: if you use this, it must be the only invocation in the chain
request(server).ws('...')
.expectConnectionError(); // any error
request(server).ws('...')
.expectConnectionError(404); // specific error code
request(server).ws('...')
.expectConnectionError('Server sent an invalid subprotocol'); // specific error message.expectUpgrade(test)
Run a check against the Upgrade response. Useful for making arbitrary assertions about parts of the Upgrade response, such as headers.
The check will be considered a failure if it returns false. Any other
value (including undefined and null) is considered a pass.
This means you can use (e.g.) Jest expectations (returning no value).
The parameter will be a
http.IncomingMessage.
request(server).ws('...')
.expectUpgrade((res) => (res.headers['set-cookie'] === 'foo=bar'));
request(server).ws('...')
.expectUpgrade((res) => {
expect(res.headers).toHaveProperty('set-cookie', 'foo=bar');
}).wait(milliseconds)
Adds a delay of a number of milliseconds using setTimeout. This is
available as an escape hatch, but try to avoid using it, as it may
cause intermittent failures in tests due to timing variations.
request(server).ws('...')
.wait(500).exec(fn)
Invokes the given function. If the function returns a promise, this waits for the promise to resolve (but ignores the result). The function will be given the WebSocket as a parameter. This is available as an escape hatch if the standard functions do not meet your needs.
request(server).ws('...')
.exec((ws) => console.log('hello debugger!'))note: this differs from Promise.then because you can continue to
chain web socket actions and expectations.
FAQ
My server is closing the connection immediately with code 1002
Your server is probably trying to indicate that you need to specify a particular sub-protocol when connecting:
request(myServer)
.ws('/path/ws', 'my-protocol-here')You will need to check the documentation for the server library you are using to find out which subprotocol is needed. If multiple sub-protocols are needed, you can provide an array of strings.
Why isn't request(app) supported?
This project aims to be API-compatible with supertest wherever possible,
but does not support the ability to pass an express app directly into
request() (instead, the server must be started in advance and the server
object passed in). The recommended approach is:
let server;
beforeEach((done) => {
server = app.listen(0, 'localhost', done);
});
afterEach((done) => {
server.close(done);
});There are several reasons for not supporting this feature:
supertest's implementation has a bug where it does not wait for the server to start before making requests. This can lead to flakey tests. For this reason it seems beneficial to discourage this approach in general (for both WebSocket and non-WebSocket tests).- It is not possible for this library to reliably know when a test has ended,
so it is not obvious when the auto-started server should be closed.
supertestnever closes these auto-started servers (leading to a large number of servers being spawned during a test run), but even this approach is not viable for WebSocket testing (typical web requests are short-lived, but websockets are long-lived and any dangling connections will prevent the test process from terminating).