lb-declarative-e2e-test
lb-declarative-e2e-test allows to write tests for Loopback.io in a object definition style.
{
name: 'admin CAN create',
verb: 'post',
auth: usersCredentials.admin,
body: {some: 'value'},
url: '/some/url/',
expect: 200
}
It combines and exposes API from Mocha and supertest. The test generation logic has been moved to declarative-test-structure-generator, check it out for the full API doc.
Latest feature!
The steps option was added in the latest release and allows to perform multiple requests in a single test, read more…
Demo
A demo example is available on Github.
Motivations
The main motivation was to reduce the boilerplate code for every e2e tests.
In the case of a simple GET request, the test is very concise. However, as soon as the request requires authentication, a first post request is required.
In the past, I abstracted this logic in separate functions and the complexity increased. Today I hope lb-declarative-e2e-test will help reduce the boilerplate code of many developers.
Issues
Please share your feedback and report the encountered issues on the project’s issues page.
Installation
npm install --save-dev lb-declarative-e2e-test
# or
npm i -D lb-declarative-e2e-test
Basics
const lbe2e = require('lb-declarative-e2e-test');
const server = require('../server');
lbe2e(server, {
'Read access': {
tests: [
{
name: 'unauthenticated CANNOT read',
verb: 'get',
url: '/some/url/',
expect: 401
},
{
name: 'admin CAN read data',
verb: 'get',
auth: {email: 'admin@test.server.com', password: 'test.admin'},
url: '/some/url/',
expect: 200
}
]
}
});
This code defines a test suite Read access and two test cases
unauthenticated CANNOT readSends an anonymousGETrequest and tests the response status is401admin CAN read dataSends a first requests to the default login endpoint to authenticate the user. Then sends an authenticatedGETrequest and tests the response status is200
From here, read the test suite definition and the test definition.
Definitions
Test suite definition
It extends the definition from declarative-test-structure-generator => test-suite-definition, accepts the following:
{
skip: {boolean}
only: {boolean}
before: {function | Array[function]}
beforeEach: {function | Array[function]}
after: {function | Array[function]}
afterEach: {function | Array[function]}
tests: {Array[TestDefinition] | Object<string, TestSuiteDefinition>}
}
See the full test suite definition API for more details.
Test definition
It extends the definition from declarative-test-structure-generator => test-definition, accepts the following:
{
name: {string}
skip: {boolean}
only: {boolean}
url: {string | function}
verb: {string}
headers: {Object}
auth: {string | Object | Array[string | Object] | function}
body: {Object | function | *}
expect: {Object | *}
error: {function}
}
Example: On the example below, userModels can be set during a before or beforeEach hook.
{
name: 'user CAN read his OWN details',
verb: 'get',
url: () => `/api/users/${userModels[0].id}`,
auth: () => ({email: userModels[0].email, password: userModels[0].password}),
expect: 200
},
name, skip and only: see the full test definition API for more details.
Url
The tested url can be passed as a string or as a callback function returning a string.
The callback value is only evaluated when configuring the request (after before / beforeEach hooks).
Verb
The verb / HTTP method to use for the request.
All verbs supported by supertest are supported, e.g. get, post, put, patch, delete, …
Headers
The headers is an Object mapping the key-value pairs. The pairs are merged over the headers in the global config
Auth
The auth should be used for authenticated requests. Custom login endpoint can be configured in the global config.
The following options are supported:
string: provides the tokenId to use for the request. It is used directly on theAuthorizationheader and the request is sent without prior login.Object: provides the credentials to use for the request (the Object provided is sent as is).Array[string|Object]: An array of any of the above.function => string|Object|Array[string|Object]: A callback returning any of the above (lazy evaluated value).
Body
Object: an object serialized to JSON before being sent.function: a callback returning body.- anything supported by supertest (which is based on superagent).
The value or callback value of body is passed directly to supertest request.send.
Expect
- The value
{*}is passed directly tosupertest.expect(), it can be used to test:- HTTP status code
- body
- response with a custom test function.
// check the HTTP status { // ... expect: 200 } // check the exact value of the body { // ... expect: {foo: 'bar'} } // callback with response { // ... expect: response=>{ /* test response */ } } - Combine multiple tests in one. Each key-value pairs in
expect.headersandexpect.bodyare passed tosupertest.expect(){ // ... expect: { headers:{ status: 200, 'Content-Type': /json/ }, body: { foo: 'bar' } } }Note:
statusandStatus-Codeare passed without the key tosupertest.expect(value) - Lazy evaluation for
expect.body. Only evaluates the value when performing the test{ // ... expect: { body: () => ({foo: 'bar'}) } }
Steps
Sometimes it is not easy to test something with only one request. The option steps allows to perform multiple requests in a single test.
All the options from the test definition are inherited in the step definition. On the example below, auth is inherited by the steps.
{
name: 'access token should be voided after logout',
auth: () => tokenId,
steps: [
{
url: '/api/users/logout',
verb: 'post',
expect: 204
},
{
verb: 'get',
url: () => `/api/users/${userModels[0].id}`,
expect: 401
}
]
}
The step definition can also be lazy evaluated. It is particularly useful when a step needs an information from the previous step.
{
name: 'some test with lazy evaluated step definition',
steps: [
{
url: '/api/users/',
verb: 'post',
body: factory(),
expect: 200
},
step0Response => {
return {
url: `/api/users/${step0Response.body.id}?filter[include]=scores`,
verb: 'get',
expect: resp => {
expect(resp.body).to.deep.match(expectedBody);
}
};
}
]
}
Note: deep match part of chai-deep-match plugin.
Error
The error is an optional callback. When provided, it will be called with the test error and the request’s response object.
See Debug a failed test for more details.
Global config definition
All the config below are optional, see how to specify a global config object.
{
baseUrl: 'base/url/v1',
headers: {
'Accept': 'application/json',
'Accept-Language': 'en-US'
},
auth: {
url: '/CustomUserModel/login/'
},
expect: {
headers: {
'content-encoding': 'gzip',
'x-frame-options': 'DENY'
}
},
error: err => {
console.error(err);
}
}
The baseUrl is prepended to the test url.
The headers is merged with the headers defined in the test definition. The test definition headers takes precedence over the global config headers.
The auth.url configures the login endpoint for the authenticated requests, defaults to /api/users/login.
IMPORTANT: auth.url should be specified when the LB app extends the built-in User model.
The expect.headers is merged with the expect.headers defined in the test definition. The test definition expect.headers takes precedence over the global config expect.headers.
The error is an optional callback, here it is configured for all tests. When provided, it will be called with the test error and the request’s response object.
Advanced usage
Test suites definition structure
The test definition structure is not limited to a single level. As in Mocha, there is no limit to the amount of nesting.
See declarative-test-structure-generator => Test suites definition structure
Specify a global config object
lbe2e accepts 2 or 3 arguments:
lbe2e(server, testsSuite);
// or
lbe2e(server, testConfig, testsSuite);
Test hooks
It is possible to run one or many function at different phase of the test. See declarative-test-structure-generator => Test hooks
TIP: Use the hook feature when you need to set some test data before the tests. See Mocha: Asynchronous hooks
Run only / skip
See declarative-test-structure-generator => Run only / skip
Testing same request with multiple users
It is possible to test the same request with a batch of users.
{
// ...
auth: [
'user-a-token-id',
{username: 'user-b', password: 'user-b-pass'},
{email: 'user-c@app.com', password: 'user-c-pass'}
]
}
See the auth in the test definition.
TIP: It is a convenient way to test negative cases for ACL.
Debug a failed test
It is possible to register an error callback for when the test fails. It could be either in the general config or on the test definition.
{
// ...
error: err => {
console.log(err);
}
}
This callback will be called with the following object:
{
error: {Error},
response: {Response}
}
View the test logging data
lb-declarative-e2e-test uses debug to log information during the tests.
You can view these logs by setting the DEBUG env variable to lb-declarative-e2e-test.
DEBUG=lb-declarative-e2e-test npm test