Over a million developers have joined DZone.

Unit Testing AngularJS Controllers, Views, and More: Part Three

· Web Dev Zone

The AngularJS-RequireJS-Seed project for this guide is here.
Duck-Angular is here.

The Story so Far

We will continue from where we left off, which is having written unit tests for two of our controllers. We’ve seen two techniques so far.

  • The plain constructor approach, by virtue of starting off a controller/service/factory, etc. as a RequireJS module. Here, we’ve had to necessarily supply all the dependencies to the controller.
  • The bootstrapped app approach, where we bootstrapped the full app, used the $controller service to access the controller, and pass in a subset of the dependencies to the controller. Dependencies that we did not inject, were fulfilled with their production values.

Unit Testing a View

However, so far, views have not featured in any of our tests. It is time to do that. This will also illustrate some of the convenience of using Duck-Angular. Before illustrating this aspect, we’ll make some small additions to controller2.js and its corresponding view.

We add two new scope methods to controller2.js.

$scope.changeSomeText = function() {
  $scope.data = "Some New Data";
};

$scope.refreshData = function() {
  return service2.get().then(function(data) {
    $scope.data = data;
  });
};

Pretty self-explanatory, changeSomeText() modifies $scope.data. refreshData() does too, but it refetches the data from service2 before doing this. We also modify the view, in this case, route2.html, like so:

<div>
  This is route #2
  <a href="" ng-click="go()">Click here to go to Route 1</a>
  Data: <span id="data">{{data}}</span>
  <div>
    <a id="changeLink" href="" ng-click="changeSomeText()">Click here to change data</a>
  </div>
  <div>
    <a id="refreshLink" href="" ng-click="refreshData()">Click here to refresh data</a>
  </div>
</div>

This simply adds two links which call the functions that we just grafted onto our scope. If you run the app, you will see two links, one to modify the data (to “Some New Data“), the other to refresh the data (to “Some Data“).

Our first test (controller1-view-test.js) will look like this:

it("can show data", function () {
  return mother.createMvc("route2Controller", "../templates/route2.html", {}).then(function (mvc) {
    var dom = new DuckDOM(mvc.view, mvc.scope);
    expect(dom.element("#data")[0].innerText).to.eql("Some Data");
  });
});

There is a bunch of things we do here, so pay attention.

  • The createMvc() function is responsible for setting up the controller and scope, and binding the view to this scope. Along the way, any dependencies that we need to inject explicitly, can be done through the third parameter. In this case, we don’t really have to, so we just pass in an empty hash.

  • createMvc() returns a promise, which, upon resolution, returns us an object that we refer to as mvc. The mvc object contains a bunch of things. Some of them are the scope, the controller, and the compiled template. The compiled template reflects what the view would look like the first time it is initialised, after the controller has had the chance to set it up. The mvc object also contains the injector, in case we need to retrieve some other registered objects.

  • Once this has been set up, we initialise a DuckDOM object. This is really a thin wrapper over JQuery/jqLite with some smarts built-in with regard to user interaction. The DuckDOM object needs the view and the scope to be of any use, which we grab from the mvc object.

  • The expectation simply checks that the inner text of the “data” element is equal to “Some Data“.

This allows us to test scope/view bindings very cheaply. No Selenium, no external browser, just a unit test.

Asserting on User Interactions

But that is not all. On without pause to the good bit, let’s test user interactions.

it("can update data", function () {
  return mother.createMvc("route2Controller", "../templates/route2.html", {}).then(function (mvc) {
    var dom = new DuckDOM(mvc.view, mvc.scope);
    var interaction = new UIInteraction(dom);
    expect(dom.element("#data")[0].innerText).to.eql("Some Data");
    dom.interactWith("#changeLink");
    expect(dom.element("#data")[0].innerText).to.eql("Some New Data");
  });
});

This test builds upon the first one. Critically, it adds an interactWith() call, which triggers a click on the link with ID “changeLink”. The succeeding expectation asserts that the data in the view has indeed changed. Again, note: this is just a unit test. This lets us test user interactions quite quickly.

Handling Asynchronous User Interactions

In most scenarios, the result of a user interaction may be an asynchronous action, like a service call. The refreshData() method defined in controller2.js is one such example. If we want to test this interaction, we’ll need to make a slight change. We’ll have to tell Duck-Angular which method it should wait for, before proceeding to making assertions. Without this information, the test run could potentially be unpredictable.

The method which actually performs the service2.get() call is refreshData(). Thus, it is logical to wait until it finishes. Or, in terms of promises, we wait until the refreshData() promise is fulfilled.

it("can reflect data that is refreshed asynchronously", function () {
  return mother.createMvc("route2Controller", "../templates/route2.html", {}).then(function (mvc) {
    var dom = new DuckDOM(mvc.view, mvc.scope);
    var interaction = new UIInteraction(dom);
    expect(dom.element("#data")[0].innerText).to.eql("Some Data");
    dom.interactWith("#changeLink");
    expect(dom.element("#data")[0].innerText).to.eql("Some New Data");
    return interaction.with("#refreshLink").waitFor(mvc.scope, "refreshData").then(function() {
      expect(dom.element("#data")[0].innerText).to.eql("Some Data");
    });
  });
});

Currently, Duck-Angular supports interactions with all common UI elements like buttons, links, checkboxes, radio buttons, dropdowns, and text boxes. Extra behaviour can be easily added by modifying the interactWith() method in duck-angular.js.

What Next?

Let’s review what we set out to do:

  • We wanted to unit test controller logic independent of AngularJS.
  • We wanted to unit test scope bindings in templates.
  • We wanted to unit test user interactions, and their consequences on views.

Now that the most basic demonstration is out of the way, I intend the next post to cover the following topics:

  • Good practices while unit testing with promises
  • The pitfalls of $q
  • How Duck-Angular resolves partials (templates included within templates)
  • How Duck-Angular achieves waitFor()




Topics:

Published at DZone with permission of Avishek Sen Gupta, 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 }}