Introducción a las pruebas unitarias de jazmín

Jasmine es la biblioteca JS más popular para aplicaciones web de prueba unitaria. En este tutorial, diseñado para principiantes, le presentaremos una guía rápida y completa para probar con Jasmine.

Se le presentará a Jasmine, un popular marco de pruebas basado en el comportamiento para JavaScript. También veremos un ejemplo práctico simple sobre cómo escribir pruebas unitarias con Jasmine que puede ayudarlo a verificar fácilmente si hay errores en su código.

En pocas palabras, veremos cómo escribir conjuntos de pruebas, especificaciones y expectativas y cómo aplicar comparadores Jasmine incorporados o construir sus propios comparadores personalizados.

También veremos cómo puede agrupar suites con el fin de organizar sus pruebas para bases de código más complejas.

Presentando a Jasmine

Jasmine es un marco de desarrollo basado en el comportamiento de JavaScript muy popular (en BDD, se escriben las pruebas antes de escribir el código real) para realizar pruebas unitarias en las aplicaciones de JavaScript. Proporciona utilidades que se pueden utilizar para ejecutar pruebas automatizadas para código síncrono y asincrónico.

Jasmine tiene muchas características como:

  • Es rápido, tiene una sobrecarga baja y no tiene dependencias externas.
  • Es una biblioteca que incluye baterías y ofrece todo lo que necesita para probar su código.
  • Está disponible tanto para Node como para el navegador.
  • Se puede usar con otros lenguajes como Python y Ruby.
  • No requiere DOM.
  • Proporciona una sintaxis limpia y fácil de entender y también una API enriquecida y sencilla.
  • Podemos usar lenguaje natural para describir las pruebas y los resultados esperados.

Jasmine es una herramienta de código abierto que está disponible bajo la licencia permisiva del MIT. En el momento de escribir estas líneas, la última versión principal es Jasmine 3.0, que proporciona nuevas funciones y algunos cambios importantes. La versión 2.99 de Jasmine proporcionará diferentes advertencias de obsolescencia para las suites que tienen un comportamiento diferente en la versión 3.0, lo que facilitará a los desarrolladores la migración a la nueva versión.

Puede leer sobre las nuevas funciones y los cambios importantes en este documento.

Usando Jasmine

Puedes usar Jasmine de muchas formas diferentes:

  • a la antigua, incluyendo tanto el núcleo de Jasmine como los archivos de prueba mediante un pt> tag,
  • as a CLI tool using Node.js,
  • as a library in Node.js,
  • as a part of a build system like Gulp.js or Grunt.js via grunt-contrib-jasmine and gulp-jasmine-browser

You can also use Jasmine for testing your Python code with jasmine-py which can be installed from PyPI using the pip install jasmine command. This package contains both a web server that serves and executes a Jasmine suite for your project and a CLI script for running tests and continuous integrations.

Jasmine is also available for Ruby projects via jasmine-gem which can be installed by adding gem 'jasmine' to your Gemfile and running bundle install. It includes a server for serving and running tests, a CLI script and also generators for Ruby on Rails projects.

Now let’s focus on how to use Jasmine with JavaScript:

Using Standalone Jasmine

Start by downloading the latest version of Jasmine from the releases page.

Then simply extract the zip file, preferably inside a folder in the project you want to test.

The folder will contain a bunch of default files and folders:

/src: contains the source files that you want to test. This may be either deleted if your already have your project's folder setup or can also be used when appropriate for hosting your source code.

/lib: contains the core Jasmine files.

/spec: contains the tests that you are going to write.

SpecRunner.html: this file is used as a test runner. You run your specs by simply launching this file.

This is the content of a default SpecRunner.html file:

    Jasmine Spec Runner v3.2.1               

Remember that you need to change the files included from the /src and /spec folders to contain your actual source and test files.

Using Jasmine as a Library

You can also use Jasmine as a library in your project. For example the following code imports and executes Jasmine:

var Jasmine = require('jasmine'); var jasmine = new Jasmine(); jasmine.loadConfigFile('spec/support/jasmine.json'); jasmine.execute();

First we require/import Jasmine and we use the loadConfigFile() method to load the config file available from spec/support/jasmine.json path then finally we execute Jasmine.

Using Jasmine via The CLI

You can also use Jasmine from the CLI which allows you to easily run Jasmine tests and by default output the results in the terminal.

We’ll follow this approach to run our example tests in this guide, so first go ahead and run the following command to install Jasmine globally:

npm install -g jasmine
You may need to run sudo for installing npm packages globally depending on your npm configuration.

Now, create a folder for your project and navigate inside it:

$ mkdir jasmine-project $ cd jasmine-project

Next, run the following command to initialize your project for Jasmine:

This command simply creates a spec folder and a JSON configuration file. This is the output of the dir command:

. └── spec └── support └── jasmine.json 2 directories, 1 file

This is the content of a default jasmine.json file:

{ "spec_dir": "spec", "spec_files": [ "**/*[sS]pec.js" ], "helpers": [ "helpers/**/*.js" ], "stopSpecOnExpectationFailure": false, "random": true }
  • spec_dir: specifies where Jasmine looks for test files.
  • spec_files: specifies the patterns of test files, by default all JS files that end with Spec or spec strings.
  • helpers: specifies where Jasmine looks for helper files. Helper files are executed before specs and can be used to define custom matchers.
  • stopSpecOnExpectationFailure: when set to true will immediately stop a spec on the first failure of an expectation (can be used as a CLI option via --stop-on-failure).
  • random: when set to true Jasmine will pseudo-randomly run the test cases (can be used as a CLI option via --random).

The spec_files and helpers arrays can also contain Glob patterns (thanks to the node-glob package) for specifying file paths which are patterns you usually use to specify a set of files when working in Bash (e.g. ls *.js).

If you don’t use the default location for the jasmine.json configuration file, you simply need to specify the custom location via the jasmine --config option.

You can find more CLI options from the official docs.

Understanding Jasmine

In this section we’ll learn about the basic elements of Jasmine testing such as suites, specs, expectations, matchers and spies, etc.

In your project’s folder, run the following command to initialize a new Node module:

This will create a package.json file with default information:

{ "name": "jasmine-project", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }

Next, create an index.js file and add the following code:

function fibonacci(n){ if (n === 1) { return [0, 1]; } else { var s = fibonacci(n - 1); s.push(s[s.length - 1] + s[s.length - 2]); return s; } } function isPrime(num){ for (let i = 2; i < num; i++) if (num % i === 0) return false; return num !== 1 && num !== 0; } function isEven(n) { return n % 2 == 0; } function isOdd(n) { return Math.abs(n % 2) == 1; } function toLowerCase(str){ return str.toLowerCase(); } function toUpperCase(str){ return str.toUpperCase(); } function contains(str, substring, fromIndex){ return str.indexOf(substring, fromIndex) !== -1; } function repeat(str, n){ return (new Array(n + 1)).join(str); } module.exports = { fibonacci: fibonacci, isPrime: isPrime, isEven: isEven, isOdd: isOdd, toLowerCase: toLowerCase, toUpperCase: toUpperCase, contains: contains, repeat: repeat };

Suites

A suite groups a set of specs or test cases. It’s used to test a specific behavior of the JavaScript code that’s usually encapsulated by an object/class or a function. It’s created using the Jasmine global function describe() that takes two parameters, the title of the test suite and a function that implements the actual code of the test suite.

Let’s start by creating our first test suite. Inside the spec folder create a MyJSUtilitiesSpec.js file and add:

describe("MyJSUtilities", function() { /* ... */ });

MyJSUtilities is the name of this top-level test suite.

How to Group and Nest Suites

For better organizing and accurately describing our set of tests we can nest suites inside the top-level suite. For example, let’s add two suites to the MyJSUtilities suite:

describe("String Utils", function() { /*...*/});describe("Math Utils", function() { /*...*/});

Inside the the Math Utils suite, let’s also add two nested suites:

describe("Basic Math Utils", function() { /* ... */ }); describe("Advanced Math Utils", function() { /* ... */ });

We are grouping related tests into tests for String Utils, Basic Math Utils and Advanced Math Utils and nesting them inside the top-level test suite MyJSUtilities. This will compose your specs as trees similar to a structure of folders.

The nesting structure will be shown on the report which makes it easy for you to find failing tests.

How to Exclude Suites

You can temporarily disable a suite using the xdescribe() function. It has the same signature (parameters) as a describe() function which means you can quickly disable your existing suites by simply adding an x to the function.

Specs within an xdescribe() function will be marked pending and not executed in the report.

Specs

A spec declares a test case that belongs to a test suite. This is done by calling the Jasmine global function it() which takes two parameters, the title of the spec (which describes the logic we want to test) and a function that implements the actual test case.

A spec may contain one or more expectations. Each expectation is simply an assertion that can return either true or false. For the spec to be passed, all expectations belonging to the spec have to be true otherwise the spec fails.

Inside our String Utils suite, add these specs:

describe("String Utils", function() { it("should be able to lower case a string",function() { /*...*/ }); it("should be able to upper case a string",function() { /*...*/ }); it("should be able to confirm if a string contains a substring",function() { /*...*/ }); it("should be able repeat a string multiple times",function() { /*...*/ });});

Inside our Basic Math Utils suite let’s add some specs:

describe("Basic Math Utils", function() { it("should be able to tell if a number is even",function() { /*...*/ }); it("should be able to tell if a number is odd",function() { /*...*/ }); });

For the Advanced Math Utils, let’s add the specs:

describe("Advanced Math Utils", function() { it("should be able to tell if a number is prime",function() { /*...*/ }); it("should be able to calculate the fibonacci of a number",function() { /*...*/ }); });

How to Exclude Specs

Just like suites, you can also exclude individual specs using the xit() function which temporary disables the it() spec and marks the spec as pending.

Expectations

Expectations are created using the expect() function that takes a value called the actual (this can be values, expressions, variables, functions or objects etc.). Expectations compose the spec and are used along with matcher functions (via chaining) to define what the developer expect from a specific unit of code to perform.

A matcher function compares between an actual value (passed to the expect() function it's chained with) and an expected value (directly passed as a parameter to the matcher) and returns either true or false which either passes or fails the spec.

You can chain the expect() function with multiple matchers. To negate/invert the boolean result of any matcher, you can use the not keyword before calling the matcher.

Let’s implement the specs of our example. For now we’ll use we’ll use expect() with the nothing() matcher which is part of the built-in matchers which we'll see a bit later. This will pass all specs since we are expecting nothing at this point.

describe("MyJSUtilities", function() {describe(">String Utils", function() { it("should be able to lower case a string",function() { expect().nothing(); }); it("should be able to upper case a string",function() { expect().nothing(); }); it("should be able to confirm if a string contains a substring",function() { expect().nothing(); }); it("should be able repeat a string multiple times",function() { expect().nothing(); }); });describe("Math Utils", function() { describe("Basic Math Utils", function() { it("should be able to tell if a number is even",function() { expect().nothing(); }); it("should be able to tell if a number is odd",function() { expect().nothing(); }); }); describe("Advanced Math Utils", function() { it("should be able to tell if a number is prime",function() { expect().nothing(); }); it("should be able to calculate the fibonacci of a number",function() { expect().nothing(); }); }); });});

This is a screenshot of the results at this point:

We have eight passed specs and zero failures.

You can either use built-in matchers or also create your own custom matchers for your specific needs.

Built-In Matchers

Jasmine provides a rich set of built-in matchers. Let’s see some of the important ones:

  • toBe() for testing for identity,
  • toBeNull() for testing for null,
  • toBeUndefined()/toBeDefined() for testing for undefined/not undefined,
  • toBeNaN() for testing for NaN (Not A Number)
  • toEqual() for testing for equality,
  • toBeFalsy()/toBeTruthy() for testing for falseness/truthfulness etc.

You can find the full list of matchers from the docs.

Let’s now implement our specs with some of these matchers when appropriate. First import the functions we are testing in our MyJSUtilitiesSpec.js file:

const utils = require("../index.js");

Next, start with the String Utils suite and change expect().nothing() with the appropriate expectations.

For example for the first spec, we expect the toLowerCase() method to be first defined and secondly to return a lower case string i.e:

it("should be able to lower case a string",function() { expect(utils.toLowerCase).toBeDefined(); expect(utils.toLowerCase("HELLO WORLD")).toEqual("hello world"); });

This is the full code for the suite:

describe(">String Utils", function() { it("should be able to lower case a string",function() { expect(utils.toLowerCase).toBeDefined(); expect(utils.toLowerCase("HELLO WORLD")).toEqual("hello world"); }); it("should be able to upper case a string",function() { expect(utils.toUpperCase).toBeDefined(); expect(utils.toUpperCase("hello world")).toEqual("HELLO WORLD"); }); it("should be able to confirm if a string contains a substring",function() { expect(utils.contains).toBeDefined(); expect(utils.contains("hello world","hello",0)).toBeTruthy(); }); it("should be able repeat a string multiple times",function() { expect(utils.repeat).toBeDefined(); expect(utils.repeat("hello", 3)).toEqual("hellohellohello"); }); });

Custom Matchers

Jasmine provides the ability to write custom matchers for implementing assertions not covered by the built-in matchers or just for the sake of making tests more descriptive and readable.

For example, let’s take the following spec:

it("should be able to tell if a number is even",function() { expect(utils.isEven).toBeDefined(); expect(utils.isEven(2)).toBeTruthy(); expect(utils.isEven(1)).toBeFalsy(); });

Let’s suppose that the isEven() method is not implemented. If we run the tests we'll get messages like the following screenshot:

The failure message we get says Expected undefined to be defined which gives us no clue of what’s happening. So let’s make this message more meaningful in the context of our code domain (this will be more useful for complex code bases). For this matter, let’s create a custom matcher.

We create custom matchers using the addMatchers() method which takes an object comprised of one or many properties that will be added as matchers. Each property should provide a factory function that takes two parameters: util, which has a set of utility functions for matchers to use (see: matchersUtil.js) and customEqualityTesters which needs to be passed in if util.equals is called, and should return an object with a compare function that will be called to check the expectation.

We need to register the custom matcher before executing each spec using the beforeEach() method:

describe("/Basic Math Utils", function () {beforeEach(function () {jasmine.addMatchers({hasEvenMethod: function (util, customEqualityTesters) {return {compare: function (actual, expected) {var result = { pass: utils.isEven !== undefined };if (result.pass) {result.message = "Expected isEven() to be not defined."}else {result.message = "Expected isEven() to be defined."}return result;}}}});});/*...*/});

We can then use the custom matcher instead of expect(utils.isEven).toBeDefined():

expect().hasEvenMethod();

This will give us a better failure message:

Using beforeEach() and afterEach()

For initializing and cleaning your specs, Jasmine provides two global functions, beforeEach() and afterEach():

  • The beforeEach function is called once before each spec in the suite where it is called.
  • The afterEach function is called once after each spec in the suite where it's called.

For example, if you need to use any variables in your test suite, you can simply declare them in the start of the describe() function and put any initialization or instantiation code inside a beforeEach() function. Finally, you can use the afterEach() function to reset the variables after each spec so you can have pure unit testing without the need to repeat initialization and cleanup code for each spec.

The beforeEach() function is also perfectly combined with many Jasmine APIs such as the addMatchers() method to create custom matchers or also with the done() function to wait for asynchronous operations before continue testing.

Failing a Test

You can force a test to fail using the global fail() method available in Jasmine. For example:

it("should explicitly fail", function () { fail('Forced to fail'); });

You should get the following error:

Testing for Exceptions

When you are unit-testing your code, errors and exceptions maybe thrown, so you might need to test for these scenarios. Jasmine provides the toThrow() and toThrowError() matchers to test for when an exception is thrown or to test for a specific exception, respectively.

For example if we have a function that throws an TypeError exception:

function throwsError() { throw new TypeError("A type error"); }

You could write a spec that to test for if an exception is thrown:

it('it should throw an exception', function () { expect(throwsError).toThrow(); });

Or you could also use test for the specific TypeError exception:

it('it should throw a TypeError', function () { expect(throwsError).toThrowError(TypeError); });

Understanding Spies

More often than not, methods depend on other methods. This means that when you are testing a method, you may also end up testing its dependencies. This is not recommended in testing i.e you need to make sure you test the pure function by isolating the method and seeing how it behaves given a set of inputs.

Jasmine provides spies which can be used to spy on/listen to method calls on objects and report if a method is called and with which context and arguments.

Jasmine provides two ways for spying on method calls: using the spyOn() or the createSpy() methods.

You can use spyOn() when the method already exists on the object, otherwise you need to use jasmine.createSpy() which returns a new function.

By default a spy will only report if a call was done without calling through the spied function (i.e the function will stop executing), but you can change the default behavior using these methods:

  • and.callThrough(): call through the original function,
  • and.returnValue(value): return the specified value,
  • and.callFake(fn): call the fake function instead of the original one,
  • and.throwError(err): throw an error,
  • and.stub(): resets the default stubbing behavior.

You can use a spy to gather run-time statistics on the spied function, for example if you want to know how many times your function was called.

Say we want to make sure our toUpperCase() method is making use of the built-in String.toUpperCase() method, we need to simply spy on String.toUpperCase() using:

it("should be able to upper case a string", function () { 
var spytoUpperCase = spyOn(String.prototype, 'toUpperCase') 
expect(utils.toUpperCase).toBeDefined(); expect(utils.toUpperCase("hello world")).toEqual("HELLO WORLD"); expect(String.prototype.toUpperCase).toHaveBeenCalled(); expect(spytoUpperCase.calls.count()).toEqual(1); });

The test has failed due to the second expectation because utils.toUpperCase("hello world") returned undefined instead of the expected HELLO WORLD. That's because, as we mentioned, earlier after creating the spy on toUpperCase(), the method is not executed. We need to change this default behavior by calling callThrough():

Please note that a spy function replaces the spied function with a stub by default. If you need to call the original function instead, you can add .and.callThrough() to your spy object.
var spytoUpperCase = spyOn(String.prototype, 'toUpperCase').and.callThrough();

Now all expectations pass.

You can also use and.callFake() or and.returnValue() to fake either the spied on function or just the return value if you don't to call through the actual function:

var spytoUpperCase = spyOn(String.prototype, 'toUpperCase').and.returnValue("HELLO WORLD"); 
var spytoUpperCase = spyOn(String.prototype, 'toUpperCase').and.callFake(function(){ return "HELLO WORLD"; });

Now, if we end up not using the built in String.toUpperCase() in our own utils.toUpperCase() implementation, we'll get these failures:

The two expectations expect(String.prototype.toUpperCase).toHaveBeenCalled()expect(spytoUpperCase.calls.count()).toEqual(1) have failed.

How to Deal with Asynchronicity in Jasmine

If the code you are testing contains asynchronous operations, you need a way to let Jasmine know when the asynchronous operations have completed.

By default, Jasmine waits for any asynchronous operation, defined by a callback, promise or the async keyword, to be finished. If Jasmine finds a callback, promise or async keyword in one of these functions: beforeEach, afterEach, beforeAll, afterAll, and it it will wait for the asynchronous to be done before proceeding to the next operation.

Using done() with beforeEach()/it() ..

Let’s take our example simulateAsyncOp() which simulates an asynchronous operation using setTimeout(). In a real world scenario this can be an Ajax request or any thing similar that happens asynchronously:

function simulateAsyncOp(callback){ 
setTimeout(function () { callback(); }, 2000); }

To test this function we can use the beforeEach() function with the special done() callback. Our code needs to invoke done() to tell Jasmine that the asynchronous operation has completed:

describe("/Async Op", function () {var asyncOpCompleted = false;beforeEach(function (done) {utils.simulateAsyncOp(function(){ asyncOpCompleted = true; done();});});it("should be able to tell if the async call has completed", function () { expect(asyncOpCompleted).toEqual(true);});});

We can quickly notice a drawback of this method, so we need to write our code to accept the done() callback. In our case, we didn't hardcode the done() method in our simulateAsyncOp(fn) but we have provided a callback parameter just to be able to call done().

Using Promises

If you don’t want to create code that depends on how you write your test, you can use a promise instead and call the done() callback when the promise has resolved. Or better yet, in Jasmine 2.7+, if your code returns a Promise, Jasmine will wait until it is resolved or rejected before executing the next code.

Using async/await

Jasmine 2.7+ supports async and await calls in specs. This relieves you from putting asserts in a .then() or .catch() block.

it("should work with async/await", async () => { let completed = false; completed = await utils.simulateAsyncOp(); expect(completed).toEqual(true); });

This is the implementation of simulateAsyncOp:

function simulateAsyncOp() { 
return new Promise(resolve => { setTimeout(() => { resolve(true); }, 1000); }); }

Using Jasmine Clock

The Jasmine clock is used to test asynchronous code that depends on time functions such as setTimeout() in the same way we test synchronous code by mocking time-based APIs with custom methods. In this way, you can execute the tested functions synchronously by controlling or manually advancing the clock.

You can install the Jasmine clock by calling the jasmine.clock().install function in your spec or suite.

After using the clock, you need to uninstall it to restore the original functions.

With Jasmine clock, you can control the JavaScript setTimeout or setInterval functions by ticking the clock in order to advance in time using the jasmine.clock().tick function, which takes the number of milliseconds you can move with.

You can also use the Jasmine Clock to mock the current date.

beforeEach(function () {jasmine.clock().install();});afterEach(function() {jasmine.clock().uninstall();});it("should call the asynchronous operation synchronously", function() {var completed = false;utils.simulateAsyncOp(function(){completed = true;});expect(completed).toEqual(false);jasmine.clock().tick(1001);expect(completed).toEqual(true);});

This is the simulateAsyncOp function:

function simulateAsyncOp(callback){ 
setTimeout(function () { callback(); }, 1000); }
In case you didn’t specify a time for the mockDate function, it will use the current date.

Handling Errors

If your asynchronous code fails due to some error, you want your specs to fail correctly. Starting with Jasmine 2.6+ any unhandled errors are sent to the currently executed spec.

Jasmine also provides a way you can use if you need to explicitly fail your specs:

  • using the done() callback with beforeEach() by calling the done.fail(err) method,
  • simply passing an error to the done(err) callback (Jasmine 3+),
  • calling the reject() method of a Promise.

Conclusion

In this guide we’ve introduced Jasmine and seen how to get started using Jasmine to unit test your JavaScript code. Thanks for reading!

This article was originally posted in techiediaries.