Jest - популярный фреймворк для тестирования кода JavaScript, написанный Facebook. Он поставляется с множеством обычных утилит для тестирования, таких как сопоставители для написания тестовых утверждений и имитирующих функций. Идея имитации функции, которая вызывает API-вызов какой-либо внешней службы, была для меня немного чуждой, пока я не использовал имитацию Jest в работе. В этом посте будет представлен краткий обзор того, как вы можете имитировать функции в ваших тестах, которые обычно вызывают API или выполняют действия CRUD в базе данных.

Ручные макеты Jest

Важной особенностью Jest является то, что он позволяет вам писать ручные макеты, чтобы использовать поддельные данные для ваших собственных модулей в вашем приложении. Для этого вам нужно написать модуль в подкаталоге __mocks__, непосредственно примыкающем к реальному модулю, и оба файла должны иметь одинаковое имя. Давайте посмотрим на пример.

Скажем, у нас есть приложение Node, которое содержит каталог lib, а внутри этого каталога находится файл с именем db.js. В этом файле есть несколько методов, которые выполняют HTTP-запросы к API базы данных. Выглядит это примерно так:

lib / db.js

const request = require('request-promise');
const selectUserById = async userId => {
  const options = {
    uri: `https://www.mydbapi.com/users/${userId}`,
    json: true
  };
  try {
    const selectResult = await request(options);
    return { data: selectResult };
  } catch (error) {
    return { error: error.response.body };
  }
};
const createUser = async userData => {
  const options = {
    method: 'POST',
    uri: 'https://www.mydbapi.com/users',
    body: userData,
    json: true
  };
  try {
    const createResult = await request(options);
    return { data: createResult };
  } catch (error) {
    return { error: error.response.body };
  }
};
module.exports = { selectUserById, createUser };

Здесь у нас есть два метода, selectUserById и createUser (обычно есть методы для обновления и удаления пользователей, но для того, чтобы этот пример был кратким, мы их исключим). Мы используем библиотеку request-promise для вызовов API к базе данных. Чтобы смоделировать эту функциональность в наших тестах, мы захотим написать очень похожий модуль в подкаталоге __mocks__. Это означает, что мы захотим создать еще один db.js файл, который находится в каталоге lib/__mocks__. Мы также создадим testData.js файл в этом каталоге, чтобы мы могли использовать поддельные данные вместо вызова API в наших тестах. Эти два файла будут выглядеть примерно так:

lib / __ издевается над __ / testData.js

const users = [
  {
    id: 1,
    firstName: 'John',
    lastName: 'Smith',
    userName: 'jsmith'
  },
  {
    id: 2,
    firstName: 'Jane',
    lastName: 'Doe',
    userName: 'jdoe'
  },
  {
    id: 3,
    firstName: 'Ben',
    lastName: 'Morrison',
    userName: 'benjimorr'
  }
];
module.exports = { users };

lib / __ издевается над __ / db.js

const _ = require('lodash');
const { users } = require('./testData');
const selectUserById = userId => {
  return new Promise((resolve, reject) => {
    const userData = _.filter(users, obj => {
      return obj.id === userId;
    });
    if (userData) {
      resolve({ data: userData });
    } else {
      reject({ error: 'User not found.' });
    }
  });
};
const createUser = userData => {
  return new Promise((resolve, reject) => {
    if (_.some(users, ['userName', userData.userName])) {
      reject({ error: 'That username already exists.' });
    } else {
      const newUser = {
        // NOTE: This is not the best way to create a "unique" id,
        // I am just using this hack for this example.
        id: users.length + 1,
        ...userData
      };
      users.push(newUser);
      resolve({ data: newUser });
    }
  });
};
module.exports = { selectUserById, createUser };

В нашем имитируемом модуле db.js мы используем поддельные данные пользователя из файла testData.js, а также некоторые полезные методы из популярной библиотеки lodash, чтобы помочь нам найти объекты в массиве поддельных пользователей. Мы также возвращаем обещания из наших имитируемых функций, чтобы имитировать HTTP-запросы, чтобы мы могли использовать async/await в наших тестах, аналогично тому, как мы это делали бы в нашем производственном коде.

Вы заметите, что наши имитирующие функции имеют те же имена, что и настоящие функции - это важная деталь, и наши фиктивные функции не будут работать, если они будут названы по-другому. А теперь пора написать тесты!

Написание наших тестов

Теперь, когда мы смоделировали наш db.js модуль, мы можем написать несколько простых тестов, чтобы убедиться, что все работает должным образом, и нам не придется беспокоиться о каких-либо внешних вызовах API.

Примечание. Поскольку в наших тестах нам потребуется модуль db.js, использование jest.mock('./db.js') обязательно.

lib / db.test.js

jest.mock('./db'); // This line is important!
const db = require('./db');
describe('selectUserById function', () => {
  it('returns an error for a user that doesn't exist', async () => {
    const userData = await db.selectUserById('foo');
    expect(userData.error).not.toBeNull();
    expect(userData.error).toBe('User not found.');
  });
  it('returns the user data for a user that exists', async () => {
    const expectedUserData = {
      id: 1,
      firstName: 'John',
      lastName: 'Smith',
      userName: 'jsmith'
    };
    const userData = await db.selectUserById(1);
    expect(userData.data).not.toBeNull();
    expect(userData.data).toEqual(expectedUserData);
  });
});
describe('createUser function', () => {
  it('returns an error if the username exists', async () => {
    const newUserData = {
      firstName: 'Jonathan',
      lastName: 'Smith',
      userName: 'jsmith'
    };
    const createResult = await db.createUser(newUserData);
    expect(createResult.error).not.toBeNull();
    expect(createResult.error).toBe('That username already exists.');
  });
  it('returns data for new user when successful', async () => {
    const newUserData = {
      firstName: 'Homer',
      lastName: 'Simpson',
      userName: 'hsimpson'
    };
    const expectedResult = { id: 4, ...newUserData };
    const createResult = await db.createUser(newUserData);
    expect(createResult.data).not.toBeNull();
    expect(createResult.data).toEqual(expectedResult);
  });
});

Здесь мы написали несколько тестов для наших selectUserById и createUser функций. Тесты подтверждают, что мы получаем error, когда что-то идет не так, и правильный data, когда все работает успешно.

Примечание. На практике вы захотите создать функцию в вашем lib/__mocks__/db.js файле, чтобы вернуть поддельный массив users в исходную форму. Вы можете использовать эту функцию в блоке afterEach, чтобы предотвратить любые странные результаты тестирования, поскольку мы добавляем новые данные в массив users в наших тестах.

Заключение

На этом мы завершаем этот учебник о том, как имитировать асинхронные методы при тестировании кода с помощью Jest. Я надеюсь, что вы нашли этот пост полезным и сможете начать использовать эти методы в своих собственных тестах!