Это гостевое сообщение Роберта Диккерта, разработчика OK GROW!

Модульные тесты - мои любимые тесты. Они могут работать за миллисекунды и заставляют меня писать лучший код. Но они также могут быть довольно сложными в настройке. Meteor также может представлять некоторые уникальные проблемы для тестирования. Когда мы тестировали Mocha, я с радостью следовал паттерну, разработанному Питом Кори с использованием testdouble.js для заглушки Meteor (здесь я буду использовать термины макет и заглушка как синонимы, но есть различия). Это отличный подход, и он также иллюстрирует пару ключевых моментов, которые не всегда осознают многие новички в модульном тестировании:

Модульные тесты должны быть быстрыми. Модульные тесты для небольшого проекта должны завершаться за десятки миллисекунд или быстрее (за исключением некоторых функций, требующих обработки большого количества данных). Это важно, потому что модульные тесты предназначены для частого выполнения (я часто запускаю их в режиме просмотра, чтобы направлять свою работу в режиме реального времени). Это по определению означает, что вы не можете использовать какие-либо ресурсы сети, базы данных или файловой системы. И, как правило, это в любом случае поможет вам выбрать правильный способ модульного тестирования, потому что вы не изолируете модули кода, если они обращаются к внешним ресурсам.

Не тестируйте фреймворк. Вы не должны проверять наличие ошибки в Meteor; они это покрыли. Ваша задача - убедиться, что Meteor вызывается правильно и что, когда он предоставляет результаты вашему коду, ваш код обрабатывает их, как ожидалось. Я бы даже сказал, что даже использовать Minimongo в модульных тестах, как правило, не очень хорошая идея, даже если это достаточно быстро.

Раньше было невозможно заглушить Метеор. Я знаю - я когда-то участвовал в тестировании скорости. Например, в какой-то момент мы пытались перенести функцию автоматического переключения из Jest, но по зависимостям Meteor было нелегко пройтись, а загрузка отдельных компонентов без глобальных объектов Meteor была очень сложной. Однако с переходом на Meteor 1.3 мы могли начать использовать стандартные модули JS с import, и это открыло двери для использования стандартных инструментов JavaScript. Ура!

ДВИЖЕНИЕ В ШТУТУ

Просто наличие «голых» тестов Mocha без Velocity или других оболочек было огромным выигрышем для скорости и возможности использовать более широкую экосистему JavaScript. Но в начале этого года ОК, РОСТ! начал экспериментировать с Jest. С нашим интенсивным использованием React и React Native, это становилось очевидным выбором для сообщества, и несколько наших разработчиков, переходящих на JavaScript из Ruby, также испытывали сильное разочарование из-за фрагментации и чрезмерной конфигурации, которые требовались Mocha. У меня были опасения, и я боялся, что иметь дело с Meteor будет более неудобно, но оказалось, что у Jest есть мощные способы справиться даже с meteor/{package} импортом Meteor.

НАШ КОД НА ТЕСТИРОВАНИИ: АДМИНИСТРАТОРЫ СОВЕРШАЕТ МОДЕЛИРУЕМЫЕ СДЕЛКИ ДЛЯ ПОЛЬЗОВАТЕЛЕЙ

Вместо того, чтобы тестировать упрощенную (a, b) => a + b функцию, давайте попробуем вообразить что-нибудь более реальное. Допустим, у вас есть платформа для торговли акциями, предоставляющая пользователям как реальные, так и смоделированные сделки. Симулятор торговли упрощен и не обрабатывает некоторые вещи, поэтому у нас есть функция, позволяющая администратору совершать имитацию торговли от имени пользователя. Например, все владельцы Whole Foods должны были быть конвертированы в акции Amazon при их приобретении Amazon, поэтому администратору необходимо было продать WFM и купить AMZN в определенный день по определенным ценам. Вот код файла placeTradeForUser.js:

// This function allows admins to place arbitrary trades for a
// user or group of users, useful for correcting problems or
// dealing with company acquisitions where one stock
// is converted into another for all owners.
import { Meteor } from 'meteor/meteor';
import { placeOrder } from './tradeSimulator';
export default placeTradeForUser = async orderSpec => {
  const user = Meteor.users.findOne({
    username: orderSpec.username
  });
  // Throw an error if the user is not a simulated user
  // (we don't want to ever issue trades to a live account!)
  if (user.tradingAccount.provider !== 'simulator') {
    throw new Error(
      `user ${user.username} is not a simulated account.`
    );
  }
  const transactionId = await placeOrder(orderSpec);
  return transactionId;
};

Размещение сделок для кого-то еще полезно на симуляторе, но вы не захотите иметь никаких шансов инициировать сделки на реальной платформе! Мы хотим проверить, правильно ли он выдаст эту ошибку (возможно, вам понадобятся и другие средства защиты 😃, но мы будем придерживаться упрощенного примера).

ТЕСТОВЫЙ КОД

Вот содержание placeTradeForUser.test.js. Обратите внимание, что имя будет помещено рядом с тестируемым файлом в вашей структуре каталогов. Это упрощает работу с обоими файлами одновременно.

jest.mock('meteor/meteor');
import { __setUsersQueryResult } from 'meteor/meteor';
jest.mock('./tradeSimulator', () => ({
  placeOrder: jest.fn()
}));
import { placeOrder } from './tradeSimulator';
import placeTradeForUser from './placeTradeForUser';
describe('placeTradeForUser()', () => {
  test('allow trades on provider `simulator`', async () => {
    __setUsersQueryResult({
      username: 'testuser',
      tradingAccount: {
        provider: 'simulator',   
      },
    });
    await placeTradeForUser({});
    expect(tradeSimulator.placeOrder).toBeCalled();
  });
  test('disallow trades on provider `fidelity`', async () => {
    __setUsersQueryResult({
      username: 'testuser',
      tradingAccount: {
        provider: 'fidelity',
      },
    });
    const result = placeTradeForUser({});
    await expect(result).rejects.toEqual(
      new Error('user testuser is not a simulated account.')
    );
  });
});

Здесь есть на что посмотреть, но сначала обратите внимание на два заявления test. В первом мы expect(tradeSimulator.placeOrder).toBeCalled(). Мы не смотрим на результат; мы просто убеждаемся, что другой модуль вызывается нормально, если учетная запись имитируется. Во втором тесте используется реальный торговый счет, и хотя тест немного сложнее читать, вы можете понять, что он должен вернуть отклоненное обещание с сообщением об ошибке (это отклоненное обещание, потому что placeTradeForUser() является функцией async - в противном случае вы могли бы использовать приятнее expect(result).toThrow(new Error(...))). Лучший способ expect отклоненных обещаний - в пути.

Так здорово, у нас есть полезный тест! Но код, который мы тестируем, обращается к placeOrder(), внешней функции, которая поступает через именованный импорт, и, что более важно, он делает запрос к базе данных через Meteor.user. Импорт Meteor приведет к тому, что Jest выдаст ошибку, потому что Jest ожидает, что путь импорта будет указывать на модуль npm, которого там нет. Большая часть этого кода представляет собой стандартную насмешку Jest (см. Документацию здесь и здесь), но две вещи заслуживают упоминания.

ИМПУЛЬСНЫЕ ИМПОРТЫ MOCKING

Поскольку мы находимся в авангарде разработки современного JavaScript, мы иногда сталкиваемся с вещами, которые по-прежнему основаны на том, как require работает. Если бы мы использовали экспорт по умолчанию (например, import placeOrder from './placeOrder';), мы могли бы заменить нашу собственную реализацию, но мы используем именованный импорт (import { placeOrder } from './tradeSimulator' - обратите внимание на { фигурные скобки }), и импорт модуля ES6 выдаст ошибку при изменении (и не может мы перемещаем его к произвольному объекту, так как Jest не подберет его и не подставит для нас). К счастью, jest.mock() с этим справится. Если мы передадим путь к файлу для фиксации и функцию, возвращающую фиктивный экспорт, мы сможем import { placeOrder } нормально (совет Джесси Розенбергеру за то, что он показал мне это).

МОКИНГ METEOR/METEOR

До этого момента все было стандартным кодом Jest, но Meteor по-прежнему импортирует пакеты Meteor в соответствии с meteor/packageName соглашением, которое Meteor умеет обрабатывать, а Jest - нет. (Примечание: мы надеемся, что эта часть устареет с будущей версией Meteor, поскольку Meteor работает на 100% на основе npm.) К счастью, Jest предоставляет нам необходимые инструменты (подсказка пользователю StackExchange chmanie ). В этом случае в реальном приложении мы будем обращаться к базе данных, и нам нужно значение, которое она возвращает, и контроль над этим значением, чтобы провести наш тест.

Сначала нам нужно указать Jest, где найти наши макеты, и, к счастью, вы можете сделать это в своем jest.config.js файле или package.json. Мы сделаем это с помощью jest.config.js, который выглядит так:

module.exports = {
  moduleNameMapper: {
    '^meteor/(.*)': '<rootDir>/.meteorMocks/index.js',
  },
}

Затем мы можем поместить наш фиктивный код в .meteorMocks/index.js:

let usersQueryResult = [];
export function __setUsersQueryResult(result) {
  usersQueryResult = result;
}
export const Meteor = {
  users: {
    findOne: jest.fn().mockImplementation(() => usersQueryResult),
    find: jest.fn().mockImplementation(() => ({
      fetch: jest.fn().mockReturnValue(usersQueryResult),
      count: jest.fn(),
    })),
  },
};
export const Mongo = {
  Collection: jest.fn().mockImplementation(() => ({
    _ensureIndex: (jest.fn()),
  })),
};

Это дает нам закрытие, которое позволяет нам установить произвольный результат usersQueryResult, который будет возвращен нашей имитируемой функцией, а также частный __setUsersQueryResult(), который наши тесты могут импортировать для изменения этого значения. Кроме того, он реализует некоторую структуру объекта Meteor. Обратите внимание, что нам не нужно реализовывать все, а только то, что используется нашей тестируемой функцией. Мы могли бы опустить find, fetch или count в этом примере, но он также может использоваться другими тестами, которым могут понадобиться эти вещи.

ИМПОРТ ДРУГИХ ПАКЕТОВ METEOR

Мок-объект Mongo в приведенном выше коде также не нужен в нашем примере, но при необходимости он успешно заменит определение коллекции, включая индексы., Например этот файл:

import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
const Performance = new Mongo.Collection('performance');
Performance._ensureIndex({
  userId: 'text',
});
export default Performance;

Видишь, как это сработало? new Mongo.Collection() вернет объект, у которого есть функция _ensureIndex, что снова позволяет нам компилировать без ошибок.

Вот и все. Теперь вы готовы протестировать что угодно и где угодно.

Об авторе: Роберт Дикерт - разработчик в OK GROW!, партнере Meteor Prime, который создает программное обеспечение и проводит обучение по JavaScript, React и GraphQL. Он активен в сообществе Meteor с 2012 года. Если вам интересно узнать больше о тестировании JavaScript и его преимуществах, присоединяйтесь к нам на Assert (js) Conf 22 февраля 2018 года!