Na semana passada, começamos a falar de Jasmine e a ver o que ele tinha para nos oferecer no que diz respeito a testes unitários. Hoje, nós vamos terminar isso com alguns dos recursos mais avançados incluídos no Jasmine, de modo que você possa ver todo o pacote e comece o testar a unidade do seu próprio JavaScript. Confie em mim, até mesmo suas características avançadas são simples de usar, então não deve ter qualquer coisa que te impeça de ler este artigo e começar a fazer o seu próprio teste unitário.
Spies e Mocks
Vamos começar com os espiões. Spies são muito legais e tiram proveito da natureza dinâmica do JavaScript para permitir que você obtenha alguns metadados interessantes sobre o que está acontecendo nos bastidores de alguns objetos. Por exemplo, se você está testando uma função que recebe um argumento callback, você pode querer ter certeza de que o callback era de fato chamado corretamente. Você pode espionar o método callback para ver se ele foi chamado e até mesmo quais os argumentos que foram chamados e quantas vezes foram chamados. Dê uma olhada abaixo para ver todas as coisas legais que você recebe ao usar spyOn, o método que você chama para espionar uma função. Este código é obtido diretamente a partir da documentação do Jasmine.
describe("A spy", function() { var foo, bar = null; beforeEach(function() { foo = { setBar: function(value) { bar = value; } }; spyOn(foo, 'setBar'); foo.setBar(123); foo.setBar(456, 'another param'); }); it("tracks that the spy was called", function() { expect(foo.setBar).toHaveBeenCalled(); }); it("tracks its number of calls", function() { expect(foo.setBar.calls.length).toEqual(2); }); it("tracks all the arguments of its calls", function() { expect(foo.setBar).toHaveBeenCalledWith(123); expect(foo.setBar).toHaveBeenCalledWith(456, 'another param'); }); it("allows access to the most recent call", function() { expect(foo.setBar.mostRecentCall.args[0]).toEqual(456); }); it("allows access to other calls", function() { expect(foo.setBar.calls[0].args[0]).toEqual(123); }); it("stops all execution on a function", function() { expect(bar).toBeNull(); }); });
É simples de usar spyOn; é só passar um objeto e o nome de um método sobre o objeto que você deseja espionar. Se olhar de perto, você pode perceber que spyOn está substituindo a função original com um espião que intercepta as chamadas de função e acompanha uma série de informações potencialmente úteis sobre elas. O problema que temos acima é que, uma vez que substituímos a função original, nós perdemos as suas capacidades. Podemos resolver isso com andCallThrough. Se você fizer um chain andCallThrough () depois de chamar spyOn, o espião então transmite todas as chamadas a ele através da função original. Aqui está outra parte de código de docs para mostrar andCallThrough:
describe("A spy, when configured to call through", function() { var foo, bar, fetchedBar; beforeEach(function() { foo = { setBar: function(value) { bar = value; }, getBar: function() { return bar; } }; spyOn(foo, 'getBar').andCallThrough(); foo.setBar(123); fetchedBar = foo.getBar(); }); it("tracks that the spy was called", function() { expect(foo.getBar).toHaveBeenCalled(); }); it("should not effect other functions", function() { expect(bar).toEqual(123); }); it("when called returns the requested value", function() { expect(fetchedBar).toEqual(123); }); });
Pode ser que você não queira chamar através do original. Talvez você só queira que o espião retorne um valor específico para que você possa testar para ver o que acontece quando esse valor é retornado. Ou talvez você só queira que ele retorne um único valor para garantir a consistência. Bem, você pode dizer um espião para retornar um valor especificado com andReturn. Ele é utilizado de forma semelhante ao andCallThrough, mas obviamente é usado para retornar um valor específico em vez de chamar através da função original. É preciso um único argumento, que é o valor a ser retornado.
describe("A spy, when faking a return value", function() { var foo, bar, fetchedBar; beforeEach(function() { foo = { setBar: function(value) { bar = value; }, getBar: function() { return bar; } }; spyOn(foo, 'getBar').andReturn(745); foo.setBar(123); fetchedBar = foo.getBar(); }); it("tracks that the spy was called", function() { expect(foo.getBar).toHaveBeenCalled(); }); it("should not effect other functions", function() { expect(bar).toEqual(123); }); it("when called returns the requested value", function() { expect(fetchedBar).toEqual(745); }); });
Para o método final andXxx espião, temos andCallfake, que terá um argumento da função. Em vez de passar pela função original, esse método irá fazer com que o espião passe a chamar a função que você especificou como argumento. Ele vai até retornar qualquer valor retornado da sua nova função falsa.
describe("A spy, when faking a return value", function() { var foo, bar, fetchedBar; beforeEach(function() { foo = { setBar: function(value) { bar = value; }, getBar: function() { return bar; } }; spyOn(foo, 'getBar').andCallFake(function() { return 1001; }); foo.setBar(123); fetchedBar = foo.getBar(); }); it("tracks that the spy was called", function() { expect(foo.getBar).toHaveBeenCalled(); }); it("should not effect other functions", function() { expect(bar).toEqual(123); }); it("when called returns the requested value", function() { expect(fetchedBar).toEqual(1001); }); });
Você pode estar se perguntando: “E se eu já não tiver um objeto com o qual queira que o espião trabalhe? Eu só quero criar um espião sem quaisquer objetos ou funções existentes. Isso é possível?” Pode apostar! Primeiro, vamos dar uma olhada em como criar uma função de espionagem do nada, então vamos seguir em frente para explorar a ideia de fazer um objeto espião inteiro.
Você faz uma função espiã com jasmine.createSpy e passa em um nome. Ela vai retornar a função espiã para você. O nome parece um pouco inútil, porque ele não é usado como um identificador ao qual podemos nos referir, mas, como você pode ver abaixo, ele pode ser usado com a propriedade identity espiãs em mensagens de erro para especificar onde ocorreu o erro.
describe("A spy, when created manually", function() { var whatAmI; beforeEach(function() { whatAmI = jasmine.createSpy('whatAmI'); whatAmI("I", "am", "a", "spy"); }); it("is named, which helps in error reporting", function() { expect(whatAmI.identity).toEqual('whatAmI') }); it("tracks that the spy was called", function() { expect(whatAmI).toHaveBeenCalled(); }); it("tracks its number of calls", function() { expect(whatAmI.calls.length).toEqual(1); }); it("tracks all the arguments of its calls", function() { expect(whatAmI).toHaveBeenCalledWith("I", "am", "a", "spy"); }); it("allows access to the most recent call", function() { expect(whatAmI.mostRecentCall.args[0]).toEqual("I"); }); });
Finalmente, vamos criar um objeto com todos os métodos espiões usando jasmine.createSpyObj. Assim como em createSpy, ele leva um nome, mas também leva um array de strings que será usado como os nomes das funções espiãs ligadas ao objeto. O nome é usado da mesma maneira que com createSpy: identificando objetos durante resultados de erro do Jasmine.
describe("Multiple spies, when created manually", function() { var tape; beforeEach(function() { tape = jasmine.createSpyObj('tape', ['play', 'pause', 'stop', 'rewind']); tape.play(); tape.pause(); tape.rewind(0); }); it("creates spies for each requested function", function() { expect(tape.play).toBeDefined(); expect(tape.pause).toBeDefined(); expect(tape.stop).toBeDefined(); expect(tape.rewind).toBeDefined(); }); it("tracks that the spies were called", function() { expect(tape.play).toHaveBeenCalled(); expect(tape.pause).toHaveBeenCalled(); expect(tape.rewind).toHaveBeenCalled(); expect(tape.stop).not.toHaveBeenCalled(); }); it("tracks all the arguments of its calls", function() { expect(tape.rewind).toHaveBeenCalledWith(0); }); });
Testando funções assíncronas
A programação assíncrona não é simples, pelo menos não tão simples quanto a programação síncrona, que é bem simples e direta. Isso faz com que as pessoas tenham mais medo de testar as funções assíncronas, mas o Jasmine também torna muito simples testar as funções assíncronas. Vamos dar uma olhada em um exemplo usando uma solicitação AJAX com jQuery:
describe("Asynchronous Tests", function() { it("is pretty simple with <code>runs</code>, <code>waitsFor</code>, <code>runs</code>", function() { var finished = false, worked = false; runs(function() { $.ajax('/example/').always(function(){ finished = true; }).done(function(){ worked = true; }); }); waitsFor(function(){ return finished; }, "AJAX should complete", 1000); runs(function() { expect(worked).toBe(true); }); }); });
Isso pode não fazer muito sentido apenas olhando, mas com uma pequena explicação vai parecer muito simples e todos os seus medos de testes assíncronos vão sumir. Para começar, vamos pular direto para o corpo do bloco it. Primeiro, a gente cria algumas flags. Eles nem sempre são necessárias, dependendo de como funciona a função assíncrona, mas, se você precisar, elas podem segurar Booleans que especificam se a função assíncrona funcionou/terminou, como eu fiz aqui. Agora chegamos à parte divertida: runs e waitsFor. A primeira chamada para runs é onde executamos uma função assíncrona. Então nós usamos waitsFor para determinar quando e se a função assíncrona terminou. Isso é feito especificando uma função que retorna um boolean que deve ser verdadeiro quando o trabalho assíncrono for concluído ou falso antes de terminar. Esse é o primeiro argumento transmitido. O próximo é o erro que queremos mostrar se ele nunca retorna verdadeiro, e o argumento final é o número de milissegundos que devemos esperar antes que ele expire e falhe a especificação. A função que é passada para waitsFor é executada em curtos intervalos de tempo até que ele retorne verdadeiro ou expire. Então, seguimos em frente e executamos a função passada para a chamada runs seguinte. Aqui é onde você faz, geralmente, o seu expect.
A parte divertida é que você pode continuar alternando entre runs e waitsfor (potencialmente) infinitamente. Então, se você deseja executar outra função assíncrona na segunda runs e depois fazer outra waitsfor e finalmente chamar runs mais uma vez para completar seus testes, é perfeitamente possível. Você vai me ver fazer isso em um artigo em breve, quando eu falarei sobre o teste Socket.IO.
Fazendo mock do JavaScript Clock
Se você tiver um código que funcione com setTimeout ou setInterval, você pode pular o teste assíncrono e usar apenas Jasmine para controlar o relógio, o que lhe permite executar o código de forma síncrona. Basta dizer ao Jasmine para usar o seu próprio relógio mock com jasmine.Clock.useMock() e depois usar jasmine.Clock.tick([number]) para mover o relógio adiante sempre que quiser.
describe("Manually ticking the Jasmine Mock Clock", function() { var timerCallback; // It is installed with a call to jasmine.Clock.useMock in a spec or suite that needs to call the timer functions. beforeEach(function() { timerCallback = jasmine.createSpy('timerCallback'); jasmine.Clock.useMock(); }); // Calls to any registered callback are triggered when the clock is ticked forward via the jasmine.Clock.tick function, which takes a number of milliseconds. it("causes a timeout to be called synchronously", function() { setTimeout(function() { timerCallback(); }, 100); expect(timerCallback).not.toHaveBeenCalled(); jasmine.Clock.tick(101); expect(timerCallback).toHaveBeenCalled(); }); it("causes an interval to be called synchronously", function() { setInterval(function() { timerCallback(); }, 100); expect(timerCallback).not.toHaveBeenCalled(); jasmine.Clock.tick(101); expect(timerCallback.callCount).toEqual(1); jasmine.Clock.tick(50); expect(timerCallback.callCount).toEqual(1); jasmine.Clock.tick(50); expect(timerCallback.callCount).toEqual(2); }); });
Tão simples quanto é o teste assíncrono, eu ainda prefiro usar isso quando eu puder. É divertido ter tanto poder. É claro, isso não afeta realmente o relógio, mas quem se importa? Parece que ele se importa, né?
Tipos de correspondência com jasmine.any
Às vezes, tentar testar um valor específico é muito rigoroso e você só quer ter a certeza de que é de um tipo específico, como um número ou objeto. Nesse caso, jasmine.any chega para ajudar. Você pode usá-lo em qualquer correspondência para verificar um tipo valor, em vez de compará-lo a um valor exato.
describe("jasmine.any", function() { it("matches any value", function() { expect({}).toEqual(jasmine.any(Object)); expect(12).toEqual(jasmine.any(Number)); }); describe("when used with a spy", function() { it("is useful for comparing arguments", function() { var foo = jasmine.createSpy('foo'); foo(12, function() { return true }); expect(foo).toHaveBeenCalledWith(jasmine.any(Number), jasmine.any(Function)); }); }); });
É preciso um nome de constructor e compará-lo ao constructor do valor. Isso significa que você pode testá-lo contra seus tipos personalizados também, não apenas nos built in.
Desabilitando specs e suítes
Às vezes, você não quer executar uma especificação ou suite, quer seja porque leva muito tempo, ou porque você sabe que vai falhar e não quer lidar com isso mais tarde. Você sempre pode comentá-lo, mas se você quiser tornar todas as especificações comentadas de volta, é difícil fazer uma pesquisa e substituir. Em vez disso, você pode incluir no início um describe ou um it com um “x”, e o suite ou especificação serão ignorados como se tivessem sido comentados, mas uma pesquisa simples por xdescribe pode ser substituída por describe. O mesmo vale para xit e it.
xdescribe("A disabled suite or spec", function() { var foo; beforeEach(function() { foo = 0; foo += 1; }); xit("will be skipped", function() { expect(foo).toEqual(1); }); });
Conclusão
Bem, isso é praticamente tudo o que você precisa saber para começar com testes unitários usando o framework Jasmine. Espero que a sua simplicidade possa atraí-lo e que, casoesteja adiando os testes de unidade, você comece agora.
***
Artigo traduzido pela Redação iMasters, com autorização do autor. Publicado originalmente em http://www.joezimjs.com/javascript/javascript-unit-testing-with-jasmine-part-2/