Front End

12 mar, 2013

Teste unitário do JavaScript com Jasmine – Parte 02

Publicidade

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/