Можно ли переопределить константы для функций конфигурации модуля в тестах?
Я потратил довольно много времени, ударяя головой о попытку переопределить введенные константы, предоставляемые в конфигурационные функции модулей. Мой код выглядит примерно как
common.constant('I18n', <provided by server, comes up as undefined in tests>);
common.config(['I18n', function(I18n) {
console.log("common I18n " + I18n)
}]);
Наш обычный способ гарантировать, что I18n вводится в наши модульные тесты, - это сделать
module(function($provide) {
$provide.constant('I18n', <mocks>);
});
Это отлично работает для моих контроллеров, но похоже, что функция config не смотрит на то, что $provide
d вне модуля. Вместо того, чтобы получать издеваемое значение, оно получает более раннее значение, определенное как часть модуля. (Undefined в случае наших тестов, в нижнем плункере, 'foo'.)
Рабочий плункер находится ниже (смотрите консоль); кто-нибудь знает, что я делаю неправильно?
http://plnkr.co/edit/utCuGmdRnFRUBKGqk2sD
Ответы
Ответ 1
Прежде всего: кажется, что жасмин не работает должным образом в вашем plunkr. Но я не совсем уверен - может быть, кто-то еще сможет это проверить. Тем не менее я создал новый plunkr (http://plnkr.co/edit/MkUjSLIyWbj5A2Vy6h61?p=preview) и выполнил следующие инструкции: https://github.com/searls/jasmine-all.
Вы увидите, что ваш код beforeEach
никогда не будет запущен. Вы можете проверить это:
module(function($provide) {
console.log('you will never see this');
$provide.constant('I18n', { FOO: "bar"});
});
Вам нужны две вещи:
-
Действительный тест в функции it
- expect(true).toBe(true)
достаточно хорош
-
Вы должны использовать inject
где-то в своем тесте, иначе функция, предоставляемая module
, не будет вызываться, а константа не будет установлена.
Если вы запустите этот код, вы увидите "зеленый":
var common = angular.module('common', []);
common.constant('I18n', 'foo');
common.config(['I18n', function(I18n) {
console.log("common I18n " + I18n)
}]);
var app = angular.module('plunker', ['common']);
app.config(['I18n', function(I18n) {
console.log("plunker I18n " + I18n)
}]);
describe('tests', function() {
beforeEach(module('common'));
beforeEach(function() {
module(function($provide) {
console.log('change to bar');
$provide.constant('I18n', 'bar');
});
});
beforeEach(module('plunker'));
it('anything looks great', inject(function($injector) {
var i18n = $injector.get('I18n');
expect(i18n).toBe('bar');
}));
});
Я надеюсь, что он будет работать так, как вы ожидаете!
Ответ 2
Хотя кажется, что вы не можете изменить, к какому объекту относится константа AngularJS после того, как он был определен, вы можете изменить свойства самого объекта.
Итак, в вашем случае вы можете ввести I18n
, как и любую другую зависимость, а затем изменить ее перед тестированием.
var I18n;
beforeEach(inject(function (_I18n_) {
I18n = _I18n_;
});
describe('A test that needs a different value of I18n.foo', function() {
var originalFoo;
beforeEach(function() {
originalFoo = I18n.foo;
I18n.foo = 'mock-foo';
});
it('should do something', function() {
// Test that depends on different value of I18n.foo;
expect(....);
});
afterEach(function() {
I18n.foo = originalFoo;
});
});
Как и выше, вы должны сохранить исходное состояние константы и восстановить ее после теста, чтобы убедиться, что этот тест не мешает другим, которые у вас могут быть, сейчас или в будущем.
Ответ 3
Я думаю, что основная проблема заключается в том, что вы определяете константы прямо перед блоком конфигурации, поэтому каждый раз, когда модуль загружается, любое значение макета, которое может существовать, будет переопределено. Мое предложение состояло в том, чтобы разделить константы и конфигурацию на отдельные модули.
Ответ 4
Вы можете переопределить определение модуля. Я просто выбрасываю это в качестве еще одного варианта.
angular.module('config', []).constant('x', 'NORMAL CONSTANT');
// Use or load this module when testing
angular.module('config', []).constant('x', 'TESTING CONSTANT');
angular.module('common', ['config']).config(function(x){
// x = 'TESTING CONSTANT';
});
Переопределение модуля уничтожит ранее определенный модуль, часто выполняемый при аварии, но в этом сценарии можно использовать в ваших интересах (если вам так хочется упаковать вещи). Просто помните, что все, что было определено в этом модуле, также будет уничтожено, поэтому вы, вероятно, захотите, чтобы он был модулем с константами, и это может быть излишним для вас.
Ответ 5
Я собираюсь пройти через более неприятное решение в виде серии аннотированных тестов. Это решение для ситуаций , где перезапись модуля не является вариантом. Это включает случаи, когда исходный константный рецепт и блок конфигурации принадлежат одному модулю, а также случаи, когда константа используется конструктором поставщика.
Вы можете запустить код inline на SO (удивительный, это новый для меня!)
Обратите внимание на предостережения о восстановлении предыдущего состояния после спецификации. Я не рекомендую этот подход, если вы оба не хорошо понимаете жизненный цикл модуля Angular и (b) уверены, что вы не можете что-то проверить каким-либо другим способом. Три очереди модулей (invoke, config, run) не считаются общедоступными API, но, с другой стороны, они были согласованы в истории Angular.
Там может быть лучший способ приблизиться к этому - Im действительно не уверен - но это единственный способ, который я нашел на сегодняшний день.
angular
.module('poop', [])
.constant('foo', 1)
.provider('bar', class BarProvider {
constructor(foo) {
this.foo = foo;
}
$get(foo) {
return { foo };
}
})
.constant('baz', {})
.config((foo, baz) => {
baz.foo = foo;
});
describe('mocking constants', () => {
describe('mocking constants: part 1 (what you can and can’t do out of the box)', () => {
beforeEach(module('poop'));
it('should work in the run phase', () => {
module($provide => {
$provide.constant('foo', 2);
});
inject(foo => {
expect(foo).toBe(2);
});
});
it('...which includes service instantiations', () => {
module($provide => {
$provide.constant('foo', 2);
});
inject(bar => {
expect(bar.foo).toBe(2);
});
});
it('should work in the config phase, technically', () => {
module($provide => {
$provide.constant('foo', 2);
});
module(foo => {
// Code passed to ngMock module is effectively an added config block.
expect(foo).toBe(2);
});
inject();
});
it('...but only if that config is registered afterwards!', () => {
module($provide => {
$provide.constant('foo', 2);
});
inject(baz => {
// Earlier we used foo in a config block that was registered before the
// override we just did, so it did not have the new value.
expect(baz.foo).toBe(1);
});
});
it('...and config phase does not include provider instantiation!', () => {
module($provide => {
$provide.constant('foo', 2);
});
module(barProvider => {
expect(barProvider.foo).toBe(1);
});
inject();
});
});
describe('mocking constants: part 2 (why a second module may not work)', () => {
// We usually think of there being two lifecycle phases, 'config' and 'run'.
// But this is an incomplete picture. There are really at least two more we
// can speak of, ‘registration’ and ‘provider instantiations’.
//
// 1. Registration — the initial (usually) synchronous calls to module methods
// that define services. Specifically, this is the period prior to app
// bootstrap.
// 2. Provider preparation — unlike the resulting services, which are only
// instantiated on demand, providers whose recipes are functions will all
// be instantiated, in registration order, before anything else happens.
// 3. After that is when the queue of config blocks runs. When we supply
// functions to ngMock module, it is effectively like calling
// module.config() (likewise calling `inject()` is like adding a run block)
// so even though we can mock the constant here successfully for subsequent
// config blocks, it’s happening _after_ all providers are created and
// after any config blocks that were previously queued have already run.
// 4. After the config queue, the runtime injector is ready and the run queue
// is executed in order too, so this will always get the right mocks. In
// this phase (and onward) services are instantiated on demand, so $get
// methods (which includes factory and service recipes) will get the right
// mock too, as will module.decorator() interceptors.
// So how do we mock a value before previously registered config? Or for that
// matter, in such a way that the mock is available to providers?
// Well, if the consumer is not in the same module at all, you can overwrite
// the whole module, as others have proposed. But that won’t work for you if
// the constant and the config (or provider constructor) were defined in app
// code as part of one module, since that module will not have your override
// as a dependency and therefore the queue order will still not be correct.
// Constants are, unlike other recipes, _unshifted_ into the queue, so the
// first registered value is always the one that sticks.
angular
.module('local-mock', [ 'poop' ])
.constant('foo', 2);
beforeEach(module('local-mock'));
it('should still not work even if a second module is defined ... at least not in realistic cases', () => {
module((barProvider) => {
expect(barProvider.foo).toBe(1);
});
inject();
});
});
describe('mocking constants: part 3 (how you can do it after all)', () => {
// If we really want to do this, to the best of my knowledge we’re going to
// need to be willing to get our hands dirty.
const queue = angular.module('poop')._invokeQueue;
let originalRecipe, originalIndex;
beforeAll(() => {
// Queue members are arrays whose members are the name of a registry,
// the name of a registry method, and the original arguments.
originalIndex = queue.findIndex(([ , , [ name ] ]) => name === 'foo');
originalRecipe = queue[originalIndex];
queue[originalIndex] = [ '$provide', 'constant', [ 'foo', 2 ] ];
})
afterAll(() => {
queue[originalIndex] = originalRecipe;
});
beforeEach(module('poop'));
it('should work even as far back as provider instantiation', () => {
module(barProvider => {
expect(barProvider.foo).toBe(2);
});
inject();
});
});
describe('mocking constants: part 4 (but be sure to include the teardown)', () => {
// But that afterAll is important! We restored the initial state of the
// invokeQueue so that we could continue as normal in later tests.
beforeEach(module('poop'));
it('should only be done very carefully!', () => {
module(barProvider => {
expect(barProvider.foo).toBe(1);
});
inject();
});
});
});
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>AngularJS Plunker</title>
<script>document.write('<base href="' + document.location + '" />');</script>
<link href="style.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.5.2/jasmine.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.5.2/jasmine-html.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.5.2/boot.js"></script>
<script src="https://code.angularjs.org/1.6.0-rc.2/angular.js"></script>
<script src="https://code.angularjs.org/1.6.0-rc.2/angular-mocks.js"></script>
<script src="app.js"></script>
<link rel="stylesheet" href="#" onclick="location.href='https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.5.2/jasmine.css'; return false;">
</head>
<body>
</body>
</html>