The Http(s) Testing Context For Super-Test Style Assertions. Includes Standard Assertions (get, set, assert), And Allows To Be Extended With JSDocumented Custom Assertions.
npm install @contexts/http
@contexts/http is The Http(s) Testing Context For Super-Test Style Assertions.
- Includes Standard Assertions (get, set, assert) And Cookies Assertions (count, presence, value, attribute).
- Allows To Be Extended With JSDocumented Custom Assertions.
- Supports sessions to carry on cookies between requests automatically.
``sh`
yarn add @contexts/http
- Table Of Contents
- API
- class HttpContext
* start(fn: (req: IncomingMessage, res: ServerResponse), secure: boolean=): Tester
* startPlain(fn: (req: IncomingMessage, res: ServerResponse), secure: boolean=): Tester
* listen(server: http.Server|https.Server): Tester
* debug(on: boolean=)
- Tester
* assert(code: number, body: (string|RegExp|Object)=): Tester
* assert(header: string, value: ?(string|RegExp)): Tester
* assert(assertion: function(Aqt.Return)): Tester
* _rqt.AqtReturn
* set(header: string, value: string): Tester
* post(path: string?, data: string|Object?, options: AqtOptions?): Tester
* postForm(path: string?, cb: async function(Form), options: AqtOptions?): Tester
* session(): Tester
- Extending
- CookiesContext
- Copyright
The package is available by importing its default and named classes. When extending the context, the Tester class is required. The _CookiesContext_ is an extension of the _HttpContext_ that provides assertions for the returned set-cookie header.
`js`
import HttpContext, { Tester } from '@contexts/http'
import CookiesContext, { CookiesTester } from '@contexts/http/cookie'
This testing context is to be used with _Zoroaster Context Testing Framework_. Once it is defined as part of a test suite, it will be available to all inner tests via the arguments. It allows to specify the middleware function to start the server with, and provides an API to send requests, while setting headers, and then assert on the result that came back. It was inspired by supertest, but is asynchronous in nature so that no done has to be called — just the promise needs to be awaited on.
| Using HttpContext Example |
|---|
` /** export default middleware /** |
For example, we might want to test some synchronous middleware. It will check for the authentication token in the headers, reject the request if it is not present, or if the corresponding user is not found, and write the response if everything is OK. |
The tests are written for Zoroaster in such a way that test suite objects are exported. When the context property is found on the test suite, it will be instantiated for all inner tests. The start method will wrap the request listener in try-catch block to send statuses 200 and 500 accordingly (see below). |
` š¦
Executed 3 tests. |
The tests can be run with Zoroaster test runner: zoroaster example/test/spec -a. |
Starts the server with the given request listener function. It will setup an upper layer over the listener to try it and catch any errors in it. If there were errors, the status code will be set to 500 and the response will be ended with the error message. If there was no error, the status code will be set by _Node.JS_ to 200 automatically, if the request listener didn't set it. This is done so that assertion methods can be called inside of the supplied function. If the server needs to be started without the wrapper handler, the startPlain method can be used instead.
When the secure option is passed, the HTTPS server with self-signed keys will be started and process.env.NODE_TLS_REJECT_UNAUTHORIZED will be set to 0 so make sure this context is only used for testing, and not on the production env.
`jsstart
// the handler installed by the method.`
const handler = async (req, res) => {
try {
await fn(req, res)
res.statusCode = 200
} catch (err) {
res.statusCode = 500
res.write(err.message)
if (this._debug) console.error(error.stack)
} finally {
res.end()
}
}
server.start(handler)
| Middleware Constructor Testing Strategy |
|---|
` export default makeMiddleware /** |
We update the source code to export a constructor of middleware, based on the given options. In this case, the middleware will be created with the users object that is scoped within the function, rather that file, so that we can pass it at the point of creating the middleware. |
` class Context { /* @type {Object export default TS /** |
The new tests require implementing a method that will call the middleware constructor prior to continuing with the request. This method is creates as part of a different context, called simply Context. It will help to create the request listener to pass to the start method, where the assertions will be written in another middleware executed after the source code one. |
` example/test/spec/constructor.js > sets the correct name š¦
Executed 4 tests: 1 error. |
We expected the last test to fail because in the assertion method we specified that the user name should be different from the one that was passed in the options to the middleware. Other tests pass because there were no errors in the assertion middleware. It is always required to call assert on the context instance, because simply requesting data with get will not throw anything even if the status code was not 200. |
Starts the server without wrapping the listener in the handler that would set status 200 on success and status 500 on error, and automatically finish the request. This means that the listener must manually do these things. Any uncaught error will result in run-time errors which will be caught by _Zoroaster_'s error handling mechanism outside of the test scope, but ideally they should be dealt with by the developer. If the middleware did not end the request, the test will timeout and the connection will be destroyed by the context to close the request.
| Plain Listener Testing | Wrapper Listener Testing |
|---|---|
` /* @type {Object export default TS | ` /* @type {Object |
| With plain listener testing, the developer can test the function as if it was used on the server without any other middleware, such as error handling or automatic finishing of requests. The listener can also be wrapped in a custom service middleware that will do these admin things to support testing. | |
` example/test/spec/plain > plain > throws an error example/test/spec/plain > plain > does not finish the request š¦
Executed 5 tests: 2 errors. | |
The output shows how tests with listeners that did not handle errors fail, so did the tests with listeners that did not end the request. The handled test suite (on the right above), wraps the plain listener in a middleware that closed the connection and caught errors, setting the status code to 500, therefore all tests passed there. The strategy is similar to the start method, but allows to implement a custom handler. | |
Starts the given server by calling the listen method. This method is used to test apps such as Koa, Express, Connect _etc_, or many middleware chained together, therefore it's a higher level of testing aka integration testing that does not allow to access the response object because no middleware is inserted into the server itself. It only allows to open URLs and assert on the results received by the request library, such as status codes, body and the headers. The server will be closed by the end of each test by the context.
| Server (App) Testing | |
|---|---|
` const app = connect() export default createServer(app) | ` /* @type {Object export default TS |
When a server needs to be tested as a whole of its middleware, the listen method of the HttpContext is used. It allows to start the server on a random port, navigate to pages served by it, and assert on the results. | |
` š¦
Executed 2 tests. | |
| The tests will be run as usual, but if there were any errors, they will be either handled by the server library, or caught by Zoroaster as global errors. Any unended requests will result in the test timing out. | |
Switches on the debugging for the start method, because it catches the error and sets the response to 500, without giving any info about the error. This will log the error that happened during assertions in the request listener. Useful to see at what point the request failed.
| Debugging Errors In Start |
|---|
` |
The debug is called once before the test. When called with false, it will be switched off, but that use case is probably not going to be ever used, since it's just to debug tests. |
` example/test/spec/debug.js > sets the code to 200 š¦
Executed 1 test: 1 error. |
The output normally fails with the error on the status code assertions, since the handler which wraps the request listener in the start methods, catches any errors and sets the response to be of status 500 and the body to the error message. |
` |
The stderr output, on the other hand, will now print the full error stack that lead to the error. |
__Tester__: The instance of a _Tester_ class is returned by the start, startPlain and listen methods. It is used to chain the actions together and extends the promise that should be awaited for during the test. It provides a testing API similar to the _SuperTest_ package, but does not require calling done method, because the _Tester_ class is asynchronous.
Assert on the status code and body. The error message will contain the body if it was present. If the response was in JSON, it will be automatically parses by the request library, and the deep assertion will be performed.
| assert(code, body=) | |
|---|---|
` | ` š¦
Executed 4 tests. |
Assert on the response header. The value must be either a string, regular expression to match the value of the header, or null to assert that the header was not set.
| assert(header, ?value) | |
|---|---|
`
| ` |
` example/test/spec/assert/header-fail.js > header example/test/spec/assert/header-fail.js > header with regexp example/test/spec/assert/header-fail.js > absence of a header š¦
Executed 6 tests: 3 errors. | |
Perform an assertion using the function that will receive the response object which is the result of the request operation with aqt. If the tester was started with start or startPlain methods, it is possible to get the response object from the request listener by calling the getResponse method on the context.
import('http').IncomingHttpHeaders __http.IncomingHttpHeaders__: The hash map of headers that are set by the server (e.g., when accessed via IncomingMessage.headers)
_rqt.AqtReturn: The return type of the function.
| Name | Type | Description |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| __body*__ | !(string \| Object \| Buffer) | The return from the server. In case the json content-type was set by the server, the response will be parsed into an object. If binary option was used for the request, a Buffer will be returned. Otherwise, a string response is returned. |
| __headers*__ | !http.IncomingHttpHeaders | Incoming headers returned by the server. |
| __statusCode*__ | number | The status code returned by the server. |
| __statusMessage*__ | string | The status message set by the server. |
| assert(assertion) | |
|---|---|
` | ` š¦
Executed 2 tests. |
Sets the outgoing headers. Must be called before the get method. It is possible to remember the result of the first request using the assert method by storing it in a variable, and then use it for headers in the second request (see example).
| set(header, value) | |
|---|---|
` | ` |
` š¦
Executed 2 tests. | |
Posts data to the server. By default, a string will be sent with the text/plain _Content-Type_, whereas an object will be encoded as the application/json type, or it can be sent as application/x-www-form-urlencoded data by specifying type: form in options. To send multipart/form-data requests, use the postForm method.
| post(path, data?, options?) | |
|---|---|
` | |
` š¦
Executed 3 tests. |
Creates a form instance, to which data and files can be appended via the supplied callback, and sends the request as multipart/form-data to the server. See the Form interface full documentation.
| postForm(path, cb, options?) | |
|---|---|
` | |
` š¦
Executed 1 test. |
Turns the session mode on. In the session mode, the cookies received from the server will be stored in the internal variable, and sent along with each following request. If the server removed the cookies by setting them to an empty string, or by setting the expiry date to be in the past, they will be removed from the tester and not sent to the server.
This feature can also be switched on by setting session=true on the context itself, so that .session() calls are not required.
Additional cookies can be set using the .set('Cookie', {value}) method, and they will be concatenated to the cookies maintained by the session.
At the moment, only expire property is handled, without the path, or httpOnly directives. This will be added in future versions.
| session() | |
|---|---|
` /* @type {TestSuite} / /* @type {TestSuite} / /* @typedef {import('../context').TestSuite} TestSuite / | |
` š¦
Executed 2 tests. | |
The package was designed to be extended with custom assertions which are easily documented for use in tests. The only thing required is to import the _Tester_ class, and extend it, following a few simple rules.
There are 2 parts of the _@contexts/Http_ software: the context and the tester. The context is used to start the server, remember the response object as well as to destroy the server. The tester is what is returned by the start/startPlain/listen methods, and is used to query the server. To implement the custom assertions with support for JSDoc, the _HttpContext_ needs to be extended to include any private methods that could be used by the tester's assertions, but might not have to be part of the _Tester_ API, and then implement those assertions in the tester by calling the private _addLink method which will add the action to the promise chain, so that the await syntax is available.
| Implementing Custom Assertions For Cookies |
|---|
` /** const pairs = mistmatch(pattern, header, ['name', 'value']) /* @type {{ name: string, value: string }} / for (let i = 0; i < pairs.length; i++) { return cookie return cookies.find(({ name: n }) => { |
The Cookies context should extend the Http context, and set this.TesterConstructor = CookieTester in its constructor, so that the start/startPlain/listen methods of the superclass will construct the appropriate tester. The additional step involved is overriding the start method to update the JSDoc type of the tester returned to CookieTester so that the autocompletion hints are available in tests. Now, additional methods that are required for assertions, can be added to the context. They will be accessible via the this.context in the tester as shown below. The tester itself is accessible via the this.tester, and the AQT response object can be accessed via the this.tester.res property. Finally, the context should implement the reset method which will be called by the super class prior to any additional requests being made, to reset its state. For example, the Cookies context hashes the parsed cookies in the cookies property, which needs to be set to null before further requests, to make sure it's not the hashed values that are tested. |
` /** /** |
The CookiesTester class allows to add the assertions to the tester. To help write assertions, the this.context type need to be updated to the /* @type {import('.').default} / this.context = null in the constructor. Each assertion is documented with standard JSDoc. The assertion method might want to create an erotic object at first, to remember the point of entry to the function, so that the assertion will fail with an error whose stack consists of a single line where the assertion is called. This e object will have to be passed as the second argument to the this._addLink method. The assertion logic, either sync or async must be implemented withing the callback passed to the this_addLink method that will update the chain and execute the assertion in its turn. If the assertion explicitly returns false, no other assertions in the chain will be called. |
Now the CookiesTester methods which are used in tests, will come up with JSDoc documentation. The context must be imported as usual from the context directory, and set up on test suites in the context property. If there are multiple test suites in a file, the export const context = CookieContext would also work without having to specify the context on each individual test suite. The JSDoc enabling line, /* type {Object still needs to be present. |
` example/test/spec/cookie/ > sets cookie for a path š¦
Executed 3 tests: 1 error. |
Because we used erotic, the test will fail at the line of where the assertion method was called. It is useful to remove too much information in errors stacks, and especially for async assertions, which otherwise would have the stack beginning at <anonymous>, and only pointing to the internal lines in the CookiesTester, but not the test suite. |
The _CookiesContext_ provides assertion methods on the set-cookie header returned by the server. It allows to check how many times cookies were set as well as what attributes and values they had.
- count(number): Assert on the number of times the cookie was set.name(string)
- : Assert on the presence of a cookie with the given name. Same as .assert('set-cookie', /name/).value(name, value)
- : Asserts on the value of the cookie.attribute(name, attrib)
- : Asserts on the presence of an attribute in the cookie.attributeAndValue(name, attrib, value)
- : Asserts on the value of the cookie's attribute.noAttribute(name, attrib)
- : Asserts on the absence of an attribute in the cookie.
The context was adapted from the work in https://github.com/pillarjs/cookies. See how the tests are implemented for more info.
Examples:
1. Testing Session Middleware.
`js``
async 'sets the cookie again after a change'({ app, startApp }) {
app.use((ctx) => {
if (ctx.path == '/set') {
ctx.session.message = 'hello'
ctx.status = 204
} else {
ctx.body = ctx.session.message
ctx.session.money = '$$$'
}
})
await startApp()
.get('/set').assert(204)
.count(2)
.get('/').assert(200, 'hello')
.name('koa:sess')
.count(2)
},
alt="Art Deco"> | Ā© Art Deco for Idio 2019 | alt="Tech Nation Visa"> | Tech Nation Visa Sucks |
|---|