Давным давно я посмотрел Терминатор1. Немного подрос и посмотрел немного отрефакторенный Терминатор 2. Третьей интерации пришлось ждать немного дольше.
И каждый раз бедному СкайНету не везло, и каждый раз все наперекосяк, и секрет этого невезения очень прост — СкайНет не был Subject Under Test (SUT).
Другими словами SkyNet подзабила на тестирование, и производила разведку боем. И, как это обычно бывает, падала в корку(под пресс, в лаву, далее не помню) примерно сразу после выкладки в продакшен.
И весь вопрос — как же протестировать что-то до этой выкладки, до того момента как станет поздно поворачивать назад. И убить всех человеков, конечно же.
Для начала воспользуемся техникой TDD(Test Driven Developement), чтобы определить что же надо сделать, и зачем. Те кто смотрел фильмы знают — все очень просто:
Есть только одна проблема — оба эти события ну как бы одинарные — ракеты надо запустить с первого раза, и Джона тоже бы с первого раза грохнуть надобно, а то подрастет и сопротивление возглавит. В общем, если у вас нет машины времени, то написать тесты, и сделать их повторяемыми будет ой как не просто.
Итак — у СкайНет есть только один шанс запустить ракеты, только один шанс отправить терминатора назад в прошлое, где он должен будет сделать свое дело с первой попытки. Ну — для этого тесты и существуют.
И единственный способ решить проблему — как-то заменить настоящие ракеты ненастоящими, так чтобы можно было их запускать-перезапускать.
По научному это называется — «замокать» (mock)
Но для ответа на этот вопрос — в начале надо посмотреть на коды самого СкайНета.
По коду выходит — для того чтобы протестировать SkyNet требуется заменить зависимости центрального файла «dooms-day.js» на контролируемые сущности.
Я рад что вы спросили. И одного простого ответа у меня нет, но есть 5 еще более простых. Еще куча различных библиотек и парочка патернов с антипатернами о том как мокать хорошо и как мокать плохо.
Но лучше начать с быстрого обзора популярных библиотек, которые предоставляют разные интерфейсы для моканья зависимостей.
Звучит просто, и просто на самом деле. Надо просто создать «замены», и «заменить».
В общем "__mock__" очень простой и удобный механизм, у которого есть только один минус — Jest. Эта фича работает только в Jest, и если Скайнет использует mocha, ava или karma для запуска тестов — прийдется искать другие интерфейсы.
Если уж магия __mock__ нам не доступна, то что на счет просто mock?
Опять же очень простой способ заменить зависимости для тестового файла, и опять же — только Jest.
На самом деле на этом возможности Jest заканчиваются, и не смотря на то что эти два подхода достаточно примитивны (и просты) — это примерно то что и нужно. Возможно все остальное — нетребуемые переусложения.
Что если можно загрузить какой либо файл, но с «измененными» частями?
Примерно так работает proxyquire — одна из самых древних популярных библиотек про это дело. Позволяет загрузить любой файл с перекрытием зависимостей непосредственно запрашиваемых из этого файла.
Главное преимущество перед jest.mock — в одном тесте можно иметь 100500 различных proxyquired поразному перекрытых файлов. Плюс различные команды типа .callThough, которая позволяет только частично перекрывать зависимость (что вообще антипатерн).
Proxyquire иногда может быть немного надоедлив, плюс изредка хочется чего-то типа __mock__, те наличия некоторых «стандартных» моков. В общем mockery!
Mockery так же обладает мощьным API, но немного в другую сторону чем proxyquire. Например там есть режим «изоляции», когда mockery начинает ругаться при подключении «неразрешенных» файлов, что может спасти от случайного изменения зависимости и логики.
TestDouble.js — библиотека от одноименной компании, которая очень много сил уделяет именно «правильности» того или иного подхода. Технически — это jest.mock, вид сбоку.
При этом возможность замены произвольных модулей появилось после очень очень долгих споров.
Вообще TD — кладезь знаний и лучших практик. Советую почитать раздел про моки.
Стоит добавить что все эти библиотеки еще плохо обращаются с кешом, что иногда приводит к серьезному замедлению работы тестов.
Не стоит забывать — вся проблема в том, что SkyNet хотелось совершенства. А все разобранные инструменты — не совершенны. Если мы начнем писать тесты с их использованием — SkyNet с начала пошлет терминаторов за нами.
Чтобы не попасть на счетчик хотелось моки как в jest, только под ava. Proxyquire синтаксис часто может удобен, как и автоматически моки jest.mock/td.replace. Мне много чего хотелось, так что я отдолжил с работы машину времени и еще год назад выложил библиотеку которая все вышеперечисленное умеет.
Заменяем jest.mock
Заменяем mockery
Заменяет proxyquire
Заменяет TD
При этом это работает везде — mocha, ava, karma под node.js или webpack. При этом совершенно по другому работает с кешом, не удаляя ничего тестами не затронутое (иногда в 100 раз быстрее). При этом всегда есть API, например чтобы быть увереным что моки используются правильно:
Или чтобы заменить один модуль другим (как mockery умеет)
Или чтобы использовать import заместо require, что может быть полезно с точки зрения name resolution и type safery.
В общем rewiremock — это та библиотека изменения зависимостей, которая СкайНету подойдет. Особенно учитывая постоянное использование машины времени для улучшения работы и выпуска новых версий.
И, самое главное, это и есть ваша текущая библиотека. Интерфейсно совместима.
И сейчас моя миссия — как-то пересадить людей с proxyquire и mockery (и более мелких продуктов) на что-то более юзабельное. Просто потому что старина Арни был «old, but not obsolete» в 2015, и с тех пор обновлений не получал. Как и proxyquire, как и mockery.
Для справки:
> Jest mocks
> Mockery
> Proxyquire
> TD.js (забудьте почитать вики странички)
Ну и самое главное: rewiremock
PS: И это уже не первая статья о данной библиотеке — остальные тоже могут быть полезны.
И каждый раз бедному СкайНету не везло, и каждый раз все наперекосяк, и секрет этого невезения очень прост — СкайНет не был Subject Under Test (SUT).
Другими словами SkyNet подзабила на тестирование, и производила разведку боем. И, как это обычно бывает, падала в корку(под пресс, в лаву, далее не помню) примерно сразу после выкладки в продакшен.
И весь вопрос — как же протестировать что-то до этой выкладки, до того момента как станет поздно поворачивать назад. И убить всех человеков, конечно же.
TDD
Для начала воспользуемся техникой TDD(Test Driven Developement), чтобы определить что же надо сделать, и зачем. Те кто смотрел фильмы знают — все очень просто:
- Когда наступит Судный День — запустить ракеты и убить всех человеков.
- Когда родится Джон Коннорр — замочить и снять про это фильм.
Есть только одна проблема — оба эти события ну как бы одинарные — ракеты надо запустить с первого раза, и Джона тоже бы с первого раза грохнуть надобно, а то подрастет и сопротивление возглавит. В общем, если у вас нет машины времени, то написать тесты, и сделать их повторяемыми будет ой как не просто.
И что теперь делать?
Итак — у СкайНет есть только один шанс запустить ракеты, только один шанс отправить терминатора назад в прошлое, где он должен будет сделать свое дело с первой попытки. Ну — для этого тесты и существуют.
И единственный способ решить проблему — как-то заменить настоящие ракеты ненастоящими, так чтобы можно было их запускать-перезапускать.
По научному это называется — «замокать» (mock)
Вопрос только — как!
Но для ответа на этот вопрос — в начале надо посмотреть на коды самого СкайНета.
// dooms-day.js
import {theDay} from './doom-scheduler'
import {Launch} from './rocket-silo';
theDay
.then(Launch)
.then( () => alert(' :) '),
() => alert(' :( next time, you know...');
// --------------------
// rocket-silo.js
import SkyNetCore from './core';
export const Launch = () => SkyNetCore.hackRockets().then(rocket => rocket.launch)
По коду выходит — для того чтобы протестировать SkyNet требуется заменить зависимости центрального файла «dooms-day.js» на контролируемые сущности.
Так как же замокать?
Я рад что вы спросили. И одного простого ответа у меня нет, но есть 5 еще более простых. Еще куча различных библиотек и парочка патернов с антипатернами о том как мокать хорошо и как мокать плохо.
Но лучше начать с быстрого обзора популярных библиотек, которые предоставляют разные интерфейсы для моканья зависимостей.
Вариант 1 — в тестах использовать «тестовые» варианты модулей
Звучит просто, и просто на самом деле. Надо просто создать «замены», и «заменить».
//__mocks__ /doom-scheduler.js -> заменяет doom-scheduler.js
export const theDay = Promise.resolve();
//__mocks__/rocket-silo.js -> заменяет rocket-silo.js
export const Launch = jest.fn(); //this is "replacement" code
// А это тест
import {Launch} from './rocket-silo'; // только silo уже не настоящий
import {theDay} from './doom-scheduler'; // и тут магия свершилась
import './dooms-day.js'; // самый настоящий dooms-day
expect(Launch).toHaveBeenCalled();
В общем "__mock__" очень простой и удобный механизм, у которого есть только один минус — Jest. Эта фича работает только в Jest, и если Скайнет использует mocha, ava или karma для запуска тестов — прийдется искать другие интерфейсы.
Вариант 2 — в тестах просто заменить настоящие модули на «тестовые»
Если уж магия __mock__ нам не доступна, то что на счет просто mock?
import {Launch} from './rocket-silo'; // вроде как настоящий
import {theDay} from './doom-scheduler'; // вроде как настоящий
import './dooms-day.js';
jest.mock('./rocket-silo'); // а тут мы мокаем что-то уже имопртированное
jest.mock('./doom-scheduler');
theDay.mockResolvedValue("comming!"); // и указываем на что заменять
expect(Launch).toHaveBeenCalled();
Опять же очень простой способ заменить зависимости для тестового файла, и опять же — только Jest.
На самом деле на этом возможности Jest заканчиваются, и не смотря на то что эти два подхода достаточно примитивны (и просты) — это примерно то что и нужно. Возможно все остальное — нетребуемые переусложения.
Вариант 3 — хочу Шварцнейгера заменить на Сталоне
Что если можно загрузить какой либо файл, но с «измененными» частями?
import proxyquire from 'proxyquire';
import sinon from 'sinon';
const Launch= sinon.stub()
const case = proxyquire.load('./dooms-day.js',{
'./rocket-silo', { Launch },
'./doom-scheduler', { theDay: Promise.resolve()
});
expect(Launch).toHaveBeenCalled();
Примерно так работает proxyquire — одна из самых древних популярных библиотек про это дело. Позволяет загрузить любой файл с перекрытием зависимостей непосредственно запрашиваемых из этого файла.
Главное преимущество перед jest.mock — в одном тесте можно иметь 100500 различных proxyquired поразному перекрытых файлов. Плюс различные команды типа .callThough, которая позволяет только частично перекрывать зависимость (что вообще антипатерн).
Вариант 4 — тоже самое, только более декларативно
Proxyquire иногда может быть немного надоедлив, плюс изредка хочется чего-то типа __mock__, те наличия некоторых «стандартных» моков. В общем mockery!
import mockery from 'mockery';
import sinon from 'sinon';
mockery.registerMock('./rocket-silo', {
Launch: sinon.stub()
});
mockery.registerMock('doom-scheduler', {
theDay: Promise.resolve()
});
mockery.enable();
const {Launch} = require('./rocket-silo'); // import использовать нельзя
mockery.disable();
expect(Launch).toHaveBeenCalled();
Mockery так же обладает мощьным API, но немного в другую сторону чем proxyquire. Например там есть режим «изоляции», когда mockery начинает ругаться при подключении «неразрешенных» файлов, что может спасти от случайного изменения зависимости и логики.
Вариант 5 — Последний из магикан
TestDouble.js — библиотека от одноименной компании, которая очень много сил уделяет именно «правильности» того или иного подхода. Технически — это jest.mock, вид сбоку.
import td from 'testdouble';
const {Launch} = td.replace('./rocket-silo'); // automock
const scheduler = td.replace('./doom-scheduler', { theDay: Promise.resolve() })
require('./dooms-day.js');
td.verify(Launch());
При этом возможность замены произвольных модулей появилось после очень очень долгих споров.
Вообще TD — кладезь знаний и лучших практик. Советую почитать раздел про моки.
Но есть проблемы
- Jest моки туповаты и работают только если у вас Jest.
- Proxyquire работает на соплях. Библиотека с нулевым фидбэком. Плюс очень многие решения, которые были логичны на момент создания — сейчас вызывают нервный тик (noPreserveCache)
- Mockery не имеет всех возможностей Proxyquire, плюс режим изоляции, одна из фич библиотеки, сломана так чтобы совсем
- Вы никогда не знаете что же мокаете в Proxyquire или Mockery, так как они агрятся на точное соовествие имени файла, а у нас бабель.
- TD обычно используется теми кто использует TD, что редкое явление, потому что у всех sinon.
Стоит добавить что все эти библиотеки еще плохо обращаются с кешом, что иногда приводит к серьезному замедлению работы тестов.
Для большого проекта — где-то секунда для исполнения require, так как до и после весь кеш nodejs(и бабеля) стирается
Так как протестировать SkyNet?
Не стоит забывать — вся проблема в том, что SkyNet хотелось совершенства. А все разобранные инструменты — не совершенны. Если мы начнем писать тесты с их использованием — SkyNet с начала пошлет терминаторов за нами.
Чтобы не попасть на счетчик хотелось моки как в jest, только под ava. Proxyquire синтаксис часто может удобен, как и автоматически моки jest.mock/td.replace. Мне много чего хотелось, так что я отдолжил с работы машину времени и еще год назад выложил библиотеку которая все вышеперечисленное умеет.
Заменяем jest.mock
import rewiremock from 'rewiremock';
import {Launch} from './rocket-silo';
import {theDay} from './doom-scheduler'
import './dooms-day.js';
// prev jest.mock('doom-scheduler');
rewiremock('./rocket-silo').mockThrough();
rewiremock('doom-scheduler').mockThrough();
theDay.resolves("comming!"); // sinon
expect(Launch).toHaveBeenCalled();
Заменяем mockery
import rewiremock from 'rewiremock';
import sinon from 'sinon';
// mockery.registerMock('./rocket-silo', {
// Launch: sinon.stub()
// });
rewiremock('./rocket-silo').with({
Launch: sinon.stub()
});
rewiremock('doom-scheduler').with({
theDay: Promise.resolve()
});
rewiremock.enable();
require('./dooms-day.js');
rewiremock.disable();
expect(Launch).toHaveBeenCalled();
Заменяет proxyquire
import rewiremock from 'rewiremock';
import sinon from 'sinon';
const Launch = sinon.stub()
// const case = proxyquire.load('./dooms-day.js',{
const case = rewiremock.proxy('./dooms-day.js',{
'./rocket-silo': { Launch },
'doom-scheduler': { theDay: Promise.resolve()}
});
expect(Launch).toHaveBeenCalled();
Заменяет TD
import rewiremock from 'rewiremock';
const {Launch} = rewiremock('./rocket-silo').mockThrough(); // automock
const scheduler = rewiremock('doom-scheduler').with({ theDay: Promise.resolve() })
rewiremock.proxy('./dooms-day.js'); // ну почти
expect(Launch).toHaveBeenCalled();
При этом это работает везде — mocha, ava, karma под node.js или webpack. При этом совершенно по другому работает с кешом, не удаляя ничего тестами не затронутое (иногда в 100 раз быстрее). При этом всегда есть API, например чтобы быть увереным что моки используются правильно:
import rewiremock from 'rewiremock';
import sinon from 'sinon';
rewiremock('./rocket-silo')
.with({ Launch: sinon.stub()});
rewiremock('./rockets')
.toBeUsed();
// ....
rewiremock.enable();
require('./dooms-day');
rewiremock.disable();
// will throw an Error - "rockets were not used", as long we mock out Silo.
Или чтобы заменить один модуль другим (как mockery умеет)
import rewiremock from 'rewiremock';
import sinon from 'sinon';
const Launch = sinon.stub()
const case = rewiremock.proxy('./dooms-day.js',{
'./rocket-silo': rewiremock.with({Launch}).toBeUsed().directChildOnly(), // "real" proxyquire
'doom-scheduler': rewiremock.by('mocked-doom-scheduler')
});
expect(Launch).toHaveBeenCalled();
Или чтобы использовать import заместо require, что может быть полезно с точки зрения name resolution и type safery.
rewiremock(() => import('doom-scheduler')).with({
theDay: Promise.resolve()
});
// rewiremock async API.
await rewiremock.module('./dooms-day.js');
В общем rewiremock — это та библиотека изменения зависимостей, которая СкайНету подойдет. Особенно учитывая постоянное использование машины времени для улучшения работы и выпуска новых версий.
И, самое главное, это и есть ваша текущая библиотека. Интерфейсно совместима.
И сейчас моя миссия — как-то пересадить людей с proxyquire и mockery (и более мелких продуктов) на что-то более юзабельное. Просто потому что старина Арни был «old, but not obsolete» в 2015, и с тех пор обновлений не получал. Как и proxyquire, как и mockery.
Для справки:
> Jest mocks
> Mockery
> Proxyquire
> TD.js (забудьте почитать вики странички)
Ну и самое главное: rewiremock
PS: И это уже не первая статья о данной библиотеке — остальные тоже могут быть полезны.
asm679
Мил человек, в Ваш СкайНет грохнется ещё до первого теста, если в слове «сущность» вы срочно не удалите мягкий знак между Ща и эН…