0.6.0 • Published 3 years ago

parkes-api-test v0.6.0

Weekly downloads
43
License
JWL
Repository
github
Last release
3 years ago

Concise JSON API tests for node 7.6.0+

Describe API (originally parkes-api-test) was built for the Parkes framework, but has no dependencies on parkes or koa so was renamed to express that you can use it to test any node server that is supported by supertest.

Why?

In environments that are pressed for time (and what development environment isn't?), it's easy for tests to get put off.

Parkes API Test allows you to quickly build out some e2e tests that at least verify that critical pathways in your API are responding with 200 and the key attributes you'd expect.

The framework is designed to get tests up in < 5 minutes for most common cases so that developers can focus on delivering new features.

Getting started

Parkes API Test requires node v7.6.0 or higher for ES2015 and async function support. The tests will run in mocha or jest.

npm install --save-dev parkes-api-test

Then in your test/api/test.js

const app = require('../../app.js');
const describeApi = require('../');

const person = { name: 'Harvey Milk', state: 'California' };

// Use route modifiers to define things you set on (almost) every route
// Set custom variables on your routes by prefixing them with $
describeApi.addModifier((route) => {
  if (route.$user) route.bearer = getTokenFor(route.$user);
})

describe('my api', () => {
  const getServer = describeApi.autorun(app);

  describeApi({ server: getServer, routeModifier: setUser, routes: [
    // GET /health, expect status == 200
    { path: '/health' },
    // POST /people with person body, response should contain { state: 'California' }
    { method: 'POST', path: '/people', body: person, expect: { state: 'California' } },
    // GET /people/1 response should contain { name: 'Harvey Milk' }
    { path: '/people/1', expect: { name: 'Harvey Milk' } },
    // GET /admin should return 403 error, (logged out) will be appended to test name
    { path: '/admin', note: '(logged out)', status: 403 },
    // GET /admin and send auth header, expect status == 200
    { path: '/admin', bearer: token } },
    // GET /sitemap send a custom header
    { path: '/sitemap', headers: { client: 'I am a robot' } },

    // The route modifier added above will set bearer attribute on the route to Bob's token
    // which will in turn cause describeApi to pass the header Authorization: Bearer <bob's token>
    { path: '/me', $user: 'Bob' },
  ] });
});

describe('RESTful api', () => {
  describeApi(getServer, '/people', [
    { }, // GET /people
    { method: 'POST', body: person }, // POST /people
    { method: 'PUT', path: '/1' },    // PUT /people/1
  ],
  () => {
    context('WHEN not authorised', () => {
      // NOTE nested calls to describeApi do not require getServer to
      // be passed in again and use the path prefix from above

      // assert GET /people returns 403 error
      describeApi([ { status: 403 } ]);
    });

    context('WHEN person owns pets', () => {
      describeApi('/1/pets', () => {
        // Expect GET /people/1/pets/1 to be Skippy the Bush Kangaroo
        [{ path: '/1', expect: { name: 'Skippy', kind: 'Bush Kangaroo' }}],
      });
    })
  });
});

What it tests

describeApi will check that the route returns the expected status code (200 by default) and that the JSON returned contains at least the properties in the expect object.

If expect is not included in the options, then only the status is checked.

Body expectations use chai-subset to do partial matching on the JSON return value.

If you're using jest, it's highly recommended you install the optional dependency chai-subset-jest-diff so that your test reports have nicely formatted diffs

You can specify a url prefix as the second parameter to describeApi that will be used for all paths.

Defining routes to test

describeApi takes an array of routes to test. Each route is defined by an object with the following keys.

All are optional. An empty object will result in a test that calls GET / expects a return status of 200.

keydefaultdescription
methodGETThe HTTP method
path*''Path to request
name`${method} ${path} (${note})`Specify if you want to override the route name
status200Expected status code from the server
noteA helpful note to differentiate the route from others
body*Body to send with the request
expect*Partial object to match JSON body against (can also be text)
headers*An object of headers to pass to the request
bearerToken to set in the Authorization: Bearer header
describeCallback to execute within the context of the route's describe block

Options marked * can be a (async) function in which case the return value of the function will be used. The functions are evaluated during test execution.

Tests by the same name may be grouped within the same describe statement by your test runner, this could lead to unexpected results, so if you are testing the same route more than once, you should use note to differentiate the tests.

Payloads always in data

Expect and body by default assume that the API nests the objects within a data attribute. If you want to specify these objects without the data wrapper, use _body and _expect.

routes = [{ '/', expect: person }];

// Expects GET / to return
{
  data: {
    name: 'Harvey Milk',
    state: 'California'
  }
}

routes = [{ '/', _expect: person }];
// Expects GET / to return
{
  name: 'Harvey Milk',
  state: 'California'
}

Passing the server in via a function

Because tests may want to spin up the server within before and after blocks describeApi takes a function that returns the server.

When a route is executed that function will be called to retrieve the server.

To simplify this setup, describeApi provides a helper function autorun which will run the necessary before and after blocks to start up the server and close it again when done.

Autorun expects an app that behaves like a koa app (returns a HttpServer from app.listen())

const app = require('koa')();

app.use(myMiddleware);

const getServer = describeApi.autorun(app);

describeApi(getServer, ...)

Extending the describe blocks

If you need more tests or hooks in the describe block for a particular test, use the describe callback.

describe('my api', () => {
  describeApi(server, [
    {
      path: '/get_gookie',
      describe: () => {
        it('returns a cookie', (response) => {
          expect(response.headers.cookies).to.be.ok;
        });
      }
    },
  });
});

afterRoute and beforeRoute

To avoid confusion due to every route running only once in it's describe block, parkes-api-test defines beforeRoute and afterRoute which are aliases for before(All) and after(All).

afterRoute and it inside a route's describe block

Inside route describes, afterRoute and it functions are overridden to pass additional arguments to the callback should you need them.

it('', (response, resolvedRoute) => {}) afterAll((response, resolvedRoute) => {})

response is the supertest response object

resolvedRoute is a copy of the route object with all dynamic attributes and any route modifiers resolved

Nesting blocks

Because describe blocks are used, you can nest them as you would expect.

You can also nest within a describe block for your router

Nested describeApi's inherit the getServer method and path from the above scope

describe('my api', () => {
  describeApi(server, '/people', [
    { path: '/1' },
  ],
  () => {
    describe('WHEN they have pets', () => {
      describeApi('/1/pets', [
        { path: '/1' }
      ]);
    });
  });
});

Under the hood

The test runner sets up a describe block for each route.

Each route is called during the beforeAll phase of the test. This is different from most test patterns, but this has the benefit that in order to assert things about the result of a HTTP request you only need to make that request once.

Each route will have one it block for asserting the return status and body content of the route.

If you define describe then all the it blocks logically sit within the describe block for that route.

// Example
describe('my api', () => {
  describeApi(getServer, '/user', [
    { expect: expectedJson, describe: () => {
      it('passes response to it block', (response) => {
        expect(response.status).to.be.ok;
      });
    } }
  ]);
});

// Is the equivalent of
describe('my api', () => {
  describe('GET /user', () => {
    let response;

    beforeAll(() => {
      response = await getServer().get('/');
    })

    it('status 200', () => {
      expect(response.status).to.eq(200);
    });
    it('body is correct', () => {
      expect(response.body).to.containSubset(expectedJson);
    });
    it('passes response to it block', (response) => {
      expect(response.status).to.be.ok;
    });
  });
})

Known Issues

Using mocha -w is currently broken as the before hooks do not run on subsequent runs. (The same problem does not occur when using Jest)

License

© 2017 Agency Ventures

Licensed under the JWL license. See LICENSE.md for details.

0.6.0

3 years ago

0.5.0

5 years ago

0.4.1

5 years ago

0.4.0

5 years ago

0.3.1

5 years ago

0.3.0

5 years ago