lb-declarative-e2e-test

  • API
  • Automation
  • e2e
  • js
  • Loopback
  • Mocha
  • Node
  • NPM
  • Rest
  • Strongloop
  • Supertest
  • Test
NPM version Build Status Coverage Status MIT License

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 read Sends an anonymous GET request and tests the response status is 401
  • admin CAN read data Sends a first requests to the default login endpoint to authenticate the user. Then sends an authenticated GET request and tests the response status is 200

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 the Authorization header 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 to supertest.expect(), it can be used to test:
    // 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.headers and expect.body are passed to supertest.expect()
    {
      // ...
      expect: {
        headers:{
          status: 200,
          'Content-Type': /json/
        },
        body: {
          foo: 'bar'
        }
      }
    }
    

    Note: status and Status-Code are passed without the key to supertest.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