Testing Basics

Unit tests (as well as container tests) are generally used to test a small piece of code and ensure that it is doing what was intended. Unlike application tests, they are narrow in scope and do not require the Ember application to be running.

Let's have a look at a common use case - testing a service - to understand the basic principles of testing in Ember. This will set the foundation for other parts of your Ember application such as controllers, components, helpers and others. Testing a service is as simple as creating a container test, looking up the service on the application's container and running assertions against it.

Testing Computed Properties

Let's start by creating a service that has a computedFoo computed property based on a foo property.

import Service from '@ember/service';
import { computed } from '@ember/object';
import { tracked } from '@glimmer/tracking';

export default class SomeThingService extends Service {
  @tracked foo = 'bar';

  get computedFoo() {
    return `computed ${this.foo}`;
  }
}

Within the test for this object, we'll lookup the service instance, update the foo property (which should trigger the computed property), and assert that the logic in our computed property is working correctly.

import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';

module('Unit | Service | some thing', function(hooks) {
  setupTest(hooks);

  test('should correctly concat foo', function(assert) {
    const someThing = this.owner.lookup('service:some-thing');
    someThing.foo = 'baz';

    assert.equal(someThing.computedFoo, 'computed baz');
  });
});

See that first, we are creating a new testing module using the QUnit.module function. This will scope all of our tests together into one group that can be configured and run independently from other modules defined in our test suite. Also, we have used setupTest, one of the several test helpers provided by ember-qunit. The setupTest helper provides us with some conveniences, such as the this.owner object, that helps us to create or lookup objects which are needed to setup our test. In this example, we use the this.owner object to lookup the service instance that becomes our test subject: someThing. Note that in a unit test you can customize any object under test by setting its properties accordingly. We can use the set method of the test object to achieve this.

Testing Object Methods

Next let's look at testing logic found within an object's method. In this case the testMethod method alters some internal state of the object (by updating the foo property).

import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';

export default class SomeThingService extends Service {
  @tracked foo = 'bar';

  testMethod() {
    this.foo = 'baz';
  }
}

To test it, we create an instance of our class SomeThing as defined above, call the testMethod method and assert that the internal state is correct as a result of the method call.

import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';

module('Unit | Service | some thing', function(hooks) {
  setupTest(hooks);

  test('should update foo on testMethod', function(assert) {
    const someThing = this.owner.lookup('service:some-thing');

    someThing.testMethod();

    assert.equal(someThing.foo, 'baz');
  });
});

In case the object's method returns a value, you can simply assert that the return value is calculated correctly. Suppose our object has a calc method that returns a value based on some internal state.

import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';

export default class SomeThingService extends Service {
  @tracked count = 0;

  calc() {
    this.count += 1;
    return `count: ${this.count}`;
  }
}

The test would call the calc method and assert it gets back the correct value.

import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';

module('Unit | Service | some thing', function(hooks) {
  setupTest(hooks);

  test('should return incremented count on calc', function(assert) {
    const someThing = this.owner.lookup('service:some-thing');

    assert.equal(someThing.calc(), 'count: 1');
    assert.equal(someThing.calc(), 'count: 2');
  });
});

Skipping tests

Some times you might be working on a feature, but know that a certain test will fail so you might want to skip it. You can do it by using skip:

import { test, skip } from 'qunit';

test('run this test', function(assert) {
  assert.ok(true);
});

skip('skip this test', function(assert) {
  assert.ok(true);
});

Stubs

Unit tests are often testing methods that call other methods or work with other objects. A stub is a substitute method or object to be used during the test. This isolates a unit test to the actual method under test.

Stubbing a method

import Service from '@ember/service';

export default class SomeThingService extends Service {
  someComplicatedOtherMethod(x) {
    return x * 2;
  }

  testMethod(y) {
    let z = this.someComplicatedOtherMethod(y);
    return `Answer: ${z}`;
  }
}

someComplicatedOtherMethod might have complex behavior that you do not want failing your unit test for testMethod, because you know testMethod works otherwise. Isolating unit tests is best practice because the tests that are failing should directly point to the method that is failing, allowing you to quickly fix it rather than figuring out which method the error is in. In we stub the other method:

import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';

module('Unit | Service | some thing', function(hooks) {
  setupTest(hooks);

  test('testMethod should return result of someComplicatedOtherFunction', function(assert) {
    const someThing = this.owner.lookup('service:some-thing');
    const originalSomeComplicatedOtherMethod =
      someThing.someComplicatedOtherMethod;
    someThing.someComplicatedOtherMethod = function() {
      return 4;
    };

    assert.equal(someThing.testMethod(2), 'Answer 4', 'testMethod is working');

    someThing.someComplicatedOtherMethod = originalSomeComplicatedOtherMethod;
  });
});

Stubbing an object

You can also stub an object:

import Service from '@ember/service';

export default class EmployeesService extends Service {
  employees = [];

  hire(person) {
    person.addJob();
    this.employees.push(person);
    return `${person.title} ${person.name} is now an employee`;
  }
}

Here, you need to pass a person object, which could be a complex class. The addJob method in Person could be complex as well, perhaps requiring another class. Instead, create a simple object and pass it instead.

import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';

module('Unit | Service | employees', function(hooks) {
  setupTest(hooks);

  test('hire adds a person to employees array', function(assert) {
    const someThing = this.owner.lookup('service:some-thing');

    class MockPerson {
      title = 'Dr.';
      name = 'Zoey';
      addJob() {}
    }

    let person = new MockPerson();

    assert.equal(someThing.hire(person), 'Dr. Zoey is now an employee');
  });
});

© 2020 Yehuda Katz, Tom Dale and Ember.js contributors
Licensed under the MIT License.
https://guides.emberjs.com/v3.25.0/testing/unit-testing-basics