Over a million developers have joined DZone.

Testing AngularDart Applications

· DevOps Zone

Discover how to optimize your DevOps workflows with our cloud-based automated testing infrastructure, brought to you in partnership with Sauce Labs

One of the great things about the Angular framework is how it enables the testability of your applications. In this article I will show different approaches to testing services, formatters, decorators, and components.

Notes

AngularDart Is Still in the Works

AngularDart is still in the works and changing quite a bit. I will keep this article up-to-date as new versions of the framework get released.

Know a Better Way? Let Me Know.

If you see a mistake in the code samples, or you have a suggestion to improve them, please, let me know.

Guinness

All the code samples are written using the Guinness testing library (you can find the intro here), but there is nothing in the samples that cannot be easily translated into unittest, or any other testing library.

DartMocks

I am using the DartMocks library for test doubles (you can find the intro here), but once again there is nothing DartMocks specific in the samples.

Setting Up Karma

I am a strong believer in the importance of running your tests continuously and painlessly. Karma is a tool that enables that. I wrote a step-by-step tutorial explaining how to set it up for a Dart project, which you can find here.

Testing Services

Suppose we have a service that makes requests to two different endpoints and then merges the results.

@Injectable()
class SearchService {
  final Http http;
  SearchService(this.http);

  Future<List> search(String query) =>
      _merge([
        _request("api1/search?query=${query}"),
        _request("api2/search?query=${query}")
      ]);

  Future _request(url) => http.get(url).then((_) => _.data);

  Future _merge(futures) =>
      Future.wait(futures).then((res) => res.expand((_) => _));
}

Let’s see how we can go about testing this service.

Without Angular

When building applications with Angular, we use plain old Dart objects. And as a result, we do not have to use any framework helpers to test them.

First, we need to define a test double for the Http service.

@proxy class _Http extends TestDouble implements Http {}

Next step is to define a fake implementation of HttpResponse.

class _HttpResponse { var data; }

And a factory function to create one.

fakeResponse(data) => new Future.value(new _HttpResponse()..data = data);

Having all of these defined, we can write our test as follows:

describe("SearchService", () {
  it("should merge the results from two endpoints", () {
    final http = new _Http();

    http.stub("get").args("api1/search?query=abc")
        .andReturn(fakeResponse(["one", "two"]));

    http.stub("get").args("api2/search?query=abc")
        .andReturn(fakeResponse(["three"]));

    final searchService = new SearchService(http);

    return searchService.search("abc").then((res) {
      expect(res).toEqual(["one", "two", "three"]);
    });
  });
});

It is a very simple, straightforward test. There is nothing Angular specific in it.

With Angular

Let’s rewrite the test, but this time with some help from the framework.

library my_lib_test;

import '../web/my_lib.dart';
import 'package:guinness/guinness.dart';
import 'package:unittest/unittest.dart' hide expect;
import 'package:angular/angular.dart';

import 'package:angular/mock/module.dart';    

describe("SearchService", () {
  beforeEach(setUpInjector);
  afterEach(tearDownInjector);

  beforeEach(module((Module m) => m..bind(SearchService)));

  it("should merge the results from two endpoints", 
    inject((MockHttpBackend http, SearchService searchService) {

    http.whenGET("api1/search?query=abc").respond('["one", "two"]');
    http.whenGET("api2/search?query=abc").respond('["three"]');

    final r = searchService.search("abc");

    scheduleMicrotask(() {
      http.flush();

      r.then(expectAsync((res) {
        expect(res).toEqual(["one", "two", "three"]);
      }));
    });
  }));
});

Let’s look closer at what is happening here:

  • import 'package:angular/mock/module.dart;' imports all Angular test helpers.
  • setUpInjector creates an instance of Injector that is used to instantiate the objects required in the test.
  • beforeEach(module((Module m) => m..bind(SearchService))); registers the service under test with the injector.
  • inject is used to get all the services we need in our test.
  • Note that we did not have to define a test double for Http, and instead used MockHttpBackend provided by Angular. We also did not not have to create an instance of SearchService. Similar to production code Angular takes care of creating all the services and wiring up all the dependencies.

Refactoring

The test looks a bit cumbersome because of scheduleMicrotask and http.flush. If you have a bunch of tests relying on the Http service, it is worth creating a helper to improve their readability:

waitForHttp(future, callback) =>
    scheduleMicrotask(() {
      inject((MockHttpBackend http) => http.flush());
      future.then(callback);
    });

Given this helper, we can rewrite out test as follows:

it("should merge the results from two endpoints", 
  inject((MockHttpBackend http, SearchService searchService) {

  http.whenGET("api1/search?query=abc").respond('["one", "two"]');
  http.whenGET("api2/search?query=abc").respond('["three"]');

  waitForHttp(searchService.search("abc"), (res) {
    expect(res).toEqual(["one", "two", "three"]);
  });
}));

With or Without Angular?

Since there are different approaches to testing, there is no right or wrong answer here. I personally prefer not using any frameworky stuff unless I have to, and Angular is probably the only client-side framework where it is possible.

Testing Formatters

Suppose we would like to test the following formatter:

@Formatter(name:'i18n')
class I18n {
  final Translations t;
  I18n(this.t);

  String call(String str, [Map opts]) =>
      t.hasTranslation(str) ? t.translate(str, opts) : "NO TRANSLATION";
}

Where Translations is defined as follows:

abstract class Translations {
  String translate(String str, Map opts);
  bool hasTranslation(String str);
}

Once again, let’s look at two ways of testing it: with and without Angular helpers.

Without Angular

describe("i18n", () {
  it("should return the translated string if there is one", () {
    final t = new _Translations()
        ..stub("hasTranslation").andReturn(true)
        ..stub("translate").andReturn("after");

    final i18n = new I18n(t);

    expect(i18n("before")).toEqual("after");
  });

  it("should return a placeholder otherwise", () {
    final t = new _Translations()
        ..stub("hasTranslation").andReturn(false);

    final i18n = new I18n(t);

    expect(i18n("before")).toEqual("NO TRANSLATION");
  });
});

Where similar to _Http we have the _Translations test double defined as follows:

@proxy class _Translations extends TestDouble implements Translations {}

With Angular

describe("i18n", () {
  beforeEach(setUpInjector);
  afterEach(tearDownInjector);

  beforeEach(module((Module m) => m
      ..bind(Translations, toImplementation:_Translations)
      ..bind(I18n)));

  it("should return the translated string if there is one", 
    inject((Translations t, I18n i18n) {

    t.stub("hasTranslation").andReturn(true);
    t.stub("translate").andReturn("after");

    expect(i18n("before")).toEqual("after");
  }));

  it("should return a placeholder otherwise", 
    inject((Translations t, I18n i18n) {

    t.stub("hasTranslation").andReturn(false);

    expect(i18n("before")).toEqual("NO TRANSLATION");
  }));
});
  • Since Translations is not a standard Angular service, the framework does not provide a mock version of it. So we still have to define the test double ourselves.
  • We cannot just register the _Translations service using bind(_Translations), because in this case Angular would not be able to instantiate the I18n formatter.

Testing Decorators

Suppose we have a decorator that disables a button after the first click.

@Decorator(selector: '[click-once]')
class ClickOnce {
  ClickOnce(Element el) {
    el.onClick.first.then((_) => el.disabled = true);
  }
}

Without Angular

In general, I find that decorators tend to heavily depend on the framework (e.g, on Scope), and consequently I almost always test them using the Angular testing machinery. But since ClickOnce is so simple, it can be done without any helpers.

describe("ClickOnce", () {
  it("should disable the element after the first click", () {
    final btn = new ButtonElement();
    new ClickOnce(btn);

    btn.click();

    expect(btn.disabled).toBeTrue();
  });
});

With Angular

describe("ClickOnce", () {
  beforeEach(setUpInjector);
  afterEach(tearDownInjector);

  beforeEach(module((Module m) => m..bind(ClickOnce)));

  it("should disable the element after the first click", 
    inject((TestBed tb) {

    final btn = tb.compile("<button click-once></button>");

    tb.triggerEvent(btn, "click");

    expect(btn.disabled).toBeTrue();
  }));
});

The TestBed object is provided by Angular and is used for compiling html and triggering events.

Testing Components

Let’s say we have a component, a form where we can edit an address.

@Component(
    publishAs: 'form',
    templateUrl: "templates/address-form.html",
    selector: 'address-form',
    map: const {
      'address' : '=>address'
    })
class AddressForm {
  Address address;

  AddressForm(Scope scope) {
    scope.watch("form.address.country", (newValue, oldValue) {
      if (oldValue != null) address.city = "";
    });
  }
}

Where Address is defined as follows:

class Address {
  String country;
  String city;

  Address(this.country, this.city);
}

And address-form.html looks like this:

Address

<label>
  Country:
  <input type="text" ng-model="form.address.country" name="country">
</label>

<label>
  City:
  <input type="text" ng-model="form.address.city" name="city">
</label>

We are using Scope to set up a watch to blank out city every time country changes.

Let’s look at the first test.

describe("AddressForm", () {
  beforeEach(setUpInjector);
  afterEach(tearDownInjector);

  beforeEach(module((Module m) => m..bind(AddressForm)));
  beforeEach(loadTemplates(["address-form.html"]));

  it("should display the given address", 
    async(inject((TestBed tb, Scope scope) {

    scope.context["addr"] = new Address("Canada", "Toronto");
    final form = tb.compile("<address-form address='addr'/>", scope: scope);

    microLeap();
    tb.rootScope.apply();

    final root = form.shadowRoot;
    final country = root.querySelector("input[name=country]");
    final city = root.querySelector("input[name=city]");

    expect(country.value).toEqual("Canada");
    expect(city.value).toEqual("Toronto");
  })));
});

Let me walk you through it:

  • Similar to the previous tests we have to invoke setUpInjector, tearDownInjector, and m..bind(AddressForm) to set up the test environment.

  • When compiling the address form Angular will try to download the corresponding template. But since the Http service is stubbed out, it will not be able to do it. A common practise is to preload all the needed templates into the template cache before running tests. That is what loadTemplates does. There are many other ways to do the same thing depending on your setup. So this particular implementation may not work for you.

loadTemplates(List<String> templates) {
  return () {
    updateCache(template, response) => inject((TemplateCache cache) => cache.put(template, response));

    final futures = templates.map((template) => HttpRequest.request('base/web/templates/$template', method: "GET").
      then((_) => updateCache("templates/$template", new HttpResponse(200, _.response))));

    return Future.wait(futures);
  };
}

  • The async helper captures all scheduleMicrotask calls, so they can be run by calling microLeap. These helpers allow you to make your code look sequential, which reduces its nesting.

  • Similar to the decorator’s test tb.compile compiles the component and returns its root element.
  • tb.rootScope.apply(); runs the digest.
  • Finally, since the component uses shadow DOM, we have to access its contents through its shadow root.

Let’s write one more test before we explore some refactorings that can be applied here.

it("should blank out city when country changes", 
  async(inject((TestBed tb, Scope scope) {

  scope.context["addr"] = new Address("Canada", "Toronto");
  final form = tb.compile("<address-form address='addr'/>", scope: scope);

  microLeap();
  tb.rootScope.apply();

  final root = form.shadowRoot;
  final country = root.querySelector("input[name=country]");
  final city = root.querySelector("input[name=city]");

  country.value = 'Australia';
  tb.triggerEvent(country, "change");

  expect(city.value).toEqual("");
})));

Refactoring

That was quite a lot of code to test a simple component. Thankfully, most of it can be extracted into general helpers that can be used throughout your application.

For example, these four lines:

beforeEach(setUpInjector);
afterEach(tearDownInjector);

beforeEach(module((Module m) => m..bind(AddressForm)));
beforeEach(loadTemplates(["address-form.html"]));

can be extracted into a function:

setUpAngular({List templates, List injectables}) {
  beforeEach(setUpInjector);
  afterEach(tearDownInjector);

  beforeEach(module((Module m) => injectables.forEach(m.bind)));
  beforeEach(loadTemplates(templates));
}

And all the code responsible for the compilation process can be extracted too:

compileComponent(String html, Map scopeData, callback){
  return async(inject((TestBed tb, Scope scope) {
    scopeData.forEach((k, v) => scope.context[k] = v);
    final el = tb.compile(html, scope: scope);

    microLeap();
    tb.rootScope.apply();

    callback(el.shadowRoot, tb);
  }));
}

Now, with these two helpers in place, the tests can be rewritten as follows:

describe("AddressForm", () {
  setUpAngular(
      injectables: [AddressForm], 
      templates: ["address-form.html"]);

  it("should display the given address", compileComponent(
      "<address-form address='addr'/>", 
      {"addr" : new Address("Canada", "Toronto")}, (root, tb){

        final country = root.querySelector("input[name=country]");
        final city = root.querySelector("input[name=city]");

        expect(country.value).toEqual("Canada");
        expect(city.value).toEqual("Toronto");
      }
  ));

  it("should display the given address", compileComponent(
      "<address-form address='addr'/>", 
      {"addr" : new Address("Canada", "Toronto")}, (root, tb){

        final country = root.querySelector("input[name=country]");
        final city = root.querySelector("input[name=city]");

        country.value = 'Australia';
        tb.triggerEvent(country, "change");

        expect(city.value).toEqual("");
  }));
});

Summing Up

  • One of the great things about Angular is that it does not make us extend anything, and we can just use plain old Dart objects. This enables testing them without any help from the framework.

  • There are, however, cases where it becomes problematic, especially when testing decorators and components. To help us with that Angular provides such functions as setUpInjector, MockHttpBackend, TestBed, async, microLeap, inject, and others.

  • These built-in helpers can also be used for defining higher-level utilities, such as setUpAngular or renderComponent.

Download “The DevOps Journey - From Waterfall to Continuous Delivery” to learn learn about the importance of integrating automated testing into the DevOps workflow, brought to you in partnership with Sauce Labs.

Topics:

Published at DZone with permission of Victor Savkin, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}