npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

parkes-api-test

v0.6.0

Published

Concise JSON API testing

Downloads

30

Readme

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.

| key | default | description | | --- | --- | ---- | | method | GET | The HTTP method | | path* | '' | Path to request | | name | `${method} ${path} (${note})` | Specify if you want to override the route name | | status | 200 | Expected status code from the server | | note | | A 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 | | bearer | | Token to set in the Authorization: Bearer header | | describe | | Callback 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.