# Usage

This section details how to use the module, including all of the ways you can extend the Server class provided by this package to mock both simple and complex REST APIs for testing.

The following concepts will be used to incrementally expose readers to the functionality provided by this package. The sections below start off simple and progressively become more and more complex to expose functionality:

For additional context on how to use this module during testing, see the Quickstart section of the documentation.

# Mocking Endpoints

The main feature provided by this package is the ability to mock REST endpoints. For that goal, we can easily define functions to mock specific rest endpoints by extending the Server class provided with this package. To set up an API mock that will automatically return fake data during axios requests made by your project, first extend the Server class and define functions for the various request types:

Note

The example immediately below is meant to be illustrative, not suggestive. See the sections after this one for a better example of how to use this package.

// contents of tests/server.js
import { Server } from 'jest-axios';

class App extends Server {
  api() {
    return {
      '/posts': {
        get: () => [
          { id: 1, title: 'Foo', body: 'foo bar' },
          { id: 2, title: 'Bar', body: 'bar baz' },
        ],
        post: (data) => { id: 3, ...data },
      },
      '/posts/1': {
        get: () => { id: 1, title: 'Foo', body: 'foo bar' },
        put: (data) => data,
        delete: () => {},
      },
      '/posts/2': {
        get: () => { id: 2, title: 'Bar', body: 'bar baz' },
      },
    };
  }
}

export default App('posts-app');

Once these endpoints are defined, any axios call issuing GET requests for those endpoints will return the data specified in the code above:

await axios.get('/posts');
/*
[
  { id: 1, title: 'Foo', body: 'foo bar' },
  { id: 2, title: 'Bar', body: 'bar baz' },
]
*/

// get new post
await axios.get('/posts/1');
/*
{ id: 1, title: 'Foo', body: 'foo bar' }
*/

For example, here is a full jest test file that uses the posts-app mock above during testing:

import axios from 'axios';
import server from './server';

jest.mock('axios');
server.init(axios);

test('posts test', async () => {

  // issue request
  const response = await axios.get('/posts');

  // check status code
  assert.equal(response.status, 200);

  // check payload
  assert.equal(response.data, [
    { id: 1, title: 'Foo', body: 'foo bar' },
    { id: 2, title: 'Bar', body: 'bar baz' },
  ]);

});

If you make a bad request (for an endpoint that isn't mocked by this library), you'll receive an appropriate response code:

axios.get('/missing-endpoint').catch(err => {
  // err will have `status` and `message` data.
});

That covers the basics of the core functionality provided by this module, but defining singular endpoints like this can become messy if you're trying to mock many data models with many model instances. To simplify the process of mocking these requests with data, we can define data() configuration that will allow us to access a database of stored data when resolving requests.

# Mocking Data Models

When testing applications, we often need to pull data for specific data models and collections of those models from an API. To showcase a mock server that fakes this type of data, let's configure a simple mock server that hosts a simple collection called posts:

import { Server } from 'jest-axios';

class App extends Server {

  data() {
    return {
      posts: [
        { title: 'Foo', body: 'foo bar' },
        { title: 'Bar', body: 'bar baz' },
      ],
    };
  }

  api() {
    return {
      '/posts': this.collection('posts'),
      '/posts/:id': this.model('posts')
    };
  }
}

TIP

Whenever an :id parameter is embedded within a URL, an the url will automatically be parsed for the id parameter and the id parameter will become the first argument to the endpoint callable.

With this simple configuration, axios will behave accordingly as the following requests are made:

// get collection of posts
await axios.get('/posts');
/*
[
  { id: 1, title: 'Foo', body: 'foo bar' },
  { id: 2, title: 'Bar', body: 'bar baz' },
]
*/

// create new post
await axios.post('/posts', { title: 'Baz', body: 'baz' });
/*
{ id: 3, title: 'Baz', body: 'baz' }
*/

// get new post
await axios.get('/posts/3');
/*
{ id: 3, title: 'Baz', body: 'baz' }
*/

// update new post
await axios.put('/posts/3', { title: 'BazBaz' });
/*
{ id: 3, title: 'BazBaz', body: 'baz' }
*/

// delete new post and check if it exists
await axios.delete('/posts/3');
try {
  await axios.get('/posts/3');
} catch (err) {
  console.log(err);
}
/*
{
  status: 402,
  message: 'Could not find resource `3`'
}
*/

# Mocking Endpoint Actions

Now that we've highlighted basic model CRUD functionality, let's add some custom endpoints for performing actions on data. In this example, we want to include a nested /posts/:id/archive endpoint for setting an archive flag on post objects. To do so, we can update our api() definition like so:

api() {
  return {
    '/posts': this.collection('posts'),
    '/posts/:id': this.model('posts'),
    '/posts/:id/archive': {
      post: (id) => {
        this.db.posts.update(id, { archived: true });
        return { status: 'ok' };
      },
    }
  };
}

With this defined, any /posts/:id/archive call will automatically be mocked:

// archive post
await axios.post('/posts/1/archive');
/*
{ status: 'ok' }
*/

// get data to show new flag
await axios.get('/posts/1');
/*
{ id: 1, title: 'Foo', body: 'foo bar', archived: true }
*/

# Mocking Model Relations

You can similarly use this pattern to automatically mock fetching nested resources. Let's say we want to track authors and comments related to a specific post. In this scenario, posts have a single author and multiple comments. To represent those relationships when defining data, you can use the following definitions:

import { Server } from 'jest-axios';


class App extends Server {

  data() {
    return {
      posts: [
        { title: 'Foo', body: 'foo bar', author_id: 1 },
        { title: 'Bar', body: 'bar baz', author_id: 1 },
      ],
      authors: [
        { name: 'Jane Doe', email: 'jane@doe.com' },
        { name: 'John Doe', email: 'john@doe.com' },
      ],
      comments: [
        { user: 'jack', body: 'foo comment', post_id: 1 },
        { user: 'jill', body: 'bar comment', post_id: 1 },
      ]
    };
  }

  api() {
    return {
      '/posts': this.collection('posts'),
      '/posts/:id': this.model('posts'),
      '/posts/:id/authors': this.model({
        model: 'authors',
        relation: 'posts',
        key: 'author_id'
      }),
      '/posts/:id/comments': this.collection({
        model: 'comments',
        relation: 'posts',
        key: 'post_id'
      }),
    };
  }
}

In the example above, the collection and model factory methods take an option object. For the model factory method, the key option represents a foreign key on the relation to the specified model. For the collection factory method, the key option represents a foreign key on the model that is linked to specified relation.

If the syntax above for configuring nested resource fetching is confusing, you can configure the relations manually (above is shorthand for configuring a common pattern). For clarity, here is how you could configure the relations manually:

api() {
  return {
    '/posts': this.collection('posts'),
    '/posts/:id': this.model('posts'),
    '/posts/:id/authors': {
      get: (id) => {
        const authorId = this.db.posts[id].author_id;
        return this.db.authors.get(authorId);
      },
      post: (id, data) => {
        this.db.posts.update(id, { author_id: data.id });
        return this.db.authors.get(data.id);
      }
    },
    '/posts/:id/comments': {
      get: (id) => {
        return this.db.comments.all().filter(x => x.post_id === id);
      },
      post: (id, data) => {
        data.post_id = id;
        return this.db.comments.add(data);
      },
    },
  };
}

# Mocking Nested Resources

Nesting resources inside payloads for a single model instance is common practice for reducing the number of requests that need to be made for pulling data associated with a view. With this library, you can mock nesting for related models by setting data properties equal to callable objects. In this example, let's say we want our /posts/:id endpoint to return nested data for authors and comments relations, but we don't want to bog down the /posts endpoint with those relations. First, we can augment the posts model with callable objects that return the data we need:

// showing data block only
data() {
  const getAuthor = post => this.db.authors.get(post.author_id) || null;
  const getComments = post => this.db.comments.all().filter(x => x.post_id === post.id);
  return {
    posts: [
      {
        title: 'Foo',
        body: 'foo bar',
        author_id: 1,
        author: getAuthor,
        comments: getComments,
      },
      {
        title: 'Bar',
        body: 'bar baz',
        author_id: 1,
        author: getAuthor,
        comments: getComments,
      },
    ],
    authors: [
      { name: 'Jane Doe', email: 'jane@doe.com' },
      { name: 'John Doe', email: 'john@doe.com' },
    ],
    comments: [
      { user: 'jack', body: 'foo comment', post_id: 1 },
      { user: 'jill', body: 'bar comment', post_id: 1 },
    ]
  };
}

And in the api() configuration block, we can subset the responses by specific keys:

api() {
  return {
    '/posts': this.collection({
      model: 'posts',
      exclude: ['author', 'comments'],
    }),
    '/posts/:id': this.model({
      model: 'posts',
      exclude: ['author_id']
    }),
  };
}

This type of configuration will result in the following axios mocks:

await axios.get('/posts');
/*
[
  { id: 1, title: 'Foo', body: 'foo bar', author_id: 1 },
  { id: 2, title: 'Bar', body: 'bar baz', author_id: 1 },
]
*/

await axios.get('/posts/1');
/*
{
  id: 1,
  title: 'Foo',
  body: 'foo bar',
  author: {
    id: 1,
    name: 'Jane Doe',
    email: 'jane@doe.com',
  }
  comments: [
    { user: 'jack', body: 'foo comment', post_id: 1 },
    { user: 'jill', body: 'bar comment', post_id: 1 },
  ]
}
*/

# Singleton Models

Alongside Models and Collections, it's often useful to have a data model that represents singleton data instead of collection data. A common example of this is providing profile data to the currently logged-in user via a /profile endpoint contextualized to return information for the current user.

Since we don't need to define a relational schema to represent users in our database (we're just concerned with actions of a single user), we can represent the profile data as a singleton model. Here is an example config with data and endpoint actions for a /profile singleton model:

import { Server } from 'jest-axios';

class App extends Server {

  data() {
    return {
      profile: {
        username: 'admin',
        last_seen: () => new Date();
      }
    };
  }

  api() {
    return {
      '/profile': self.singleton('profile'),
    };
  }
}

export default App('profile');

For clarity, here is equivalent configuration for the api() block above:

api() {
  return {
    '/profile': {
      get: () => this.db.profile,
      put: (data) => {
        return this.db.profile.update(data);
      },
      delete: () => {
        return this.db.profile.reset();
      },
    },
  };
}

With the endpoint configuration above, axios requests will return an object for the endpoint instead of a collection:

// get profile
await axios.get('/profile');
/*
{
  username: 'admin',
  last_seen: '2020-03-16T4:24:38.680Z',
}
*/

// update profile
await axios.put('/profile', { username: 'foo'});
/*
{
  username: 'foo',
  last_seen: '2020-03-16T4:24:38.680Z',
}
*/

// send delete request and check value
await axios.delete('/profile');
await axios.get('/profile');
/*
{
  username: 'admin',
  last_seen: '2020-03-16T4:24:39.680Z',
}
*/

Additionally, you can see that mocking functions are always re-evaluated when new GET requests are made.

# Server Utilities

There are several utilities that can be used during testing. First, to reset a database between sessions, you can use the server.reset() function:

jest.mock('axios');
server.init(axios);
beforeEach(() => {
  server.reset();
});

This will reset the database down to it's initial state (with data configured in the data() block). To clear the database completely during testing, use the server.clear() function:

jest.mock('axios');
server.init(axios);
beforeAll(() => {
  server.clear();
});

Finally, to access the state of a database during or after testing, use the server.dump() property:

jest.mock('axios');
server.init(axios);
afterAll(() => {
  console.log('Printing Database!');
  console.log(server.dump());
});

If you have any questions that aren't answered by this documentation, feel free to file a documentation issue in the GitHub Issue Tracker for this project.