Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recommended pattern for testing REST API endpoints? #310

Closed
mattkrick opened this issue Dec 6, 2015 · 9 comments
Closed

Recommended pattern for testing REST API endpoints? #310

mattkrick opened this issue Dec 6, 2015 · 9 comments

Comments

@mattkrick
Copy link
Contributor

How would you recommend we test endpoints where we need to send http requests? Is this on the roadmap or is there another package that plugs in nicely? I know of other packages like supertest, chakram, and frisby, but nothing seems to have an API as beautiful as ava 😄

@sindresorhus
Copy link
Member

You can use any assert module with AVA, including HTTP ones. The only downside is that planned assertions won't yet work with it (#25). We don't plan on having something like that built-in. There's already a lot of good modules for that. From a quick look it seems Supertest and Chakram should work fine with AVA. Frisby seems tied to Jasmine.

Since this is such a common use-case, I think we should have a recipe for how to do this with AVA. Help welcome :)

@mattkrick
Copy link
Contributor Author

Agreed, a nice example or 2 in the readme would be awesome. I'll play around with some packages & see if any good patterns emerge

@sotojuan
Copy link
Contributor

sotojuan commented Dec 8, 2015

Just tested out a super basic JSON endpoint with supertest:

// This is an express app
import app from './index.js';

test.cb('get', t => {
  request(app)
    .get('/hello')
    .expect('Content-Type', /json/)
    .expect(200)
    .end(function (err, res) {
      t.notOk(err);
      t.is(res.body.id, 1);
      t.is(res.body.username, 'sotojuan');
      t.end();
    });
});

Seems good enough for me, and there's promise versions of supertest that may be even easier to work with.

@mattkrick
Copy link
Contributor Author

that's good to see! I decided to keep it simple & just use the native (or polyfilled) fetch:

test('AuthController:signup:Success', async t => {
  t.plan(4);
  const res = await postJSON('/auth/signup', {email: 'AuthControllerSignup@Success', password: '123123'});
  const parsedRes = await res.json();
  t.is(res.status, 200);
  t.ok(parsedRes.authToken);
  t.is(parsedRes.user.email, 'authcontrollersignup@success');
  t.false(parsedRes.user.strategies.local.isVerified);
});

//in my utils file
export function postJSON(route, obj) {
  return fetch(hostUrl() + route, {
    method: 'post',
    credentials: 'include',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(obj)
  })
}

@i5ting
Copy link

i5ting commented Jun 7, 2016

var superkoa = require('superkoa')

test.cb("superkoa()", t => {
  superkoa('./koa.app.js')
    .get("/")
    .expect(200, function (err, res) {
      t.ifError(err)
      var userId = res.body.id;
      t.is(res.text, 'Hello Koa', 'res.text == Hello Koa')
      t.end()
    });
});

@kevva
Copy link
Contributor

kevva commented Jun 7, 2016

I usually use nock for testing API endpoints/HTTP requests. Looks something like https://github.com/kevva/download/blob/master/test.js#L13-L23.

@marr
Copy link

marr commented Jun 15, 2016

@i5ting here is an example without the 'callback style' assertion

https://github.com/marr/tests/commit/05506ed154b4a3403cf674e8aff910c6218daf0d

@i5ting
Copy link

i5ting commented Jun 16, 2016

thanks @marr @kevva another practise with generator from me

// *  GET    /users[/]        => user.list()
test('GET /' + model, function * (t) {
  var res = yield superkoa('../../app.js')
    .get('/' + model)

  t.is(200, res.status)
  t.regex(res.text, /table/g)
})

for atomic test

// *  GET    /users/:id       => user.show()
test('GET /' + model + '/:id show', function * (t) {
  var res1 = yield superkoa('../../app.js')
    .post('/api/' + model)
    .send(mockUser)
    .set('Accept', 'application/json')
    .expect('Content-Type', /json/)

  user = res1.body.user

  var res = yield superkoa('../../app.js')
    .get('/' + model + '/' + user._id)

  t.is(200, res.status)
  t.regex(res.text, /Edit/)
})

@eloquence
Copy link

eloquence commented Aug 20, 2016

I recommend playing with ava's async/await support to make this easier. Here's an example using supertest-as-promised (which is a thin wrapper around supertest) and a POST request to a form (not an API endpoint, but the principles are similar). For more on the t.context use, see further below.

test(`Changing to German returns German strings`, async t => {
  let mainPageResponse = await t.context.agent.get('/');
  let matches = mainPageResponse.text.match(/<input type="hidden" value="(.*?)" name="_csrf">/);
  if (matches && matches.length) {
    let csrf = matches[1];
    let postResponse = await t.context.agent
      .post('/actions/change-language')
      .type('form')
      .send({
        _csrf: csrf,
        lang: 'de' // Change language
      })
      .expect(302)
      .expect('location', '/');

    // We don't have to capture this response if the .expect() tests
    // are sufficient.
    await t.context.agent
      .get(postResponse.headers.location)
      .expect(200)
      .expect(/respektiert deine Freiheit/); // String in footer

  } else
    t.fail('Could not obtain CSRF token');
});

As you can see, we can combine supertest's testing patterns with ava's. Since these particular requests are dependent on each other, we can write them as quasi-synchronous code. If we have multiple requests we want to fire without regard for which one completes first, we can use the Promise.all pattern described in this Ponyfoo article which is also a useful intro to async/await.

This particular test depends on t.context properties being initialized through a beforeEach call:

test.beforeEach(async t => {
  // Ensure we initialize from scratch
  Reflect.deleteProperty(require.cache, require.resolve('../app'));
  let getApp = require('../app');
  let app = await getApp({
    silent: true // No logging
  });
  let agent = request.agent(app);
  t.context.agent = agent;
});

The t.context is usable by the test, and gives us access to a fresh app instance and an agent that persists cookies across the requests it makes within that test.

If you use Express, this may also give you a couple of ideas how to address asynchronous, repeated initialization of a an Express app that has non-synchronous code that needs to be run before the app is ready (in my case, Handlebars partials).

If I'm missing anything in the above, please let me know -- still new to ava, but I like it so far. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants