Работа с Angular сервисами и инжектором

Protractor, как инструмент для end-to-end тестирования Angular-приложений, тесно интегрирован с Angular-инфраструктурой. Одним из ключевых аспектов является взаимодействие с Angular-сервисами через инъекцию зависимостей (Dependency Injection, DI) и возможность работы с сервисами непосредственно из тестов.


Инжектор Angular и его роль

Angular использует инжектор, который управляет созданием и предоставлением сервисов по всему приложению. Инжектор хранит singleton-инстансы сервисов, что позволяет компонентам, директивам и другим сервисам получать доступ к одним и тем же объектам без ручного создания экземпляров.

В контексте тестирования это значит, что Protractor может обращаться к сервисам через глобальный Angular-инжектор и взаимодействовать с ними напрямую, минуя DOM. Это особенно полезно для:

  • настройки состояния приложения перед тестами,
  • подмены сервисов на mock-версии,
  • проверки бизнес-логики без необходимости повторять действия пользователя через интерфейс.

Получение доступа к сервису

Protractor предоставляет возможность взаимодействовать с Angular-инжектором через объект browser.executeAsyncScript(). Это позволяет выполнять функции внутри Angular-контекста и получать доступ к сервисам:

browser.executeAsyncScript(function(callback) {
    var injector = angular.element(document.body).injector();
    var myService = injector.get('MyService');
    myService.getData().then(function(data) {
        callback(data);
    });
});

Здесь ключевые моменты:

  • angular.element(document.body).injector() — получение корневого инжектора приложения;
  • injector.get('MyService') — извлечение экземпляра нужного сервиса;
  • использование промисов (then) для асинхронной работы с сервисами, так как большинство Angular-сервисов возвращают промисы.

Mock-сервисы и подмена зависимостей

Для более изолированного тестирования часто требуется заменить реальный сервис на mock. Это можно сделать через Angular module и provide:

beforeEach(() => {
    browser.addMockModule('mockModule', function() {
        angular.module('mockModule', []).config(function($provide) {
            $provide.service('MyService', function() {
                this.getData = function() {
                    return Promise.resolve(['mocked', 'data']);
                };
            });
        });
    });
});

Особенности подхода:

  • browser.addMockModule() добавляет Angular-модуль перед загрузкой страницы;
  • $provide.service или $provide.value позволяет полностью подменить оригинальный сервис;
  • mock-сервис доступен только в контексте теста, не влияя на продакшен-код.

Асинхронная работа с сервисами

Protractor автоматически ожидает завершения Angular-зон ($digest циклов), но внутри executeAsyncScript необходимо явно использовать callback для передачи результата в тест.

Пример получения данных с асинхронного сервиса:

it('должен получить данные из сервиса', async () => {
    const data = await browser.executeAsyncScript(function(callback) {
        const injector = angular.element(document.body).injector();
        const myService = injector.get('MyService');
        myService.getData().then(callback);
    });
    expect(data).toEqual(['expected', 'values']);
});

Использование async/await в тестах делает код более читаемым и упрощает обработку промисов.


Взаимодействие с сервисами и DOM

Иногда нужно синхронизировать состояние сервиса с DOM. Например, после изменения данных через сервис необходимо дождаться обновления интерфейса:

await browser.executeAsyncScript(function(callback) {
    const injector = angular.element(document.body).injector();
    const myService = injector.get('MyService');
    myService.addItem('newItem').then(callback);
});

const listItems = element.all(by.css('.item'));
expect(await listItems.count()).toBe(1);
expect(await listItems.get(0).getText()).toBe('newItem');

Важные моменты:

  • сервис выполняет бизнес-логику, изменяющую модель;
  • Protractor автоматически ждёт $digest и обновление Angular-приложения, поэтому element.all возвращает актуальные элементы;
  • при сложных сценариях можно комбинировать работу с сервисами и обычными действиями пользователя (клики, ввод текста).

Особенности тестирования сервисов в Angular 2+ (Protractor с Angular CLI)

Для Angular 2+ подход аналогичен, но с учётом новой архитектуры:

  • вместо глобального angular используется window.getAllAngularTestabilities();
  • доступ к сервисам возможен через ng-probe / TestBed, если приложение собралось с ng.probe;
  • асинхронные сервисы возвращают Promise или Observable, что требует правильной обработки через await или .then().

Пример с Observable:

await browser.executeAsyncScript(function(callback) {
    const injector = window.ng.probe(document.body).injector;
    const myService = injector.get('MyService');
    myService.getObservableData().subscribe(data => callback(data));
});

Практические рекомендации

  1. Использовать инжектор только для сервисов, которые не зависят от DOM, иначе могут возникнуть гонки с рендерингом.
  2. Mock-сервисы лучше создавать через отдельный модуль, чтобы избежать конфликтов с продакшен-кодом.
  3. Асинхронные операции всегда оборачивать в callback или await, иначе тест завершится до получения данных.
  4. Комбинировать работу с сервисами и взаимодействие с интерфейсом, чтобы проверять полное поведение приложения.
  5. Не обращаться к приватным методам сервисов напрямую, тесты должны использовать только публичный API.

Работа с Angular-сервисами через Protractor позволяет обходить пользовательский интерфейс для проверки бизнес-логики, ускорять тесты и создавать гибкие сценарии с mock-данными, сохраняя при этом полное взаимодействие с Angular-инфраструктурой.