Это будет длинный ответ, касающийся модульного тестирования, заглушек и sinon.js (в некоторой степени).
(Если вы хотите пропустить этот шаг, прокрутите вниз до заголовка №3 и посмотрите на окончательную реализацию вашей спецификации)
1. Установите цель
Я хочу убедиться, что пользователь, которого передают, тот же, что и в параметре разрешения модального окна.
Отлично, значит, у нас есть цель.
Ожидается, что возвращаемое значение resolve { user: fn }
$modal.open
будет пользователем, которого мы передали в метод $scope.showProfile
.
Учитывая, что $modal
является внешней зависимостью в вашей реализации, нас просто не волнует внутренняя реализация $modal
. Очевидно, что мы не хотим внедрять настоящий сервис $modal
в наш набор тестов.
Посмотрев на свой набор тестов, вы, кажется, уже поняли это (милый!), поэтому нам не придется касаться причин, лежащих в основе этого слишком.
Я полагаю, что первоначальная формулировка ожидания будет выглядеть примерно так:
Должен был быть вызван $modal.open, а его функция resolve.user должна вернуть пользователя, переданного в $scope.showProfile.
2. Подготовка
Сейчас я собираюсь вырезать многое из вашего набора тестов, чтобы сделать его немного более читабельным. Приношу свои извинения, если отсутствуют какие-либо детали, жизненно важные для прохождения спецификации.
перед каждым
Я бы начал с упрощения блока beforeEach
. Гораздо чище иметь один блок beforeEach
для каждого блока описания, это упрощает читаемость и уменьшает шаблонный код.
Ваш упрощенный блок beforeEach может выглядеть примерно так:
var $scope, $modal, createController; // [1]: createController(?)
beforeEach(function () {
$modal = {}; // [2]: empty object?
module('myApp', function ($provide) {
$provide.value('$modal', $modal); // [3]: uh?
});
inject(function ($controller, $injector) { // [4]: $injector?
$scope = $injector.get('$rootScope').$new();
$modal = $injector.get('$modal');
createController = function () { // [5(1)]: createController?!
return $controller('profileController', {
$scope: $scope
$modal: $modal
});
};
});
// Mock API's
$modal.open = sinon.stub(); // [6]: sinon.stub()?
});
Итак, некоторые заметки о том, что я добавил/изменил:
[1]: createController
— это то, что мы установили в моей компании уже довольно давно, когда писали модульные тесты для контроллеров angular. Это дает вам большую гибкость в изменении указанных зависимостей контроллеров для каждой спецификации.
Предположим, что в реализации контроллера у вас было следующее:
.controller('...', function (someDependency) {
if (!someDependency) {
throw new Error('My super important dependency is missing!');
}
someDependency.doSomething();
});
Если вы хотели написать тест для throw
, но отказались от метода createController
, вам нужно настроить отдельный блок describe
с собственным вызовом beforeEach|before
для установки someDependency = undefined
. Большая проблема!
С "отложенным $inject" это так же просто, как:
it('throws', function () {
someDependency = undefined;
function fn () {
createController();
}
expect(fn).to.throw(/dependency missing/i);
});
[2]: пустой объект. Заменяя глобальную переменную пустым объектом в начале вашего блока beforeEach, мы можем быть уверены, что все оставшиеся методы из предыдущей спецификации умерли. сильный>
[3]: $предоставить с помощью $providing
смоделированного (на данный момент пустого) объекта в качестве значения для нашего module
нам не нужно загружать модуль, содержащий реальную реализацию $modal
.
По сути, это делает модульное тестирование углового кода легким делом, так как вы никогда снова не столкнетесь с Error: $injector:unpr Unknown Provider
в своих модульных тестах, просто убив все без исключения ссылки на un- интересный код для гибкого, сфокусированного модульного теста.
[4]: $injector Я предпочитаю использовать $injector, так как он уменьшает количество аргументов, которые необходимо передать методу inject()
, практически до нуля. Делай здесь, что хочешь!
[5]: createController Прочтите №1.
[6]: sinon.stub В конце вашего блока beforeEach
я бы посоветовал вам снабдить все ваши заглушенные зависимости необходимыми методами. Заглушенные методы.
Если вы уверены, что метод-заглушка должен всегда возвращаться, скажем, разрешенное обещание — вы можете изменить эту строку на:
dependency.mockedFn = sinon.stub().returns($q.when());
// dont forget to expose, and $inject -> $q!
Но в целом я бы рекомендовал явные операторы возврата в отдельных it()
.
3. Написание спецификации
Итак, вернемся к рассматриваемой проблеме.
Учитывая вышеупомянутый блок beforeEach
, ваш describe/it
может выглядеть примерно так:
describe('displaying a user profile', function () {
it('matches the passed in user details', function () {
createController();
});
});
Казалось бы, нам нужно следующее:
- Пользовательский объект.
- Звонок
$scope.showProfile
.
- Ожидание возвращаемого значения функции разрешения вызванного $modal.open.
Проблема заключается в том, что мы тестируем что-то, что не в наших руках. То, что $modal.open()
делает за кулисами, не входит в набор спецификаций для вашего контроллера - это зависимость, а зависимости заглушаются.
Однако мы можем проверить, вызывал ли наш контроллер $modal.open
с правильными параметрами, но связь между resolve
и controller
не является частью этого набора спецификаций (подробнее об этом позже).
Итак, чтобы пересмотреть наши потребности:
- Пользовательский объект.
- Звонок
$scope.showProfile
.
- Ожидание от параметров, переданных в $modal.open.
it('calls $modal.open with the correct params', function () {
// Preparation
var user = { name: 'test' };
var expected = {
templateUrl: 'components/profile/profile.html',
resolve: {
user: sinon.match(function (value) {
return value() === user;
}, 'boo!')
},
controller: sinon.match.any
};
// Execution
createController();
$scope.showProfile(user);
// Expectation
expect($modal.open).to.have
.been.calledOnce
.and.calledWithMatch(expected);
});
Я хочу убедиться, что пользователь, которого передают, тот же, что и в параметре разрешения модального окна.
"Должен быть создан экземпляр $modal.open, а его функция resolve.user должна возвращать пользователя, переданного в $scope.showProfile."
Я бы сказал, что наша спецификация покрывает именно это — и мы «отменили» $modal для загрузки. Мило.
Объяснение пользовательских сопоставителей, взятое из документы sinonjs.
Пользовательские сопоставители создаются с помощью фабрики sinon.match
, которая принимает тестовую функцию и необязательное сообщение. Тестовая функция принимает значение в качестве единственного аргумента, возвращает true
, если значение соответствует ожидаемому, и false
в противном случае. Строка сообщения используется для генерации сообщения об ошибке в случае, если значение не соответствует ожидаемому.
По сути;
sinon.match(function (value) {
return /* expectation on the behaviour/nature of value */
}, 'optional_message');
Если вы абсолютно хотите проверить возвращаемое значение resolve
(значение, которое заканчивается в $modal controller
), я бы посоветовал вам протестировать контроллер изолированно, извлекая его в именованный контроллер, а не в анонимную функцию.
$modal.open({
// controller: function () {},
controller: 'NamedModalController'
});
Таким образом, вы можете написать ожидания для модального контроллера (конечно, в другом файле спецификаций) как таковые:
it('exposes the resolved {user} value onto $scope', function () {
user = { name: 'Mike' };
createController();
expect($scope).to.have.property('user').that.deep.equals(user);
});
Так вот, многое из этого было повторением — вы уже делаете многое из того, что я затронул, надеюсь, я не выйду из роли инструмента.
Некоторые подготовительные данные в it()
, который я предложил, можно было бы переместить в блок beforeEach
, но я бы посоветовал делать это только тогда, когда есть множество тестов, вызывающих один и тот же код.
Сохранение набора спецификаций СУХИМ не так важно, как сохранение ваших спецификаций в явном виде, чтобы избежать путаницы, когда другой разработчик придет, чтобы прочитать их и исправить некоторые регрессионные ошибки.
Чтобы завершить, некоторые из встроенных комментариев, которые вы написали в своем оригинале:
Синон.матч.любой
var modalOptions = {
resolve:{
agent:sinon.match.any // No idea if this is correct, trying to match jasmine.any(Function)
},
};
Если вы хотите сопоставить его с функцией, вы должны сделать:
sinon.match.func
, что эквивалентно jasmine.any(Function)
.
sinon.match.any
соответствует всему.
sinon.stub.yield([arg1, arg2])
// open cannot yield since it was not yet invoked.
modalSpy.yield(function(options){
actualOptions = options;
return fakeModal;
});
Прежде всего, у вас есть несколько методов на $modal
, которые (или должны быть) заглушены. Таким образом, я думаю, что маскировать $modal.open
под modalSpy
- плохая идея - не очень ясно, какой метод для yield
.
Во-вторых, вы смешиваете spy
с stub
(я делаю это все время...), когда ссылаетесь на свою заглушку как modalSpy
.
spy
оборачивает исходную функциональность и оставляет ее, записывая все «события» для предстоящих ожиданий, и на этом все.
stub
фактически является spy
, с той разницей, что мы можем изменить поведение указанной функции, указав .returns()
, .throws()
и т. д. Короче говоря; сочный шпион.
Как следует из сообщения об ошибке, функция не может yield
пока не будет вызвана.
it('yield / yields', function () {
var stub = sinon.stub();
stub.yield('throwing errors!'); // will crash...
stub.yields('y');
stub(function () {
console.log(arguments);
});
stub.yield('x');
stub.yields('ohno'); // wont happen...
});
Если бы мы удалили строку stub.yield('throwing errors!');
из этой спецификации, вывод выглядел бы так:
LOG: Object{0: 'y'}
LOG: Object{0: 'x'}
Коротко и приятно (это примерно все, что я знаю о урожайности/урожайности);
yield
после вызова вашего обратного вызова-заглушки/шпиона.
yields
перед вызовом вашего обратного вызова-заглушки/шпиона.
Если вы зашли так далеко, вы, вероятно, поняли, что я могу часами болтать на эту тему. К счастью, я устаю, и пришло время закрыть глаза.
Некоторые ресурсы, слабо относящиеся к теме:
person
Kasper Lewau
schedule
17.07.2015