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();
});
});
});