Aurelia is one of a crop of new front end JavaScript frameworks that make it easier to manage complex interactions in the browser. It implements a MVVM pattern and includes routing, dependency injection etc.
Unit testing Aurelia custom elements and attributes is described in the Aurelia docs (http://aurelia.io/hub.html#/doc/article/aurelia/testing/latest/testing-components. However, more general testing of business logic or service code is not discussed. This article gives a basic introduction to testing these classes using the Aurelia CLI.
- What libraries are included in the CLI, what do they do?
- Writing a basic test
- Running tests
- Improving the test output
- Debugging tests
- Mocking in tests
- Testing code with promises.
Note: Aurelia supports the babel.js and Typescript transpilers. The code examples below are in Typescript but should be readable for anyone who is familiar with modern javascript.
1. What libraries are included in the CLI, what do they do?
When creating an Aurelia project using the CLI the following testing libraries are included:
- Jasmine – popular BDD JavaScript testing framework. Provides the test structure and asserts.
- Karma – test runner. Allows you to run the tests from the command line and debug the tests within a browser.
Angular Protractor and selenium web-driver tests are also included – they are used for end to end testing and so not discussed in this article.
2. Writing a basic test
Test classes are placed in the /tests folder and should include spec in the title e.g. articleStore.spec.js. The test class should include the following elements.
Import referenced classes
Import the class under test (and any other relevant classes) at the top of the test class e.g.
Import { ArticleStore } from ‘../../src/articles/ArticleStore’;
Create a top level describe function.
Create a describe function that indicates the name of the class under test as a string and the details of the tests as an argument e.g.
describe(‘the ArticleStore’, () => {…});
Note: The text ‘the ArticleStore’ will then be outputted when we run the tests.
Create a test setup
As with most testing framework Jasmine provides a mechanism to run setup and teardown code before/after each individual test. This is achieved by creating a beforeEach/afterEach function that takes a function as an argument.
In this example we will use this to create an instance of the class under test before each test is run e.g.
let target: ArticleStore; beforeEach(() => { this.target = new ArticleStore(); });
Create a test
To create the test itself we create an it() function which takes the name of the test as a string and the test code as an function argument. Again, the name of the test will be outputted by the test runner.
The test itself makes use of the Jasmine asserts to confirm expected state.
e.g.
it(“should have an empty articles collection”, () = { expect(this.target.Articles).not.toBeNull(); expect(this.target.Articles.length).toBe(0); });
3. Running tests
To run the tests issue the following statement from a command prompt:
au test
This invokes the Karma test runner to run any tests it finds in files ending spec.js and outputs the details of any tests that have failed.
“au test” will run the tests once, report the output and close. However, as with the “Aurelia run” command you can include the watch argument:
au test --watch
This will run the tests, report the output but not close. The test runner will maintain a watch for any changes to code and when new code is saved will rerun the tests and display the new output.
4. Improving the test output
The default configuration of Karma within Aurelia will only report failing tests. To get a comprehensive list of all tests that were run make the following change to the /karma.conf.js file:
Change this: reporters: [‘progress’],
To this: reporters: [‘spec’],
5. Debugging tests
Karma makes debugging quite easy as it creates a browser instance and allows you to use the standard in browser debug tools (F12). As well as being simple to use, debugging in the browser is more accurate than debugging in an IDE or similar, as it will correctly replicate any browser issues.
To debug from the Karma test runner:
- In the command prompt run: au test –watch
- A browser spins up. Click the ‘Debug’ button in the green bar on the top right.
- A new tab opens with the unit tests loaded. Press F12 to open developer tools, view the source, add breakpoints etc. as normal and press refresh to re-run the tests and hit the breakpoints.
6. Mocking in Aurelia tests
Mocking is an important part of any unit testing strategy. Currently the standard tool for mocks/stubs/spies in Javascipt is to use the sinon.js library. However in my experience this did not play well with Jasmine.
An alternative, simpler and more modern mocking library is called TestDouble (https://github.com/testdouble/testdouble.js) . This can be imported via NPM.
Import the testdouble package
npm install testdouble –save-dev
Add testdouble as a vendor-bundle dependency.
This is optional but adding the following to the dependencies section of the /aurelia-project/aurelia.json file, vendor-bundle dependencies section makes regularly including the testdouble library within test classes simpler as you can refer to it with a simple name rather than needing the relative path.
{ "name": "testdouble", "path": "../node_modules/testdouble/dist/testdouble" }
Import TestDouble in your test class
Import * as TestDouble from ‘testdouble’;
Note include the relative path if you did not complete the previous step
Create/Destroy the mock objects
Within the test code create a variable for the object being mocked, with the type of the object being mocked e.g. if we are mocking a class of type ApiConnector
let apiConnector: ApiConnector;
Initialize the mock on the beforeEach() method, and call TestDouble.reset() on the afterEach() method e.g.
beforeEach(() => { this.apiConnector = TestDouble.object(ApiConnector); });
afterEach(() => { TestDouble.reset(); });
Setup and/or verify the mocks within the tests
It(‘getStatus should return the updating status from the apiConnector”, () => { // setup the mock let knownStatus = “cached”; TestDouble.when(this.apiConnector.getStatus).thenReturn(knownStatus); // make the call under test Let result = this.articleStore.getStatus(); // assert the mocked result is returned and the call was made on the mock object Expect(result).toBe(knownStatus); this.articleStore.verify(getApiStatus); });
7. Testing code containing promises
Asynchronous code is very common in the JavaScript world and a modern approach to implementing async code is to use promises. Asynchronous code and promises requires some minor changes in testing approach.
To handle async code Jasmine takes an optional argument to the it(), beforeEach() and afterEach() methods called “done”. The “done” argument is a method that should be called once all other test code has completed. When using a promises approach to asynchronous code this would typically be at the end of the last “then” call.
The following code gives an example of creating a promise object and returning this promise from a mocked method. This is a common scenario e.g. where an ajax call which returns a promise needs to be mocked.
For a promise to be processed either its resolve or reject method should be called. In this example the mock returns a promise that has a resolve method and the test asserts that when that promise is successfully resolved the refreshAll method returns a promise with the data true.
it("refreshAll returns a promise with data:true when api call successful", (done) => { // arrange let promise = new Promise((resolve, reject) => { resolve("Success data"); }); TestDouble.when(this.apiConnector.getMany()).thenReturn(promise); // act let result = this.articleStore.refreshAll(); // assert result.then(data => { expect(data).toBe(true); done(); }); });
Complete example code
As a summary, I have included below a complete example of an Aurelia test class which mocks promises:
import { ArticleStore } from '../../../../src/resources/data-service/ArticleStore'; import { ApiConnector } from '../../../../src/resources/data-service/ApiConnector'; import * as TestDouble from 'testdouble'; describe('the ArticleStore', () => { // setup let apiConnector: ApiConnector; let articleStore: ArticleStore; beforeEach(() => { TestDouble.reset(); this.apiConnector = TestDouble.object(ApiConnector); this.articleStore = new ArticleStore(this.apiConnector); }); afterEach(() => { TestDouble.reset(); }); it("refreshAll returns a promise with data:true when api call successful", (done) => { // arrange let promise = new Promise((resolve, reject) => { resolve("Success data"); }); TestDouble.when(this.apiConnector.getMany()).thenReturn(promise); // act let result = this.articleStore.refreshAll(); // assert result.then(data => { expect(data).toBe(true); done(); }); }); });
0 Responses
Stay in touch with the conversation, subscribe to the RSS feed for comments on this post.