В этой главе:
• основы модульного тестирования с применением среды Jasmine;
• базовые средства, получаемые из библиотеки тестирования Angular;
• тестирование основных исполнителей Angular-приложения: сервисов, компонентов и маршрутизатора;
• запуск модульных тестов на браузерах с использованием средства для запуска тестов Karma;
• реализация модульного тестирования на примере онлайн-аукциона.
Чтобы убедиться в отсутствии ошибок в программном средстве, его нужно протестировать. Даже если в нем сегодня нет никаких ошибок, они могут появиться завтра, после внесения изменений в существующий код или введения нового кода. Пусть код в отдельно взятом модуле и не изменялся, но он может заработать неправильно в силу изменений в каком-нибудь другом модуле. Код вашего приложения должен регулярно подвергаться тестированию, и этот процесс нужно автоматизировать. Вам следует подготовить тестовые сценарии и приступить к их запуску в вашем разработочном цикле как можно раньше.
Существует два основных типа тестирования клиентской части программы веб-приложения.
• Модульное (блочное) тестирование, которое доказывает, что небольшие блоки кода (например, компоненты или функции) воспринимают ожидаемые входные данные и возвращают ожидаемый результат. Данный вид тестирования касается проверки изолированных частей кода, главным образом открытых интерфейсов. Именно это мы и будем рассматривать в настоящей главе.
• Сквозное тестирование, доказывающее работоспособность всего приложения в рамках ожиданий конечных пользователей и правильную организацию взаимодействия всех блоком друг с другом. Для сквозного тестирования приложений Angular можно воспользоваться библиотекой Protractor (см. /#/).
ПРИМЕЧАНИЕ
Нагрузочное тестирование, или стресс-тест, показывает, сколько пользователей могут одновременно работать с веб-приложением при сохранении им ожидаемого времени отклика. Средства нагрузочного тестирования в основном касаются проверки серверных сторон веб-приложений.
Модульное тестирование предназначено для проверки логики функционирования отдельных модулей кода, и, как правило, такие тесты запускаются намного чаще сквозных. Последние могут имитировать действия пользователей (например, нажатия кнопок) и проверять поведение вашего приложения. Запускать сценарии модульного тестирования в ходе сквозного нельзя.
Данная глава посвящена модульному тестированию Angular-приложений. Для реализации и запуска модульных тестов существует целый ряд специально созданных сред, и наш выбор пал на Jasmine. Фактически это не только наш выбор; на момент написания этих строк имеющаяся в Angular библиотека тестирования работала в области модульного тестирования только с Jasmine. Соответствующее описание приведено в разделе Jasmine Testing 101 документации по Angular ().
Сначала мы рассмотрим основы модульного тестирования с применением среды Jasmine, а ближе к концу главы вы создадите и запустите сценарии для модульного тестирования компонентов онлайн-аукциона. Мы предоставим краткий обзор среды Jasmine, чтобы поспособствовать быстрому переходу к созданию модульных тестов; все остальные подробности можно будет найти в документации по Jasmine (). Для запуска тестов будет использоваться специальное средство под названием Karma (), которое является независимой утилитой командной строки, способной запускать тесты, написанные в различных средах тестирования.
Среда Jasmine позволяет реализовать процесс разработки через реализацию поведения (behavior-driven development (BDD)), предполагающий, что тесты любого блока программного средства должны формулироваться в понятиях желаемого поведения блока. При использовании BDD для описания ваших замыслов относительно предназначения кода применяются обычные языковые конструкции. Спецификации модульных тестов пишутся в виде коротких предложений, например, ApplicationComponent is successfully instantiated (Экземпляр ApplicationComponent успешно создан) или StarsComponent emits the rating change event (StarsComponent выдает событие изменения рейтинга).
Поскольку назначения тестов воспринимаются довольно легко, они могут послужить в качестве документации вашей программы. Когда другим разработчикам понадобится ознакомиться с вашим кодом, для выяснения ваших намерений они могут приступить к чтению кода модульных тестов. Использование обычного языка для описания тестов дает еще одно преимущество: простоту понимания результатов тестирования, в чем можно убедиться, посмотрев на рис. 9.1.
Рис. 9.1. Запуск тестов с применением исполнителя тестов среды Jasmine
По терминологии Jasmine тест называется спецификацией (spec), а комбинация нескольких спецификаций — набором (suite). Тестовый набор определяется функцией describe(): именно здесь дается описание тому, что проверяется. Каждая тестовая спецификация программируется как функция it(), в которой определяется ожидаемое поведение анализируемого кода и порядок его тестирования. Рассмотрим пример:
describe('MyCalculator', () => {
it('should know how to multiply', () => {
// Сюда помещается код, тестирующий умножение
});
it('should not divide by zero', () => {
// Сюда помещается код, тестирующий деление на нуль
});
});
В средах тестирования используется такое понятие, как утверждение (assertion), являющееся способом опроса, во что вычисляется выражение — в true или в false. Если утверждение имеет значение false, то среда выдает ошибку. В Jasmine утверждения определяются с помощью функции expect(), за которой следуют сопоставления (matchers): toBe(), toEqual() и т.д. Это похоже на написание предложения. I expect 2+2 to equal 4 (Я ожидаю, что 2 + 2 равно 4) выглядит следующим образом:
expect(2 + 2).toEqual(4);
Сопоставления реализуют булевы сравнения фактических и ожидаемых значений.
При возвращении сопоставлением значения true спецификация считается пройденной. Если ожидается, что у результата теста нет конкретного значения, то нужно перед сопоставлением добавить ключевое слово not:
expect(2 + 2).not.toEqual(5);
Полный список сопоставлений можно найти на ресурсе GitHub, на странице Джейми Мейсона (Jamie Mason), которая называется Jasmine-Matchers: .
Мы дали нашим тестовым наборам такие же имена, как и у тестируемых файлов, добавив к ним суффикс .spec, что является стандартным приемом; например, в application.spec.ts содержится тестовый сценарий для application.ts. Следующий тестовый набор взят из файла application.spec.ts; он тестирует создание экземпляра ApplicationComponent:
import AppComponent from './app';
describe('AppComponent', () => {
it('is successfully instantiated', () => {
const app = new AppComponent();
expect(app instanceof AppComponent).toEqual(true);
});
});
В этом тестовом наборе содержится всего один тест. Если извлечь тексты из describe() и it() и объединить их, то получится предложение, разъясняющее, что именно здесь тестируется: ApplicationComponent is successfully instantiated (Экземпляр ApplicationComponent успешно создан).
ПРИМЕЧАНИЕ
Если другим разработчикам понадобится узнать, что тестируется вашей спецификацией, то они могут прочитать тексты в describe() и в it(). Каждый тест должен давать свое собственное описание и служить в качестве программной документации.
В предыдущем коде создается экземпляр AppComponent и ожидается, что выражение app instanceof AppComponent будет вычислено в true. Присутствие инструкции import может навести вас на мысль о нахождении сценария тестирования в том же самом каталоге, в котором помещается и AppComponent.
Где следует хранить файлы с тестами Среда Jasmine используется для модульного тестирования приложений на JavaScript, написанных в различных средах или на чистом JavaScript. Одним из подходов к хранению файлов с тестами является создание отдельного каталога test для хранения в нем исключительно файлов со сценариями тестов, чтобы они не смешивались с кодом приложения. В Angular-приложениях мы отдали предпочтение хранению каждого тестового сценария в том же самом каталоге, где находится тестируемый компонент или сервис. Это удобно по двум причинам. Все файлы, имеющие отношение к компоненту, хранятся в одном каталоге. Обычно каталог создается для хранения принадлежащих компоненту файлов с расширениями .ts, .html и .css; добавление файла с расширением .spec не станет захламлять содержимое каталога. Не нужно изменять конфигурацию загрузчика SystemJS, который уже знает, где находятся файлы приложения. Он будет загружать тесты из того же самого места. |
Если нужно, чтобы перед каждым тестом выполнялся какой-нибудь код (например, для подготовки тестовых зависимостей), то вы можете указать его в настроечных функциях beforeAll() и beforeEach(), которые будут запущены соответственно перед набором или перед каждой спецификацией. При необходимости выполнить какой-нибудь код сразу же после завершения набора или каждой спецификации следует воспользоваться демонтирующими функциями afterAll() и afterEach().
СОВЕТ
Если у спецификации несколько тестов it() и нужно, чтобы средство запуска тестов пропустило некоторые из них, то измените их название с it() на xit().
Теперь, когда стало понятно, как тестировать, остается спросить, что именно нужно проверять. В Angular-приложениях, написанных на TypeScript, можно тестировать функции, классы и компоненты.
• Тестирование функций — предположим, имеется функция, переводящая символы переданной строки в верхний регистр. Для нее можно написать сразу несколько тестов — для тех случаев, когда аргументом является null, пустая строка, неопределенное значение, слово с символами в нижнем регистре, слово с символами в верхнем регистре, слово с символами в смешанном регистре, число и т.д.
• Тестирование классов — если имеется класс, содержащий несколько методов (наподобие класса ProductService), то можно написать тестовый набор, включающий все тесты, необходимые для того, чтобы убедиться в правильном функционировании каждого из методов класса.
• Тестирование компонентов — можно проверять открытый API ваших сервисов или компонентов. Помимо тестирования их на корректность работы, вам будут показаны примеры кода, использующие общедоступные свойства или методы.
Получить Jasmine можно путем загрузки автономного дистрибутива этой среды, но вы установите ее с помощью npm, как делалось для всех других пакетов в данной книге. В хранилище npm имеется несколько связанных с Jasmine пакетов, но вам нужен только jasmine-core. Откройте в корневом каталоге своего проекта окно команд и запустите следующую команду:
npm install jasmine-core --save-dev
Чтобы компилятор TypeScript разбирался в типах среды Jasmine, запустите следующую команду для установки имеющегося у Jasmine файла определения типов:
npm i @types/jasmine --save-dev
После написания тестов вам понадобится приложение для их запуска. Jasmine поставляется с двумя пусковыми средствами, одно из которых предназначено для работы с командной строкой (см. npm-пакет jasmine), а другое основано на применении кода HTML. Начнем с пускового средства на основе HTML, но для запуска тестов из командной строки будет использоваться другая программа — Karma.
Хотя Jasmine поставляется с заранее сконфигурированным средством запуска на основе HTML в виде образцового приложения, для тестирования своего собственного кода вам нужно будет создать HTML-файл. В него должны быть включены следующие сценарные теги, загружающие Jasmine:
<link rel="stylesheet" href="node_modules/jasmine-core/lib/jasmine-core/
jasmine.css">
<script src="node_modules/jasmine-core/lib/jasmine-core/jasmine.js">
</script>
<script src="node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js">
</script>
<script src="node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>
Использование автономного дистрибутива Jasmine Если вам хочется поскорее увидеть запущенными тесты Jasmine, то загрузите zip-файл с автономной версией Jasmine с . Распакуйте его и откройте в своем браузере файл SpecRunner.html. Вы должны увидеть там следующее окно.
Тестирование образцового приложения, поставляемого вместе с Jasmine |
Нужно также добавить все востребованные Angular зависимости, как делалось в каждом файле index.html во всех примерах кода в данной книге, плюс библиотеку тестирования Angular. Продолжим использовать загрузчик SystemJS, но на этот раз загрузим код модульных тестов (файлы с расширением .spec files), которые загрузят код приложения через операторы import.
В этой главе мы опишем процесс создания модульных тестов. Запускать их будем вручную, сначала с помощью средств запуска на основе HTML. Затем покажем порядок использования программы Karma, которая может запускать тесты командной строки, сообщающие в различных браузерах о возможных ошибках. В главе 10 она будет встроена в процесс сборки приложения, чтобы модульные тесты запускались автоматически как часть сборки.
Angular поставляется с библиотекой тестирования, включающей оболочки для имеющихся в Jasmine функций describe(), it() и xit(), а также добавляющей такие функции, как beforeEach(), async(), fakeAsync() и др.
Поскольку в ходе выполнения тестов приложение не настраивается и не загружается, Angular предоставляет вспомогательный класс TestBed, который позволяет объявлять модули, компоненты, поставщики и т.д. Этот класс включает такие функции, как configureTestingModule(), createComponent(), inject() и др. Например, синтаксис для настройки модуля тестирования похож на настройку аннотации @NgModule:
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ ReactiveFormsModule, RouterTestingModule,
RouterTestingModule.withRoutes(routes)],
declarations: [AppComponent, HomeComponent, WeatherComponent],
providers: [{provide: WeatherService, useValue: {} }
]
})
});
Функция beforeEach() используется в тестовых наборах в фазе настройки. Она позволяет указать требуемые модули, компоненты и поставщики, которые могут понадобиться для каждого теста.
Функция inject() создает средство внедрения и вводит конкретные объекты в тесты, в соответствии с поставщиками приложения, настроенными для Angular DI:
inject([Router, Location], (router: Router, location: Location) => {
// Выполнение каких-либо действий
})
Функция async() запускается в Zone и может использоваться c асинхронными сервисами. Эта функция не заканчивает тест до тех пор, пока не завершатся все его асинхронные операции или не истечет указанное время ожидания:
it(' does something', async(inject([AClass], object => {
myPromise.then(() => { expect(true).toEqual(true); });
}), 3000));
Функция fakeAsync() позволяет ускорить тестирование асинхронных сервисов путем имитации течения времени:
В библиотеке тестирования Angular имеется интерфейс NgMatchers, который включает следующие сопоставления:
• toBePromise() — ожидает, что значением будет Promise-объект;
• toBeAnInstanceOf() — ожидает, что значением будет экземпляр класса;
• toHaveText() — ожидает, что у элемента имеется в точности заданный текст;
• toHaveCssClass() — ожидает, что у элемента имеется заданный класс CSS;
• toHaveCssStyle() — ожидает, что у элемента имеются заданные стили CSS;
• toImplement() — ожидает, что в классе реализован интерфейс заданного класса;
• toContainError() — ожидает, что в исключении содержится заданный текст ошибки;
• toThrowErrorWith() — ожидает, что функция при выполнении выдаст ошибку с заданным текстом ошибки.
Документация по имеющемуся в Angular API тестирования для TypeScript может быть найдена на . Порядок тестирования сервисов, маршрутизаторов, источников событий и компонентов будет показан в этой главе чуть позже, но сначала рассмотрим некоторые основы.
Обычно Angular-сервисы внедряются в компоненты; для настройки средств внедрения нужно определить поставщики для блока it(). Angular предлагает настроечный метод beforeEach(), запускаемый перед каждым запуском it(). Чтобы протестировать в сервисе синхронные функции, можно воспользоваться методом inject() и внедрить этот сервис в it().
Реальным сервисам для завершения работы может понадобиться некоторое время, и это способно замедлить ваши тесты. Есть два способа их ускорения.
• Создание класса, реализующего сервис-имитатор, который сможет быстро возвратить жестко заданные данные за счет расширения класса настоящего сервиса. Например, можно создать сервис-имитатор для WeatherService, тут же возвращающий данные без выполнения каких-либо запросов к удаленному серверу на возвращение фактических погодных данных:
class MockWeatherService implement WeatherService {
getWeather() {
return Observable.empty();
}
}
• Применение функции fakeAsync(), которая автоматически идентифицирует асинхронные вызовы и заменяет паузы, функции обратного вызова и Promise-объекты функциями, выполняемыми без всяких промедлений. Функция tick() позволяет ускорить время, чтобы не нужно было ожидать истечения срока паузы. Примеры использования fakeAsync() будут показаны в данной главе чуть позже.
Для тестирования маршрутизатора сценарии спецификаторов могут вызывать такие методы маршрутизации, как navigate() и navigateByUrl(). Первый метод получает массив сконфигурированных маршрутов (команд), выстраивающих маршрут в виде аргумента, а второй метод получает строку, представляющую собой сегмент URL, по которому нужно выполнить переход.
В процессе использования метода navigate() указываются параметры сконфигурированного пути и маршрута, если таковые имеются. При правильной конфигурации маршрутизатора он должен обновить URL в адресной строке браузера.
В следующем фрагменте программного кода показано, как программным способом перейти к маршруту товара product, передать в качестве параметра маршрута 0 и убедиться в том, что после перехода веб-адрес (представленный объектом типа Location) имеет сегмент /product/0:
it('should be able to navigate to product details using commands API',
fakeAsync(inject([Router, Location], (router: Router, location:
Location) => {
TestBed.createComponent(AppComponent);
router.navigate(['/products', 0]);
tick();
expect(location.path()).toBe('/product/0');
})
));
Когда маршрутизатору предоставляется массив значений, им вызывается API команд. Чтобы заработал предыдущий фрагмент кода, маршрутизатор с параметром /products/:productId должен быть сконфигурирован в соответствии с объяснениями, которые даны в главе 3.
Функция it() вызывает функцию обратного вызова, предоставляемую в качестве второго аргумента. Метод fakeAsync() инкапсулирует функцию, предоставленную в качестве аргумента (в предыдущем примере это inject()), и выполняет ее в Zone. Функция tick() позволяет вручную ускорить время и переносить на более ранний срок задачи в очереди микрозадач браузерного цикла событий. Иными словами, можно имитировать время, занимаемое решением асинхронных задач, и выполнять асинхронный код в синхронном режиме, что упрощает и ускоряет проведение модульных тестов.
При использовании метода TestBed.createComponent() (рассматриваемого в следующем разделе) создается экземпляр компонента. В момент вызова метода маршрутизатора navigate() с помощью функции tick() ускоряются асинхронные задачи, выполняющие навигацию, и проверяется соответствие текущего местоположения ожидаемому.
Функция navigateByUrl() получает конкретный сегмент URL и должна правильно выстроить Location.path, представляющий клиентскую часть в адресной строке браузера. Вот то, что будет тестироваться:
router.navigateByUrl('/products');
...
expect(location.path()).toBe('/products');
Порядок использования функции navigateByUrl() будет показан в разделе 9.3.
При тестировании маршрутизатора можно воспользоваться SpyLocation — это имитатор поставщика Location. Он позволяет проводить тесты имитации событий местоположений. Например, можно подготовить конкретный веб-адрес и имитировать изменение хеш-части, работу кнопок браузера Back (Назад) и Forward (Вперед) и многое другое.
Компоненты представляют собой классы с шаблонами. Если в классе содержатся методы, реализующие логику приложения, то их можно протестировать, как и любые другие функции; но чаще всего будут тестироваться шаблоны. В частности, вы можете проявить интерес к тестированию правильной работы привязок и к проверке того, какие данные отображают шаблоны.
Angular предлагает метод TestBed.createComponent(), возвращающий объект ComponentFixture, который будет использоваться для работы с компонентами при их создании. Это приспособление (fixture) дает доступ как к компоненту, так и к собственным экземплярам HTML-элементов, позволяя присваивать значения свойствам компонентов, а также находить конкретные HTML-элементы в шаблоне компонента.
Кроме того, можно активизировать циклическое определение изменений путем вызова в отношении объекта-приспособления метода detectChanges(). После того как определение изменения обновило пользовательский интерфейс (UI), можно запустить функцию expect(), чтобы проверить выведенные значения. Эти действия показаны в следующем фрагменте кода с помощью компонента ProductComponent, имеющего свойство product, при условии его привязки к элементу шаблона <h4>:
let fixture = TestBed.createComponent(ProductDetailComponent);
let element = fixture.nativeElement;
let component = fixture.componentInstance;
component.product = {title: 'iPhone 7', price: 700};
fixture.detectChanges();
expect(element.querySelector('h4').innerHTML).toBe('iPhone 7');
А теперь создадим типовое приложение, в котором реализуем модульное тестирование компонента, маршрутизатора и сервиса.
Попробуем протестировать компоненты и сервисы Angular, используя приложение, имеющее главную страницу с двумя ссылками: Home и Weather. Для перехода на страницу погоды Weather будет применяться маршрутизатор, являющийся реструктурированной версией приложения для прогноза погоды, созданного в главе 5 (observable-events-).
В главе 5 большой фрагмент кода был помещен в конструктор AppComponent, что усложняет тестирование, поскольку не дает возможности вызвать код конструктора после создания объекта. Теперь WeatherComponent получит внедрение WeatherService, и этот сервис станет использовать удаленный сервер из главы 5 для получения информации о погоде. На рис. 9.2 показано, как выглядит окно при запуске данного приложения после перехода по маршруту Weather и набора в поле ввода строки New York.
Структура этого проекта показана на рис. 9.3 (см. каталог test_weather). Обратите внимание на файлы с расширением .spec, в которых содержится код для модульного тестирования компонентов и сервиса погоды.
|
|
|
|
|
|
Рис. 9.2. Проверка погодного компонента в проекте test_samples |
| Рис. 9.3. Структура проекта test_samples |
Для запуска этих тестов создайте следующий файл test.html (листинг 9.1), загружающий все файлы spec.ts, обведенные на рис. 9.3.
Листинг 9.1. Содержимое файла test.html
Чтобы воспользоваться средством запуска тестов на основе HTML, нужно добавить модули тестирования Angular в настройки вашего загрузчика модулей SystemJS. Фрагмент файла systemjs.config.js, поставляемого вместе с проектом, выглядит следующим образом (листинг 9.2).
Листинг 9.2. Фрагмент файла systemjs.config.js
'@angular/common/testing' : 'ng:common/bundles/
common-testing.umd.js',
'@angular/compiler/testing' : 'ng:compiler/bundles/
compiler-testing.umd.js',
'@angular/core/testing' : 'ng:core/bundles/
core-testing.umd.js',
'@angular/router/testing' : 'ng:router/bundles/
router-testing.umd.js',
'@angular/' : 'ng:/
',
'@angular/platform-browser/testing' : 'ng:platform-browser/
bundles/platform-browser-testing.umd.js',
'@angular/platform-browser-dynamic/testing':
'ng:platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js',
},
paths: {
'ng:': 'node_modules/@angular/'
},
Маршрутизатор для этого приложения конфигурируется в файле app.routing.ts (листинг 9.3).
Листинг 9.3. Содержимое файла app.routing.ts
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './components/home';
import { WeatherComponent } from './components/weather';
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'weather', component: WeatherComponent }
];
export const routing = RouterModule.forRoot(routes);
Хотя конфигурировать маршруты можно либо в модуле вашего приложения, либо в отдельном файле, последний способ предпочтительнее. Он позволяет использовать конфигурацию маршрутов многократно, чтобы запускать не только приложения, но и сценарии тестов. Сценарий в файле app.module.ts приложения погоды задействует в объявлении @NgModule константу routes (листинг 9.4).
Листинг 9.4. Содержимое файла app.module.ts
@NgModule({
imports: [BrowserModule, HttpModule, ReactiveFormsModule, routing],
declarations: [AppComponent, HomeComponent, WeatherComponent],
bootstrap: [AppComponent],
providers: [
{ provide: LocationStrategy, useClass: HashLocationStrategy },
{ provide: WEATHER_URL_BASE, useValue: '/
data/2.5/weather?q=' },
{ provide: WEATHER_URL_SUFFIX, useValue:
'&units=imperial&appid=ca3f6d6ca3973a518834983d0b318f73' },
WeatherService
]
})
Сценарий теста для маршрутов находится в файле app.spec.ts, и в нем повторно используется та же самая константа routes (листинг 9.5).
Листинг 9.5. Содержимое файла app.spec.ts
Обратите внимание на импортирование модуля ReactiveFormsModule, поскольку компонент WeatherComponent использует API Forms.
ПРИМЕЧАНИЕ
Не проводите модульное тестирование в своем приложении кода сторонних поставщиков. В листинге 9.5 в качестве поставщика WeatherService используется пустой объект, который в реальном приложении обращается к удаленному сервису погоды. А что получится, если удаленный сервис в момент запуска тестовой спецификации даст сбой? Модульные тесты позволяют убедиться в работоспособности ваших сценариев, а не в работоспособности сторонних программных средств. Именно поэтому применяется не настоящий сервис WebService, а пустой объект.
При тестировании навигации вашего приложения на клиентской стороне будет использован класс Router и его методы navigate() и navigateByUrl().
В листинге 9.5 для тестирования правильности обновления адресной строки приложения программными средствами навигации показываются оба метода, и navigate(), и navigateByUrl(). Но, поскольку в ходе тестирования приложение не запускается, адресной строки браузера не существует, следовательно, она должна быть имитирована. Именно поэтому вместо модуля RouterModule используется модуль RouterTestingModule, который знает, как проверить ожидаемое содержимое адресной строки, задействуя класс Location.
Теперь посмотрим на тестирование внедрения сервисов. Собственно говоря, вы уже внедрили сервисы при тестировании маршрутов:
fakeAsync(inject([Router, Location],...))
Но в следующем подразделе мы покажем другой способ инициализации требуемых сервисов: будет получен объект типа Injector и вызван его метод get().
Обмен данными с сервисом погоды инкапсулируется в классе WeatherService (листинг 9.6).
Листинг 9.6. Содержимое файла weather.service.ts
import {Inject, Injectable, OpaqueToken} from '@angular/core';
import {Http, Response} from '@angular/';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/map';
export const WEATHER_URL_BASE = new OpaqueToken('WeatherUrlBase');
export const WEATHER_URL_SUFFIX = new OpaqueToken('WeatherUrlSuffix');
export interface WeatherResult {
place: string;
temperature: number;
humidity: number;
}
@Injectable()
export class WeatherService {
constructor(
private : Http,
@Inject(WEATHER_URL_BASE) private urlBase: string,
@Inject(WEATHER_URL_SUFFIX) private urlSuffix: string) {
}
getWeather(city: string): Observable<WeatherResult> {
return this.
.get(this.urlBase + city + this.urlSuffix)
.map((response: Response) => response.json())
.filter(this._hasResult)
.map(this._parseData);
}
private _hasResult(data): boolean {
return data['cod'] !== '404' && data.main;
}
private _parseData(data): WeatherResult {
let [first,] = data.list;
return {
place: data.name || 'unknown',
temperature: data.main.temp,
humidity: data.main.humidity
};
}
}
Обратите внимание на использование типа OpaqueToken, упомянутого в главе 4. Он задействован дважды для внедрения в urlBase и urlSuffix значения, предоставляемого в декораторе @NgModule. Применение внедрения зависимостей для urlBase и urlSuffix упрощает замену при необходимости реального сервиса погоды имитатором.
Показанный в листинге 9.6 метод getWeather() формирует URL для HTTP-запроса get() путем объединения urlBase, city и urlSuffix. Результат обрабатывается методами map(), filter() и еще раз методом map(), поэтому наблюдаемый объект будет выдавать объекты типа WeatherResult.
ПРИМЕЧАНИЕ
Методы _hasResult() и _parseData() не проверяются, поскольку закрытые методы не могут подвергаться модульному тестированию. Если принять решение по их тестированию, то следует изменить их уровень доступности на открытый.
Для тестирования сервиса WeatherService будет использоваться класс MockBackend, являющийся одной из Angular-реализаций Http-объекта. Этот класс не выполняет никаких HTTP-запросов, но перехватывает их и позволяет создавать и возвращать жестко заданные данные в формате ожидаемого результата.
Перед каждым тестом будет получена ссылка на объект типа Injector, который станет предоставлять новые экземпляры MockBackend и WeatherService. Код тестирования последнего находится в файле weather.service.spec.ts (листинг 9.7).
Листинг 9.7. Содержимое файла weather.service.spec.ts
Из тестирования внедрения сервисов можно выделить следующие ключевые моменты:
• подготовку поставщиков;
• создание имитаторов при использовании сервисов, выполняющих запросы в адрес внешних серверов.
Способы тестирования навигации и сервисов мы показали, теперь посмотрим, как можно протестировать компонент Angular.
Сервис WeatherService внедряется в компонент WeatherComponent (weather.ts) (листинг 9.8) с помощью конструктора, где происходит подписка на наблюдаемые сообщения, поступающие от WeatherService. Когда пользователь начинает вводить название города в пользовательском интерфейсе, вызывается метод getWeather() и возвращенные данные о погоде отображаются в шаблоне через привязку.
Листинг 9.8. Содержимое файла weather.ts
import {Component} from '@angular/core';
import {FormControl} from '@angular/forms';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/switchMap';
import {WeatherService, WeatherResult} from '../services/weather.service';
@Component({
selector: 'my-weather',
template: `
<h2>Weather</h2>
<input type="text" placeholder="Enter city" [formControl]="searchInput">
<h3>Current weather in {{weather?.place}}:</h3>
<ul>
<li>Temperature: {{weather?.temperature}}F</li>
<li>Humidity: {{weather?.humidity}}%</li>
</ul>
`,
})
export class WeatherComponent {
searchInput: FormControl;
weather: WeatherResult;
constructor(weatherService: WeatherService) {
this.searchInput = new FormControl('');
this.searchInput.valueChanges
.debounceTime(300)
.switchMap((place: string) => weatherService.getWeather(place))
.subscribe(
(wthr: WeatherResult) => this.weather = wthr,
error => console.error(error),
() => console.log('Weather is retrieved'));
}
}
Желательно написать тест для проверки того, что при получении значений свойством weather шаблон соответственно обновляется с помощью привязки. Желательно также проверить, что при изменении значения объекта searchInput наблюдаемый объект выдает данные через свое свойство valueChanges.
Elvis-оператор Шаблон компонента WeatherComponent включает выражения со знаками вопроса, например, weather?.place. В этом контексте знак вопроса называется Elvis-оператором. Свойство weather заполняется в асинхронном режиме, и если на момент вычисления выражения это null, то выражение weather.place выдаст ошибку. Для подавления null-разыменования используется Elvis-оператор, чтобы создать короткое замыкание на дополнительное вычисление, если значением weather является null. Elvis-оператор предлагает явную запись, показывающую, какие значения могут быть вычислены в null. |
Тестовый набор будет включать один тест для проверки ожидаемой работоспособности привязки данных. Код TestBed.createComponent(WeatherComponent); создаст приспособление ComponentFixture, содержащее ссылки на WeatherComponent, а также на DOM-объект, представляющий этот компонент. В листинге 9.8 свойство weather используется для привязок; оно инициализируется литералом объекта, который содержит жестко заданные значения для места, влажности и температуры (place, humidity и temperature) .
После этого будет принудительно обнаружено изменение путем вызова метода detectChanges(), принадлежащего экземпляру класса ComponentFixture. Появление значений от weather ожидается в шаблоне компонента в одном теге <h3> и двух тегах <li>. Код для данного теста находится в файле weather.spec.ts (листинг 9.9).
Листинг 9.9. Содержимое файла weather.spec.ts
СОВЕТ
В листинге 9.9 для имитации WeatherService используется пустой объект, поскольку в отношении него вызов каких-либо методов не планируется. Определить сервис-имитатор можно в виде класса MockWeatherService, реализующего WeatherService и предоставляющего реализацию реальных методов, но они будут возвращать жестко заданные значения. При определении сервиса-имитатора в приложениях, предназначенных для реальной работы, целесообразно создавать классы, реализующие интерфейсы настоящих сервисов.
ПРИМЕЧАНИЕ
В главе 7 были рассмотрены два подхода к созданию форм в Angular. Хотя шаблон-ориентированный подход требует меньшего объема кода, применение реактивных форм делает их более пригодными для тестирования, не требуя для этого DOM-объект.
Запуск тестов в средстве запуска на основе HTML. Запустим тестовый набор для погодного приложения в средстве запуска на основе HTML. Для этого нужно просто запустить живой сервер и ввести в адресную строку браузера URL . Все тесты должны быть пройдены, и окно браузера должно выглядеть так же, как на рис. 9.4.
При написании тестов хочется увидеть, как они могут быть не пройдены. Устроим так, чтобы один тест не был пройден, для наблюдения того, как будет сообщено об этом. Измените температуру в строке, где инициализируется свойство weather, на 58 градусов:
component.weather = {place: 'New York', humidity: 44, temperature: 58};
А тест по-прежнему будет ожидать, что пользовательский интерфейс выведет на экран температуру 57 градусов:
expect(element.querySelector('li:nth-of-type(1)').innerHTML)
.toBe('Temperature: 57F');
Рис. 9.4. Тесты пройдены
Вывод результатов тестирования, показанный на рис. 9.5, сообщает о сбое одного из пяти тестов.
Рис. 9.5. Тесты не пройдены
Запуск тестов в браузере вручную — не самый лучший способ модульного тестирования кода. Нужен процесс тестирования, запускаемый как сценарий из командной строки, чтобы его можно было встроить в автоматизированный процесс сборки. В Jasmine имеется средство запуска, которое может использоваться из приглашения командной строки. Но предпочтительнее задействовать независимое средство для запуска тестов под названием Karma, способное работать с множеством различных сред модульного тестирования. Порядок применения этой программы будет рассмотрен в следующем разделе.
Karma () — средство для запуска тестов, изначально созданное командой разработчиков, но использовавшееся для тестирования кода JavaScript, написанного с применением или без применения какой-либо среды разработки. Это средство создано с помощью Node.js, и, хотя оно не запускается в браузере, у него имеется возможность запускать тесты, чтобы проверить, будет ли ваше приложение работать в нескольких браузерах (вы будете запускать тесты для Chrome и Firefox).
Применительно к приложению Weather вы установите Karma и дополнительные модули для Jasmine, Chrome и Firefox и сохраните их в разделе devDependencies файла package.json:
npm install karma karma-jasmine karma-chrome-launcher karma-firefox-
launcher --save-dev
Для запуска Karma настройте в npm команду test вашего проекта следующим образом:
"scripts": {
"test": "karma start karma.conf.js"
}
ПРИМЕЧАНИЕ
Исполнительный файл karma имеет двоичную природу и находится в каталоге node_modules/.bin.
Кроме того, вы создадите небольшой конфигурационный файл karma.conf.js (листинг 9.10), позволяющий Karma узнать о проекте. Этот файл будет находиться в корневом каталоге проекта и включать пути к файлам Angular, а также настройки конфигурации для средства запуска тестов Karma.
Листинг 9.10. Содержимое файла karma.conf.js
В большей части листинга 9.10 перечисляются пути нахождения требуемых файлов. Karma создает временную HTML-страницу, включающую файлы, перечисленные с included: true. Файлы, перечисленные с included: false, будут динамически подгружаться в ходе выполнения. Все Angular-файлы, включая тестируемые, загружаются в динамическом режиме с использованием SystemJS.
Вам следует добавить к проекту еще один файл: karma-test-runner.js (листинг 9.11). Это сценарий, который фактически запускает тесты.
Листинг 9.11. Содержимое файла karma-test-runner.js
Теперь все готово к запуску ваших тестов с использованием в командной строке команды npm test. В ходе своей работы Karma будет открывать и закрывать каждый указанный в конфигурации браузер и выводить на экран результаты тестирования, как показано на рис. 9.6.
Рис. 9.6. Тестирование погодного приложения с использованием Karma
Разработчики склонны применять самые последние версии браузеров, имеющих наиболее совершенные инструменты; именно таким является Google Chrome. Нам попадались реальные проекты, в ходе реализации которых разработчиком демонстрировалась великолепная работа приложения в браузере Chrome, а затем пользователи жаловались на ошибки проекта, проявлявшиеся в Safari. Нужно обеспечить использование в процессе разработки Karma и тестировать приложение во всех браузерах. Перед передачей приложения команде по проверке качества или перед его демонстрацией своему руководителю следует убедиться, что средство для запуска тестов Karma не сообщило об ошибках во всех востребованных браузерах.
Мы закончили обзор создания и запуска модульных тестов, теперь приступим к реализации тестов для онлайн-аукциона.
Цель этого практикума — демонстрация модульного тестирования отдельно взятых модулей приложения онлайн-аукциона. В частности, модульные тесты будут добавлены для ApplicationComponent, StarsComponent и ProductService. Тесты запустятся с использованием имеющегося в Jasmine средства запуска тестов на основе HTML, а затем с применением Karma.
ПРИМЕЧАНИЕ
В качестве отправной точки мы собираемся воспользоваться приложением-аукционом из главы 8, поэтому его нужно скопировать в отдельный каталог и следовать инструкциям, рассматриваемым в данном разделе. Если вы больше склоняетесь к просмотру кода, а не к его набору, то задействуйте код, предоставляемый с главой 9, и запустите тесты.
Установите Jasmine, Karma, файлы определения типов для Jasmine и все зависимости Angular, запустив следующие команды:
npm install jasmine-core karma karma-jasmine karma-chrome-launcher karma-
firefox-launcher --save-dev
npm install @types/jasmine --save-dev
npm install
В клиентском каталоге создайте новый файл auction-unit-tests.html для загрузки тестов Jasmine (листинг 9.12).
Листинг 9.12. Содержимое файла auction-unit-tests.html
<!DOCTYPE html>
<html>
<head>
<title>[TEST] Online Auction</title>
<!-- браузерный компилятор TypeScript -->
<script src="node_modules/typescript/lib/typescript.js"></script>
<!-- Полифиллы -->
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<!-- Jasmine -->
<link rel="stylesheet" href="node_modules/jasmine-core/lib/jasmine-core/
jasmine.css">
<script src="node_modules/jasmine-core/lib/jasmine-core/jasmine.js">
</script>
<script src="node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js">
</script>
<script src="node_modules/jasmine-core/lib/jasmine-core/boot.js"></script>
<!-- Zone.js -->
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/zone.js/dist/proxy.js"></script>
<script src="node_modules/zone.js/dist/sync-test.js"></script>
<script src="node_modules/zone.js/dist/jasmine-patch.js"></script>
<script src="node_modules/zone.js/dist/async-test.js"></script>
<script src="node_modules/zone.js/dist/fake-async-test.js"></script>
<script src="node_modules/zone.js/dist/long-stack-trace-zone.js"></script>
<!-- SystemJS -->
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
</head>
<body>
<script>
var SPEC_MODULES = [
'app/components/application/application.spec',
'app/components/stars/stars.spec',
'app/services/product-service.spec'
];
Promise.all([
System.import('@angular/core/testing'),
System.import('@angular/platform-browser-dynamic/testing')
])
.then(function (modules) {
var testing = modules[0];
var browser = modules[1];
testing.TestBed.initTestEnvironment(
browser.BrowserDynamicTestingModule,
browser.platformBrowserDynamicTesting());
// загрузка всех файлов спецификаций
return Promise.all(SPEC_MODULES.map(function (module) {
return System.import(module);
}));
})
.then(window.onload)
.catch(console.error.bind(console));
</script>
</body>
</html>
Содержимое этого файла похоже на содержимое файла test.html из погодного приложения. Единственное отличие заключается в том, что вы загружаете здесь другие файлы спецификаций: application.spec, stars.spec и product-service.spec.
Чтобы протестировать успешность создания экземпляра компонента ApplicationComponent, создайте в каталоге client/app/components/application файл application.spec.ts (листинг 9.13). Это, конечно, не самый полезный тест, но он может послужить иллюстрацией того, успешно ли создан экземпляр класса TypeScript (даже не связанный с Angular).
Листинг 9.13. Содержимое файла application.spec.ts
import ApplicationComponent from './application';
describe('ApplicationComponent', () => {
it('is successfully instantiated', () => {
const app = new ApplicationComponent();
expect(app instanceof ApplicationComponent).toEqual(true);
});
});
Чтобы протестировать ProductService, создайте в каталоге app/services файл product-service.spec.ts (листинг 9.14). В этой спецификации будет тестироваться HTTP-сервис и, хотя функция it() совсем небольшая, перед запуском теста предстоит множество подготовительных действий.
Листинг 9.14. Содержимое файла product-service.spec.ts
Сначала создается литерал объекта, mockProduct = {id: 1}, используемый для имитации данных, которые могут поступить с сервера в качестве HTTP-ответа. Нужно, чтобы mockBackend выполнил имитацию и осуществил возврат объекта с жестко заданными значениями для каждого HTTP-запроса. Вы могли бы создать экземпляр Product и с более богатыми свойствами, но для этого простого теста достаточно наличия одного идентификатора.
Для последнего теста мы выбрали StarsComponent, поскольку в нем демонстрируется, как можно протестировать свойства компонента и средство выдачи события. Компонент StarsComponent загружает свой HTML из файла, для которого в процессе тестирования требуется специальная обработка. Angular загружает файлы, указанные в templateUrl в асинхронном режиме, и выполняет их компиляцию к нужному моменту. Вам потребуется сделать то же самое в спецификации теста путем вызова метода TestBed.compileComponents(). Этот шаг необходим для любого компонента, использующего свойство templateUrl. Создайте в каталоге client/app/components/stars файл stars.spec.ts со следующим содержимым (листинг 9.15).
Листинг 9.15. Файл stars.spec.ts
Класс TestBed создает новый экземпляр компонента StarsComponent (здесь внедренный экземпляр не используется) и предоставляет приспособление со ссылками на компонент и на исходный элемент. Для проверки того, что все звезды пустые, входному свойству rating экземпляра компонента присваивается нуль. Фактически свойство rating является в StarsComponent сеттером, модифицирующим как rating, так и массив stars:
set rating(value: number) {
this._rating = value || 0;
this.stars = Array(this.maxStars).fill(true, 0, this.rating);
}
Затем запускается цикл обнаружения изменений, заставляющий цикл *ngFor заново выполнять вывод изображений в виде звезд в шаблоне StarsComponent:
<p>
<span *ngFor="let star of stars; let i = index"
class="starrating glyphicon glyphicon-star"
[class.glyphicon-star-empty]="!star"
(click)="fillStarsWithColor(i)">
</span>
<span *ngIf="rating">{{rating | number:'.0-2'}} stars</span>
</p>
Кодом CSS для заполненных звезд является starrating glyphicon glyphicon-star. У пустых звезд имеется дополнительный класс CSS, glyphicon-star-empty. Тест на пустоту всех звезд — 'all stars are empty' — использует селектор glyphicon-star-empty и ожидает, что имеется именно пять исходных элементов с таким классом.
Тест на заполненность всех звезд — 'all stars are filled' — присваивает рейтинг 5. В нем применяется CSS-селектор .glyphicon-star:not(.glyphicon-star-empty), в котором оператор not задействован для подтверждения того, что звезды не пусты.
Тест на выдачу события изменения рейтинга, когда readonly имеет значение false, — 'emits rating change event when readonly is false' — использует внедренный компонент. В нем выполняется подписка на событие ratingChange в ожидании, что значением рейтинга будет 3. Когда пользователь хочет изменить рейтинг, он нажимает третью звезду (оставляя отзыв), тем самым в отношении компонента вызывается метод заполнения звезд цветом — fillStarsWithColor, с передачей 3 в качестве аргумента index:
fillStarsWithColor(index) {
if (!this.readonly) {
this.rating = index + 1; // для предотвращения нулевого рейтинга
this.ratingChange.emit(this.rating);
}
}
Поскольку никакой пользователь во время модульного тестирования кнопку мыши не нажимает, этот метод вызывается программным путем:
component.readonly = false;
component.fillStarsWithColor(2);
Если нужно посмотреть, как этот тест не будет пройден, то измените аргумент fillStarsWithColor() на любое число, отличное от двух.
Порядок операций в тестировании событий В коде теста 'emits rating change event when readonly is false' то обстоятельство, что предыдущие две строки помещены в конце теста, после вызова subscribe(), может вызвать удивление. Выполнение подписки на Observable имеет ленивый характер, и следующий элемент будет получен только после вызова метода fillStarsWithColor(2), что приведет к выдаче события. Если переместить вызов метода subscribe() вниз, то событие будет выдано еще до создания подписчика и тест не будет пройден по истечении времени ожидания, поскольку метод done() никогда не будет вызван. |
Чтобы запустить тесты, сначала нужно провести повторную компиляцию серверного кода, запустив команду npm run tsc. Затем следует запустить приложение-аукцион, введя в консоли команду npm start. Тем самым на порте 8000 будет запущен Node-сервер. После ввода в браузер адреса тесты должны запуститься и произвести вывод, показанный на рис. 9.7.
Рис. 9.7. Тестирование онлайн-аукциона с использованием средства запуска тестов на основе HTML
Для запуска этих же тестов с использованием Karma скопируйте файлы karma.conf и karma-test-runner из каталога auction главы 9 в корневой каталог вашего проекта. (Данные файлы рассматривались в разделе 9.4.) Запустите команду npm test и увидите вывод, показанный на рис. 9.8.
Рис. 9.8. Тестирование аукциона с использованием Karma
Важность модульного тестирования Angular-приложений трудно переоценить. Модульные тесты позволяют убедиться в том, что каждый компонент или сервис приложения работают в точном соответствии с вашими ожиданиями. В данной главе был показан порядок создания модульных тестов с использованием среды Jasmine, а также продемонстрированы приемы их запуска с применением либо Jasmine, либо Karma.
Вот основные выводы этой главы.
• Хорошими кандидатами на создание тестового набора являются компонент или сервис.
• Хотя все файлы тестов можно хранить отдельно от приложения, удобнее всего будет хранить их рядом с тестируемым компонентом.
• Модульные тесты выполняются очень быстро, и ими должна быть протестирована основная часть логики функционирования приложения.
• При создании тестов создавайте условия, при которых код их не проходит, чтобы убедиться, что в сообщениях о сбое будет нетрудно разобраться.
• Если будет принято решение о реализации сквозного тестирования, то в ходе данного тестирования повторно запускать модульные тесты не нужно.
• Запуск модульных тестов должен стать частью вашего процесса автоматизированной сборки. Как это делается, будет показано в главе 10.