Статьи

Тестирование приложений AngularJS

Эта статья является второй в серии об изучении AngularJS . Он описывает, как протестировать простое приложение AngularJS. В предыдущей статье « Начало работы с AngularJS» я показал, как разработать простую функцию поиска и редактирования.

Что вы узнаете

Вы научитесь использовать Jasmine для модульного тестирования контроллеров и Protractor для интеграционного тестирования. В документации Angular есть хорошее руководство для разработчиков по модульному тестированию, если вам нужна дополнительная информация о тестировании и почему это важно.

Лучшая причина для написания тестов — автоматизировать тестирование. Без тестов вы, скорее всего, будете тестировать вручную. Это ручное тестирование будет занимать все больше и больше времени по мере роста вашей кодовой базы.

Что вам нужно

  • Около 15-30 минут
  • Любимый текстовый редактор или IDE. Мы рекомендуем IntelliJ IDEA .
  • Git установлен.
  • Node.js и NPM установлены.

Получить учебный проект

Клонируйте репозиторий angular-tutorial с помощью git и установите зависимости.

git clone https://github.com/mraible/angular-tutorial.git
cd angular-tutorial
npm install

Если вы еще не завершили учебное пособие « Начало работы с AngularJS» , вам следует внимательно изучить его, чтобы понять, как работает это приложение. Вы также можете просто запустить приложение с помощью «npm start» и просмотреть его в своем браузере по адресу http: // localhost: 8000 / app / .

Проверьте SearchController

  1. Создайте app/search/search_test.jsи заполните его базовой тестовой инфраструктурой. Этот код устанавливает макет , SearchServiceкоторый имеет первую функцию мы хотим проверить: query(term). Используется $provideдля переопределения по умолчанию SearchService. Контроллеры угловых юнит-тестов — служба имитации внутри контроллера была полезным вопросом для Stack Overflow, чтобы выяснить, как имитировать сервисы в контроллерах.

    'use strict';
     
    describe('myApp.search module', function() {
        var mockSearchService;
     
        beforeEach(module('myApp.search', function($provide) {
            mockSearchService = {query: function(term) {}};
            $provide.value("SearchService", mockSearchService);
        }));
    });
  2. Измените karma.conf.js(в корневом каталоге), чтобы добавить реализацию поиска и проверить.

    files : [
      ...
      'app/components/**/*.js',
      'app/search/search.js',
      'app/search/search_test.js',
      'app/view*/**/*.js'
    ],
  3. Добавьте первый тест в search_test.js. Этот тест проверяет, что установка условия поиска и выполнение search()функции вызовет службу и вернет результаты. Вы можете видеть, что результаты, возвращенные службой, высмеиваются deferred.resolve(). deferred.resolve()Вызов , как обрабатывать обещания модульных тестов . Связанный: Введение в модульное тестирование: Шпионы — хорошее введение в использование шпионов в модульных тестах.

    describe('search by term', function() {
        var scope, rootScope, controller, deferred;
     
        // setup the controller with dependencies.
        beforeEach(inject(function($rootScope, $controller, $q) {
            rootScope = $rootScope;
            scope = $rootScope.$new();
            controller = $controller('SearchController', {$scope: scope, SearchService: mockSearchService });
            deferred = $q.defer();
        }));
     
        it('should search when a term is set and search() is called', function() {
            spyOn(mockSearchService, 'query').andReturn(deferred.promise);
            scope.term = 'M';
            scope.search();
            deferred.resolve({data: {name: "Peyton Manning"}});
            rootScope.$apply();
            expect(scope.searchResults).toEqual({name: "Peyton Manning"});
        });
    });
  4. Запустите следующую команду в командной строке, чтобы запустить тестер кармы. Вы можете оставить этот процесс запущенным, и новые тесты будут запускаться автоматически. Вы можете изменить проверенные данные и ожидания, чтобы увидеть, что ваш тест не пройден.

    npm test
    Запуск тестов из IntelliJ IDEA

    См.
    Раздел «Запуск модульных тестов на Karma», чтобы узнать, как запускать тесты из IntelliJ IDEA.

  5. Добавьте тест, чтобы убедиться, что поиск происходит автоматически, когда термин входит в URL. Обратите внимание, как структура кода должна была немного измениться для обработки $routeParams.

    describe('search by term automatically', function() {
        var scope, rootScope, controller, location, deferred;
     
        beforeEach(inject(function($rootScope, $controller, $q) {
            rootScope = $rootScope;
            scope = $rootScope.$new();
     
            // in this case, expectations need to be setup before controller is initialized
            var routeParams = {"term": "peyton"};
            deferred = $q.defer();
            spyOn(mockSearchService, 'query').andReturn(deferred.promise);
            deferred.resolve({data: {name: "Peyton Manning"}});
     
            controller = $controller('SearchController', {$scope: scope, $routeParams: routeParams, SearchService: mockSearchService });
        }));
     
        it('should search automatically when a term is on the URL', function() {
            rootScope.$apply();
            expect(scope.searchResults).toEqual({name: "Peyton Manning"});
        });
    });
  6. Добавьте тест для проверки EditControllerработ, как ожидалось.

    describe('edit person', function() {
        var scope, rootScope, controller, location, deferred;
     
        beforeEach(inject(function($rootScope, $controller, $q) {
            rootScope = $rootScope;
            scope = $rootScope.$new();
     
            // expectations need to be setup before controller is initialized
            var routeParams = {"id": "1"};
            deferred = $q.defer();
            spyOn(mockSearchService, 'fetch').andReturn(deferred.promise);
            deferred.resolve({data: {name: "Peyton Manning", address: {street: "12345 Blake Street", city: "Denver"}}});
     
            controller = $controller('EditController', {$scope: scope, $routeParams: routeParams, SearchService: mockSearchService });
        }));
     
        it('should fetch a single record', function() {
            rootScope.$apply();
            expect(scope.person.name).toBe("Peyton Manning");
            expect(scope.person.address.street).toBe("12345 Blake Street");
        });
    });

    После добавления этого теста вы, вероятно, увидите следующую ошибку в своем терминале.

    Chrome 40.0.2214 (Mac OS X 10.10.2) myApp.search module edit person should fetch a single record FAILED
        fetch() method does not exist
        TypeError: Cannot read property 'name' of undefined

    Это происходит потому, что mockSearchServiceне определен метод выборки. Измените beforeEach()строку 7, чтобы добавить эту функцию.

    mockSearchService = {query: function(term) {}, fetch: function(id) {}};

Дополнительный кредит

Создать тест для спасения человека. Вот вопрос о переполнении стека, который может помочь вам проверить местоположение после выполнения сохранения.

Проверьте функцию поиска

Чтобы проверить, работает ли приложение полностью, вы можете написать сценарии с помощью Protractor . Они также известны как интеграционные тесты, так как они тестируют интеграцию между всеми уровнями вашего приложения.

Чтобы проверить работу сквозных тестов в проекте перед началом, выполните следующую команду в одном окне терминала:

npm start

Затем в другом окне выполните следующую команду, чтобы выполнить тесты:

npm run protractor

1. Напишите свой первый интеграционный тест, чтобы убедиться, что вы можете перейти к / search и ввести поисковый запрос, чтобы увидеть результаты. Добавьте следующее к e2e-tests/scenarios.js:

describe('search', function() {
  var searchTerm = element(by.model('term'));
  var searchButton = element(by.id('search'));
 
  beforeEach(function() {
    browser.get('index.html#/search');
  });
 
  it('should allow searching at /search', function() {
    searchTerm.sendKeys("M");
    searchButton.click().then(function() {
      expect(element.all(by.repeater('person in searchResults')).count()).toEqual(3);
    });
  });
});

Переменная searchTerm представляет поле ввода. В by.model()синтаксических связывается с атрибутом «нг-модели» вы определили в HTML.

2. Запустите «Запуск транспортира npm». Это должно произойти со следующей ошибкой.

[launcher] Running 1 instances of WebDriver
Selenium standalone server started at http://172.16.6.39:64230/wd/hub
...F
Failures:
  1) my app search should allow searching at /search
   Message:
     NoSuchElementError: No element found using locator: By.id("search")

3. Для исправления необходимо добавить атрибут «id» к кнопке «Поиск» в app/search/index.html.

<form ng-submit="search()">
    <input type="search" name="search" ng-model="term">
    <button id="search">Search</button>
</form>

4. Запустите тесты снова, используя «npm run protractor». Они все должны пройти это время.

5. Напишите еще один тест, чтобы убедиться, что редактирование пользователя отображает его информацию.

describe('edit person', function() {
  var name = element(by.model('person.name'));
  var street = element(by.model('person.address.street'));
  var city = element(by.model('person.address.city'));
 
  beforeEach(function() {
    browser.get('index.html#/edit/1');
  });
 
  it('should allow viewing a person', function() {
    // getText() doesn't work with input elements, see the following for more information:
    // https://github.com/angular/protractor/blob/master/docs/faq.md#the-result-of-gettext-from-an-input-element-is-always-empty
    expect(name.getAttribute('value')).toEqual("Peyton Manning");
    expect(street.getAttribute('value')).toEqual("1234 Main Street");
    expect(city.getAttribute('value')).toEqual("Greenwood Village");
  });
});

Убедитесь, что он работает с «npm run protractor».

6. Наконец, напишите тест, чтобы убедиться, что вы можете сохранить человека, и его данные обновляются. Эта проблема помогла выяснить, как проверить URL-адрес после его изменения .

describe('save person', function() {
  var name = element(by.model('person.name'));
  var save = element(by.id('save'));
 
  beforeEach(function() {
    browser.get('index.html#/edit/1');
  });
 
  it('should allow updating a name', function() {
    name.sendKeys(" Updated");
    save.click().then(function() {
      // verify url set back to search results
      browser.driver.wait(function() {
        return browser.driver.getCurrentUrl().then(function(url) {
          expect(url).toContain('/search/Peyton%20Manning%20Updated');
          return url;
        });
      });
      // verify one element matched this change
      expect(element.all(by.repeater('person in searchResults')).count()).toEqual(1);
    });
  });
});

7. Когда вы запускаете этот тест с «npm run protractor», он должен завершиться неудачей, потому что нет элемента с id="save"in app/search/edit.html. Добавьте его к кнопке Сохранить в этом файле и повторите попытку. Вы должны увидеть что-то похожее на следующее:

Finished in 4.478 seconds
6 tests, 9 assertions, 0 failures

Исходный код

Завершенный проект с этим кодом доступен на GitHub по адресу https://github.com/mraible/angular-tutorial в разделе тестирования .

Есть два коммита, которые вносят изменения для двух основных шагов в этом уроке:

Резюме

Надеюсь, вам понравился этот быстрый и легкий учебник по тестированию приложений AngularJS. Первая пара приложений AngularJS, которые я разработал, не имела тестов и требовала много ручного тестирования, чтобы проверить их качество. Узнав, что тестировать приложения AngularJS довольно легко, я теперь делаю это на всех своих проектах. Надеюсь, этот урок мотивирует вас сделать то же самое.