angularjs - que - ¿Es posible anular constantes para las funciones de configuración del módulo en las pruebas?
ng controller que es (5)
He pasado bastante tiempo golpeando mi cabeza en contra de intentar anular las constantes inyectadas proporcionadas a las funciones de configuración de los módulos. Mi código se ve algo como
common.constant(''I18n'', <provided by server, comes up as undefined in tests>);
common.config([''I18n'', function(I18n) {
console.log("common I18n " + I18n)
}]);
Nuestra forma habitual de garantizar lo que se inyecta I18n en nuestras pruebas de unidad es haciendo
module(function($provide) {
$provide.constant(''I18n'', <mocks>);
});
Esto funciona bien para mis controladores, pero parece que la función de configuración no se ve en lo que $provide
d fuera del módulo. En lugar de obtener los valores simulados, obtiene el valor anterior definido como parte del módulo. (No definido en el caso de nuestras pruebas; en el desplumador a continuación, ''foo''.)
Un desplumador de trabajo está debajo (mira la consola); ¿Alguien sabe lo que estoy haciendo mal?
Aunque parece que no puede cambiar a qué objeto se refiere una constante de AngularJS después de haberlo definido, puede cambiar las propiedades del objeto en sí.
Entonces, en su caso, puede inyectar I18n
como lo haría con cualquier otra dependencia, y luego modificarlo antes de su prueba.
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;
});
});
Como se indicó anteriormente, debe guardar el estado original de la constante y restaurarlo después de la prueba, para asegurarse de que esta prueba no interfiera con ninguna otra que pueda tener, ahora o en el futuro.
Creo que el problema fundamental es que estás definiendo las constantes justo antes del bloque de configuración, por lo que cada vez que se cargue el módulo, se anulará cualquier valor simulado que pueda existir. Mi sugerencia sería separar las constantes y configurar en módulos separados.
En primer lugar: parece que el jazmín no funciona correctamente en su plunkr. Pero no estoy muy seguro, tal vez alguien más pueda revisar esto nuevamente. Sin embargo, he creado un nuevo plunkr ( http://plnkr.co/edit/MkUjSLIyWbj5A2Vy6h61?p=preview ) y seguí estas instrucciones: https://github.com/searls/jasmine-all .
Verás que tu código de beforeEach
nunca se ejecutará. Puede comprobar esto:
module(function($provide) {
console.log(''you will never see this'');
$provide.constant(''I18n'', { FOO: "bar"});
});
Necesitas dos cosas:
Una prueba real en la función
it
-expect(true).toBe(true)
es suficientemente buenaDebe usar
inject
en algún lugar de su prueba, de lo contrario, la función proporcionada almodule
no se llamará y la constante no se establecerá.
Si ejecuta este código verá "verde":
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'');
}));
});
Espero que funcione como esperas!
Puede anular una definición de módulo. Solo estoy lanzando esto como una variación más.
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'';
});
La redefinición de un módulo eliminará el módulo previamente definido, que a menudo se realiza en forma accidental, pero en este escenario se puede utilizar para su ventaja (si tiene ganas de empaquetar cosas de esa manera). Solo recuerde que cualquier otra cosa definida en ese módulo también será eliminada, así que probablemente querrá que sea un módulo de constantes únicas, y esto puede ser una exageración para usted.
Voy a caminar a través de una solución más desagradable como una serie de pruebas anotadas. Esta es una solución para situaciones donde la sobrescritura de módulos no es una opción . Esto incluye los casos en los que la receta de la constante original y el bloque de configuración pertenecen al mismo módulo, así como los casos en que la constante la emplea un constructor proveedor.
Puede ejecutar el código en línea en SO (impresionante, ¡esto es nuevo para mí!)
Tenga en cuenta las advertencias sobre la restauración del estado anterior después de la especificación. No recomiendo este enfoque a menos que ambos (a) tengan una buena comprensión del ciclo de vida del módulo Angular y (b) estén seguros de que no pueden probar algo de otra manera. Las tres colas de módulos (invocar, configurar, ejecutar) no se consideran API públicas, pero , por otro lado, han sido consistentes a lo largo de la historia de Angular.
Puede que haya una mejor manera de abordar esto, realmente no estoy seguro, pero es la única que he encontrado hasta la fecha.
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="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.5.2/jasmine.css">
</head>
<body>
</body>
</html>
Ahora, puede que se pregunte por qué uno haría esto en primer lugar. El OP en realidad describe un escenario muy común que Angular + Karma + Jasmine no puede abordar. El escenario es que hay algún valor de configuración provisto por la ventana que determina el comportamiento de la aplicación, como, por ejemplo, habilitar o deshabilitar el "modo de depuración", y usted necesita probar qué sucede con diferentes dispositivos, sin embargo, esos valores, que generalmente se usan para la configuración, son necesarios Temprano. Podemos suministrar estos valores de ventana como accesorios y luego encaminarlos a través de la receta constante del módulo para ''angularizarlos'', pero solo podemos hacerlo una vez , porque Karma / Jasmine normalmente no nos brinda un entorno nuevo por prueba o incluso por especificación. . Esto está bien cuando el valor se va a utilizar en la fase de ejecución, pero de manera realista, el 90% de las veces, los indicadores ambientales de este tipo serán de interés tanto en la fase de configuración como en los proveedores.
Probablemente podría abstraer este patrón en una función auxiliar más robusta para reducir las posibilidades de alterar el estado del módulo de línea de base.