Статьи

Модульное тестирование контроллеров AngularJS, представлений и многого другого: часть третья

Проект AngularJS-RequireJS-Seed для этого руководства находится здесь .
Утка-Угловая здесь .

История до сих пор

Мы продолжим с того места, где остановились, написав модульные тесты для двух наших контроллеров. Мы уже видели две техники.

  • Подход простого конструктора благодаря запуску контроллера / службы / фабрики и т. Д. В качестве модуля RequireJS. Здесь мы должны были обязательно предоставить все зависимости контроллеру.
  • Подход с загрузкой приложения, когда мы загружали полное приложение, использовали службу $ controller для доступа к контроллеру и передачи подмножества зависимостей в контроллер. Зависимости, которые мы не вводили, удовлетворялись их производственными ценностями.

Модульное тестирование представления

Тем не менее, пока что представления не были показаны ни в одном из наших тестов. Настало время сделать это. Это также проиллюстрирует некоторые удобства использования Duck-Angular. Прежде чем проиллюстрировать этот аспект, мы сделаем несколько небольших дополнений к controller2.js и его соответствующему представлению.

Мы добавили два новых метода области видимости в controller2.js .

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

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

Довольно понятно , changeSomeText () изменяет $ scope.data . RefreshData () делает тоже самое , но refetches данные из service2 , прежде чем делать это. Мы также изменим представление, в данном случае, route2.html , примерно так:

<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>

Это просто добавляет две ссылки, которые вызывают функции, которые мы только что внедрили в нашу область. Если вы запустите приложение, вы увидите две ссылки: одну для изменения данных (на « Некоторые новые данные »), другую для обновления данных (на « Некоторые данные »).

Наш первый тест ( controller1-view-test.js ) будет выглядеть так:

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

Здесь мы делаем кучу вещей, поэтому обратите внимание.

  • Функция createMvc () отвечает за настройку контроллера и области действия и привязывает представление к этой области. Попутно любые зависимости, которые нам нужно внедрить явно, можно сделать через третий параметр. В этом случае нам на самом деле не нужно, поэтому мы просто передаем пустой хеш.
  • createMvc () возвращает обещание, которое после разрешения возвращает нам объект, который мы называем mvc . Объект MVC содержит кучу вещей. Некоторые из них — это область действия, контроллер и скомпилированный шаблон. Скомпилированный шаблон отражает то, как будет выглядеть представление при первой инициализации, после того как у контроллера будет возможность его настроить. Объект mvc также содержит инжектор, на случай, если нам понадобится получить некоторые другие зарегистрированные объекты.
  • 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()