Quando eu comecei a aprender sobre testes de aplicações em Angular, eu achei surpreendentemente difícil de entender. Em particular, eu continuei preso na estranha sintaxe que era necessária para configurar as dependências de teste.
Como se vê, não há nada de especial e difícil sobre injeção de dependência e testes com Angular. Você só precisa se acostumar com a sintaxe. Uma maneira de fazer isso é fingir que é outra coisa. Permitam-me explicar.
Teste com Angular no modo fácil (mas inexistente)
Vamos imaginar que estamos escrevendo um aplicativo Pastry Shop. Nosso aplicativo precisa de um serviço para fazer bolos, que chamaremos de Baker. O serviço Baker requer algumas coisas para fazer o seu trabalho. Por exemplo, ele precisa ter acesso a uma Store (loja) onde se pode obter os ingredientes necessários. Ele também precisa de um Mixer (batedeira) e um Oven (forno). Em outras palavras, ele tem certas dependências.
Agora, vamos imaginar que estamos usando uma versão especial do JavaScript, que torna mais fácil expressar as dependências usando anotações. É assim que o nosso serviço Baker pode parecer:
@inject(Store, Mixer, Oven) function Baker() { var bake = function(item) { if (item === 'vanilla cake') { var ingredients = [ Store.get('sugar'), Store.get('flower'), Store.get('eggs') ]; var dough = Mixer.mix(ingredients); var cake = Oven.cook(dough); cake.frosting = 'vanilla'; return cake; }; }; return: { bake: bake }; };
Estamos usando uma anotação chamada @inject para especificar que Baker precisa de Store, Mixer e Oven para fazer o seu trabalho. Essa mesma anotação poderia ser usada para escrever os testes correspondentes:
@inject(Baker, Store, Mixer, Oven) describe('Baker Service tests', function () { it('should bake vanilla cake', function () { // arrange spyOn(Store, 'get').andReturnValue({}); spyOn(Mixer, 'mix').andReturnValue({}); spyOn(Oven, 'cook').andReturnValue({}); // act var cake = Baker.bake('cake'); // assert expect(Store.get).toHaveBeenCalledWith('sugar'); expect(Store.get).toHaveBeenCalledWith('flower'); expect(Store.get).toHaveBeenCalledWith('baking soda'); expect(Store.get).toHaveBeenCalledWith('eggs'); expect(Mixer.mix).toHaveBeenCalled(); expect(Oven.cook).toHaveBeenCalled(); expect(cake.frosting).toEqual('vanilla'); }); });
Teste com Angular no modo (menos) fácil (mas realmente possível)
Infelizmente, o código acima não vai funcionar em JavaScript. A boa notícia é que o Angular nos oferece várias maneiras de fazer a mesma coisa:
- Dependências implícitas – use os nomes dos parâmetros da função para indicar as dependências. Este método não funciona para código minimizado e deve, portanto, ser evitado.
- $inject – configura a propriedade $inject em um objeto que requer dependências de um array de nomes de dependência.
- Anotação de array em linha – transmite a lista de dependências como um array para a função constructor (como factory() ou controller()).
Dos três métodos acima descritos, o número 3 parece ser o mais aceito. Então, vamos reescrever o nosso código acima usando esse método. Primeiro, vamos dar uma olhada no serviço:
// simple, but not possible @inject(Store, Mixer, Oven) function Baker() {...} // noisy, but possible angular.module('PastryShop').factory('Baker', ['Store', 'Mixer', 'Oven', function(Store, Mixer, Oven) {...} ]);
Em seguida, vamos reescrever o teste:
// simple, but not possible @inject(Store, Mixer, Oven) describe('Baker Service tests', function () { it('should bake vanilla cake', function () {...}); }); // noisy, but possible describe('Baker Service tests', function () { var Baker, Store, Mixer, Oven; beforeEach(angular.mock.module('PastryShop')); beforeEach(function () { angular.mock.inject(function (_Baker_, _Store_, _Mixer_, _Oven_) { Baker = _Baker_; Store = _Store_; Mixer = _Mixer_; Oven = _Oven_; }); }); it('should bake vanilla cake', function () {...}); });
Vamos entender o que está acontecendo aqui. O Angular fornece um módulo especial chamado ngMock, que expõe (entre outras coisas) dois métodos muito úteis:
- module() – usado para iniciar seu aplicativo antes de cada teste.
- inject() – usado para obter instâncias de vários serviços injetadas em seus testes. Por convenção, se você fizer um wrap no nome do serviço em underscores, o Angular vai retirar esses underscores antes de te dar o serviço certo.
Assim que obtivermos os serviços necessários do inject(), vamos simplesmente armazená-los no escopo do nosso bloco describe(), e eles se tornarão disponíveis para todos os nossos testes.
Uma nota sobre controllers
Existem algumas coisas a mais que temos que levar em consideração ao testar controllers. Em primeiro lugar, para realmente criar um controller para nossos testes, precisamos usar o serviço $controller. Em segundo lugar, uma vez que a maioria dos controllers precisam de um $scope para trabalhar, podemos obter um usando o método $rootScope do $new().
Aqui está o código:
describe('Baker Controller tests', function () { var scope, BakerController, Baker; beforeEach(angular.mock.module('PastryShop')); beforeEach(function () { angular.mock.inject(function ($rootScope, $controller, _Baker_) { scope = $rootScope.$new(); Baker = _Baker_; BakerController = $controller('BakerController', { $scope: scope, Baker: Baker }); }); }); });
Mais uma vez, estamos usando o método inject() para obter as nossas dependências, que nesse caso incluem $rootScope e $controller. Nós criamos uma instância de um $scope e o injetamos, juntamente com uma instância Baker, no BakerController.
***
Artigo traduzido pela Redação iMasters com autorização do autor. Publicado originalmente em http://tatiyants.com/groking-angular-dependency-injection-and-testing/