Unit Testing your ES6 Custom Elements - (my) Best Practices for Aurelia

Updated December 2016

Let's be honest - getting in to the habit of writing unit tests for all of the code in your application can become tedious. There I said it.

Finding the line of what needs to be tested in your project and what can be assumed to be covered by the tests in the framework takes practice. Great code can speak for itself but if it isn't being used by anyone it doesn't matter how well it is written or tested.

Ok we get it, what's this about anyway?

Introduction

I found myself in the position where I feel like the Aurelia application's I've been working on lately are going pretty well. As we've mentioned on the blog numerous times we're down to working on docs and performance optimizations at this point in preparation for beta / v1. It's a very exciting time!

Now that I've got the PoC apps and others that are starting to come along I realized that tech debt is catching up to me. I'm ready to start sharing these applications with my team mates to work on together. I'm especially excited to show these off to some co-workers and friends who have been reluctant about learning new client-side technologies, But how can I give them the confidence to work on these apps worrying about breaking the functionality I've implemented?

Requirements

For the purpose of this walk-thru -

  1. Start with an app based on our skeleton-navigation
  2. Make sure your karma.conf.js matches the most up-to-date release's!
  3. Make sure you're able to run the base skeleton-navigation repo's tests

Testing to the rescue

On most of the projects I work on, tests are an agreement between team members that this code was working when I pushed it out and I've written my tests with the confidence that as long as they are passing, you are probably good! There's always the edge cases that even the best developers miss but no one's perfect.

Alright let's see some code already!

Let's use a simple widget view-model -

import {bindable} from 'aurelia-templating';  
export class Widget {  
  @bindable isOpen = false;
  @bindable name = '';
  toggleOpen() {
    this.isOpen = !this.isOpen;
  }
  isOpenChanged(newValue) {
    if(newValue) {
      this.name = '';  
    }
  }
}

As you can see our widget can be in a state of open or closed, which probably triggers an if or show binding in the view to toggle content. When the toggleOpen method is called we want to toggle the open property of our widget. Whenever isOpen changes, we want to check if it is set to true and then clear the name if so.

Can't we just put the logic to clear the name in our toggleOpen method?

Well, we could but since isOpen is bindable we also want to confirm that if the view that composed our widget decided to change state that we also triggered the behavior.

Base skeleton test

I enjoy taking what I know works and expanding on that. In the spirit of sharing this is what my custom element ES6 spec's start out as -

import {Widget} from 'src/widget';  
import {Container} from 'aurelia-dependency-injection';  
import {BehaviorInstance} from 'aurelia-templating';

describe('Widget', () => {  
  let widget;
  let container;

  beforeEach(() => {
    container = new Container();
    widget = container.get(Widget);
  });
});
  1. We need to pull in the custom element's view-model that we are testing. Make sure this is a relative path or that you have a path in config.js to map it for you!
  2. We need to get a container from dependency-injection and use it to grab the instance of our view-model. If our view-model uses DI to resolve other dependencies and we want to spy on their methods we should use otherDep = container.get(OtherDep).

We also have a more in-depth testing library you can read more about here http://aurelia.io/hub.html#/doc/article/aurelia/testing/latest/testing-components

Super basic test - property exists

Start with the above 'skeleton test' in your my-app-name/test/unit/ directory and give it a name, such as widget.spec.js. The suffix .spec.js is what karma is looking for to know that it needs to run the tests inside the spec file.

Start Karma in your console -

$ karma start

For our first test, let's check if the name property exists on our newly instantiated widget -

import {Widget} from 'src/widget';  
describe('the Widget module', () => {  
  // ...
  beforeEach(() => {
    // ...
  });
  it('constructs with a name property', () => {
    expect(widget.name).toEqual('');
  });
});

We add our test nested inside of our describe method. For the duration of this walk-thru I'll assume you read that and put them one after each other :)

Our first assertion is that we have a name property that is equal to string empty, because that's the default value.

Basic test - interaction

Let's go a bit more in-depth and check that when our isOpen property changed that the name is cleared -

  it('clears the name property when isOpen is set to true', () => {
    widget.name = 'Testing';
    widget.isOpen = false;
    expect(widget.name).toEqual('');
  });

Looks great! But karma is reporting our test failed!

The reason why is that we need to open the thread up so that the Aurelia can call the proper propertyChanged method. Let's try again -

  it('clears the name property when isOpen is set to true', (done) => {
    widget.name = 'Testing';
    widget.isOpen = true;
    setTimeout(() => {
      expect(widget.name).toEqual('')
      done();
    });
  });

Karma should be reporting a passed test now!

By using setTimeout() we can let Aurelia react to the change in the isOpen property and clear our our name property. We could also use the task queue to handle this.

Also, you'll notice we added a argument to the test method named done which is a method we call after the test is finished. This is a feature of Karma and you can see it in their docs.

Basic test - testing the inverse

Let's also make sure that if our property is set to a falsey value that our name isn't cleared for good practice -

  it('doesnt clear the name property when isOpen is set to false', done => {
    let testValue = 'Testing'
    widget.name = testValue;
    widget.isOpen = false;
    setTimeout(() => {
      expect(widget.name).toEqual(testValue)
      done();
    });
  });

Great, now we can confirm that our changed method is being called properly and updating as we expect. Let's add one more basic test -

Intermediate test - results of method invocation

Let's make sure that when our element is clicked that it will actually trigger the property changed method -

  it('clears the name property when toggleOpen is clicked', done => {
    spyOn(widget, 'isOpenChanged');
    widget.name = 'testing';
    widget.toggleOpen();
    setTimeout(() => {
      expect(widget.isOpenChanged).toHaveBeenCalledWith(true, false)
      done();
    });
  });

Here we use a spy to make sure that the call to the toggleOpen() method properly invoked our isOpenChanged() method and passed in the right parameters (in this case, it is the new value and the previous value of false and true)

Wrap-up

We've looked a few basic ways we can test our custom elements. These tests alone aren't enough to cover our widget fully but they are a good start.

Now, once your teammate comes to check out the shiney new widget you created they can easily understand what is going on without a bunch of code comments describing the behaviors. This is a really important aspect of testing - your tests should describe what your code does!

Thanks, and as always looking forward to being berated violently in the comments :)