Это второй выпуск серии по тестированию в Angular с использованием Jasmine. В первой части руководства мы написали базовые модульные тесты для класса Pastebin и компонента Pastebin. Испытания, которые вначале не прошли, были позже сделаны зелеными
обзор
Вот обзор того, над чем мы будем работать во второй части урока.
В этом уроке мы будем:
- создание новых компонентов и написание большего количества юнит-тестов
- написание тестов для пользовательского интерфейса компонента
- написание юнит-тестов для сервиса Pastebin
- тестирование компонента с входами и выходами
- тестирование компонента с маршрутами
Давайте начнем!
Добавление пасты (продолжение)
Мы были на полпути в процессе написания модульных тестов для компонента AddPaste. Вот где мы остановились в первой части серии.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
it(‘should display the `create Paste` button’, () => {
//There should a create button in view
expect(element.innerText).toContain(«create Paste»);
});
it(‘should not display the modal unless the button is clicked’, () => {
//source-model is an id for the modal.
expect(element.innerHTML).not.toContain(«source-modal»);
})
it(‘should display the modal when `create Paste` is clicked’, () => {
let createPasteButton = fixture.debugElement.query(By.css(«button»));
//triggerEventHandler simulates a click event on the button object
createPasteButton.triggerEventHandler(‘click’,null);
fixture.detectChanges();
expect(element.innerHTML).toContain(«source-modal»);
})
})
|
Как упоминалось ранее, мы не будем писать строгие тесты пользовательского интерфейса. Вместо этого мы напишем некоторые базовые тесты для пользовательского интерфейса и поищем способы проверить логику компонента.
Действие щелчка запускается с помощью DebugElement.triggerEventHandler()
, который является частью утилит тестирования Angular.
Компонент AddPaste в основном предназначен для создания новых паст; следовательно, шаблон компонента должен иметь кнопку для создания новой вставки. При нажатии на кнопку должно появиться «модальное окно» с идентификатором «source-modal», которое в противном случае должно оставаться скрытым. Модальное окно будет разработано с использованием Bootstrap; поэтому вы можете найти много CSS-классов внутри шаблона.
Шаблон для компонента add-paste должен выглядеть примерно так:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
<!— add-paste.component.html —>
<div class=»add-paste»>
<button> create Paste </button>
<div id=»source-modal» class=»modal fade in»>
<div class=»modal-dialog» >
<div class=»modal-content»>
<div class=»modal-header»></div>
<div class=»modal-body»></div>
<div class=»modal-footer»></div>
</div>
</div>
</div>
</div>
|
Второй и третий тесты не дают никакой информации о деталях реализации компонента. Вот исправленная версия add-paste.component.spec.ts .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
it(‘should not display the modal unless the button is clicked’, () => {
//source-model is an id for the modal.
expect(element.innerHTML).not.toContain(«source-modal»);
//Component’s showModal property should be false at the moment
expect(component.showModal).toBeFalsy(«Show modal should be initially false»);
})
it(‘should display the modal when `create Paste` is clicked’,() => {
let createPasteButton = fixture.debugElement.query(By.css(«button»));
//create a spy on the createPaste method
spyOn(component,»createPaste»).and.callThrough();
//triggerEventHandler simulates a click event on the button object
createPasteButton.triggerEventHandler(‘click’,null);
//spy checks whether the method was called
expect(component.createPaste).toHaveBeenCalled();
fixture.detectChanges();
expect(component.showModal).toBeTruthy(«showModal should now be true»);
expect(element.innerHTML).toContain(«source-modal»);
})
|
Пересмотренные тесты являются более явными в том смысле, что они прекрасно описывают логику компонента. Вот компонент AddPaste и его шаблон.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
<!— add-paste.component.html —>
<div class=»add-paste»>
<button (click)=»createPaste()»> create Paste </button>
<div *ngIf=»showModal» id=»source-modal» class=»modal fade in»>
<div class=»modal-dialog» >
<div class=»modal-content»>
<div class=»modal-header»></div>
<div class=»modal-body»></div>
<div class=»modal-footer»></div>
</div>
</div>
</div>
</div>
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
/* add-paste.component.ts */
export class AddPasteComponent implements OnInit {
showModal: boolean = false;
// Languages imported from Pastebin class
languages: string[] = Languages;
constructor() { }
ngOnInit() { }
//createPaste() gets invoked from the template.
public createPaste():void {
this.showModal = true;
}
}
|
Тесты все равно не addPaste
потому что шпион addPaste
не может найти такой метод в PastebinService. Давайте вернемся к PastebinService и добавим немного мяса.
Написание тестов для сервисов
Прежде чем мы начнем писать больше тестов, давайте добавим немного кода в сервис Pastebin.
1
2
3
4
5
6
|
public addPaste(pastebin: Pastebin): Promise<any> {
return this.http.post(this.pastebinUrl, JSON.stringify(pastebin), {headers: this.headers})
.toPromise()
.then(response =>response.json().data)
.catch(this.handleError);
}
|
addPaste()
— это метод сервиса для создания новых вставок. http.post
возвращает наблюдаемое, которое преобразуется в обещание с помощью toPromise()
. Ответ преобразуется в формат JSON, и любые исключения во время выполнения перехватываются и сообщаются handleError()
.
Не могли бы вы написать тесты для сервисов, спросите вы? И мой ответ однозначно да. Сервисы, которые внедряются в компоненты Angular через Dependency Injection (DI), также подвержены ошибкам. Более того, тесты для сервисов Angular относительно просты. Методы в PastebinService должны напоминать четыре операции CRUD с дополнительным методом для обработки ошибок. Методы следующие:
- HandleError ()
- getPastebin ()
- addPaste ()
- updatePaste ()
- deletePaste ()
Мы реализовали первые три метода в списке. Давайте попробуем написать тесты для них. Вот блок описания.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import { TestBed, inject } from ‘@angular/core/testing’;
import { Pastebin, Languages } from ‘./pastebin’;
import { PastebinService } from ‘./pastebin.service’;
import { AppModule } from ‘./app.module’;
import { HttpModule } from ‘@angular/http’;
let testService: PastebinService;
let mockPaste: Pastebin;
let responsePropertyNames, expectedPropertyNames;
describe(‘PastebinService’, () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [PastebinService],
imports: [HttpModule]
});
//Get the injected service into our tests
testService= TestBed.get(PastebinService);
mockPaste = { id:999, title: «Hello world», language: Languages[2], paste: «console.log(‘Hello world’);»};
});
});
|
Мы использовали TestBed.get(PastebinService)
чтобы внедрить реальный сервис в наши тесты.
01
02
03
04
05
06
07
08
09
10
11
|
it(‘#getPastebin should return an array with Pastebin objects’,async() => {
testService.getPastebin().then(value => {
//Checking the property names of the returned object and the mockPaste object
responsePropertyNames = Object.getOwnPropertyNames(value[0]);
expectedPropertyNames = Object.getOwnPropertyNames(mockPaste);
expect(responsePropertyNames).toEqual(expectedPropertyNames);
});
});
|
getPastebin
возвращает массив объектов Pastebin. Проверка типов во время компиляции TypeScript не может использоваться для проверки того, что возвращаемое значение действительно является массивом объектов Pastebin. Следовательно, мы использовали Object.getOwnPropertNames()
чтобы оба объекта имели одинаковые имена свойств.
Второй тест следует:
1
2
3
4
5
|
it(‘#addPaste should return async paste’, async() => {
testService.addPaste(mockPaste).then(value => {
expect(value).toEqual(mockPaste);
})
})
|
Оба теста должны пройти. Вот остальные тесты.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
it(‘#updatePaste should update’, async() => {
//Updating the title of Paste with id 1
mockPaste.id = 1;
mockPaste.title = «New title»
testService.updatePaste(mockPaste).then(value => {
expect(value).toEqual(mockPaste);
})
})
it(‘#deletePaste should return null’, async() => {
testService.deletePaste(mockPaste).then(value => {
expect(value).toEqual(null);
})
})
|
Исправьте pastebin.service.ts с помощью кода для updatePaste()
и deletePaste()
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
//update a paste
public updatePaste(pastebin: Pastebin):Promise<any> {
const url = `${this.pastebinUrl}/${pastebin.id}`;
return this.http.put(url, JSON.stringify(pastebin), {headers: this.headers})
.toPromise()
.then(() => pastebin)
.catch(this.handleError);
}
//delete a paste
public deletePaste(pastebin: Pastebin): Promise<void> {
const url = `${this.pastebinUrl}/${pastebin.id}`;
return this.http.delete(url, {headers: this.headers})
.toPromise()
.then(() => null )
.catch(this.handleError);
}
|
Вернуться к компонентам
Остальные требования для компонента AddPaste следующие:
- Нажатие кнопки « Сохранить» должно вызвать метод
addPaste()
службы Pastebin. - Если операция
addPaste
выполнена успешно, компонент долженaddPaste
событие, чтобы уведомить родительский компонент. - Нажатие на кнопку « Закрыть» должно удалить идентификатор «source-modal» из DOM и
showModal
свойствоshowModal
на false.
Поскольку вышеприведенные тестовые примеры связаны с модальным окном, было бы неплохо использовать вложенные блоки описания.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
describe(‘AddPasteComponent’, () => {
.
.
.
describe(«AddPaste Modal», () => {
let inputTitle: HTMLInputElement;
let selectLanguage: HTMLSelectElement;
let textAreaPaste: HTMLTextAreaElement;
let mockPaste: Pastebin;
let spyOnAdd: jasmine.Spy;
let pastebinService: PastebinService;
beforeEach(() => {
component.showModal = true;
fixture.detectChanges();
mockPaste = { id:1, title: «Hello world», language: Languages[2], paste: «console.log(‘Hello world’);»};
//Create a jasmine spy to spy on the addPaste method
spyOnAdd = spyOn(pastebinService,»addPaste»).and.returnValue(Promise.resolve(mockPaste));
});
});
});
|
Объявление всех переменных в корне блока описания является хорошей практикой по двум причинам. Переменные будут доступны внутри блока описания, в котором они были объявлены, и это сделает тест более читабельным.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
it(«should accept input values», () => {
//Query the input selectors
inputTitle = element.querySelector(«input»);
selectLanguage = element.querySelector(«select»);
textAreaPaste = element.querySelector(«textarea»);
//Set their value
inputTitle.value = mockPaste.title;
selectLanguage.value = mockPaste.language;
textAreaPaste.value = mockPaste.paste;
//Dispatch an event
inputTitle.dispatchEvent(new Event(«input»));
selectLanguage.dispatchEvent(new Event(«change»));
textAreaPaste.dispatchEvent(new Event(«input»));
expect(mockPaste.title).toEqual(component.newPaste.title);
expect(mockPaste.language).toEqual(component.newPaste.language);
expect(mockPaste.paste).toEqual(component.newPaste.paste);
});
|
Приведенный выше тест использует метод inputTitle
querySelector()
для назначения inputTitle
, SelectLanguage
и textAreaPaste
их соответствующим HTML-элементам ( <input>
, <select>
и <textArea>
). Затем значения этих элементов заменяются значениями свойств mockPaste
. Это эквивалентно тому, что пользователь заполняет форму через браузер.
element.dispatchEvent(new Event("input"))
запускает новое событие ввода, чтобы дать шаблону знать, что значения поля ввода изменились. Тест ожидает, что входные значения должны быть newPaste
свойство newPaste
компонента.
newPaste
свойство newPaste
следующим образом:
1
|
newPaste: Pastebin = new Pastebin();
|
И обновите шаблон следующим кодом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
<!— add-paste.component.html —>
<div class=»add-paste»>
<button type=»button» (click)=»createPaste()»> create Paste </button>
<div *ngIf=»showModal» id=»source-modal» class=»modal fade in»>
<div class=»modal-dialog» >
<div class=»modal-content»>
<div class=»modal-header»>
<h4 class=»modal-title»>
<input placeholder=»Enter the Title» name=»title» [(ngModel)] = «newPaste.title» />
</h4>
</div>
<div class=»modal-body»>
<h5>
<select name=»category» [(ngModel)]=»newPaste.language» >
<option *ngFor =»let language of languages» value={{language}}> {{language}} </option>
</select>
</h5>
<textarea name=»paste» placeholder=»Enter the code here» [(ngModel)] = «newPaste.paste»> </textarea>
</div>
<div class=»modal-footer»>
<button type=»button» (click)=»onClose()»>Close</button>
<button type=»button» (click) = «onSave()»>Save</button>
</div>
</div>
</div>
</div>
</div>
|
Дополнительные div и классы предназначены для модального окна Bootstrap. [(ngModel)]
— это угловая директива, которая реализует двустороннее связывание данных. (click) = "onClose()"
и (click) = "onSave()"
являются примерами методов привязки событий, используемых для привязки события click к методу в компоненте. Вы можете прочитать больше о различных методах привязки данных в официальном руководстве по синтаксису шаблонов Angular.
Если вы столкнулись с ошибкой разбора шаблона, это потому, что вы не импортировали FormsModule
в AppComponent.
Давайте добавим больше спецификаций в наш тест.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
it(«should submit the values», async() => {
component.newPaste = mockPaste;
component.onSave();
fixture.detectChanges();
fixture.whenStable().then( () => {
fixture.detectChanges();
expect(spyOnAdd.calls.any()).toBeTruthy();
});
});
it(«should have a onClose method», () => {
component.onClose();
fixture.detectChanges();
expect(component.showModal).toBeFalsy();
})
|
component.onSave()
аналогичен вызову triggerEventHandler()
для элемента кнопки «Сохранить». Поскольку мы уже добавили пользовательский интерфейс для кнопки, вызов component.save()
звучит более осмысленно. Оператор ожидания проверяет, были ли сделаны какие-либо звонки шпиону. Вот окончательная версия компонента AddPaste.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
import { Component, OnInit, Input, Output, EventEmitter } from ‘@angular/core’;
import { Pastebin, Languages } from ‘../pastebin’;
import { PastebinService } from ‘../pastebin.service’;
@Component({
selector: ‘app-add-paste’,
templateUrl: ‘./add-paste.component.html’,
styleUrls: [‘./add-paste.component.css’]
})
export class AddPasteComponent implements OnInit {
@Output() addPasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>();
showModal: boolean = false;
newPaste: Pastebin = new Pastebin();
languages: string[] = Languages;
constructor(private pasteServ: PastebinService) { }
ngOnInit() { }
//createPaste() gets invoked from the template.
public createPaste():void {
this.showModal = true;
}
//onSave() pushes the newPaste property into the server
public onSave():void {
this.pasteServ.addPaste(this.newPaste).then( () => {
console.log(this.newPaste);
this.addPasteSuccess.emit(this.newPaste);
this.onClose();
});
}
//Used to close the Modal
public onClose():void {
this.showModal=false;
}
}
|
Если операция onSave
выполнена успешно, компонент должен onSave
событие, сигнализирующее родительскому компоненту (компоненту Pastebin), чтобы обновить его представление. addPasteSuccess
, который является свойством события, декорированным декоратором @Output
, служит для этой цели.
Тестирование компонента, который генерирует выходное событие, очень просто.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
describe(«AddPaste Modal», () => {
beforeEach(() => {
.
.
//Subscribe to the event emitter first
//If the emitter emits something, responsePaste will be set
component.addPasteSuccess.subscribe((response: Pastebin) => {responsePaste = response},)
});
it(«should accept input values», async(() => {
.
.
component.onSave();
fixture.detectChanges();
fixture.whenStable().then( () => {
fixture.detectChanges();
expect(spyOnAdd.calls.any()).toBeTruthy();
expect(responsePaste.title).toEqual(mockPaste.title);
});
}));
});
|
Тест подписывается на свойство addPasteSuccess
же, как и родительский компонент. Ожидание к концу подтверждает это. Наша работа над компонентом AddPaste завершена.
Раскомментируйте эту строку в pastebin.component.html :
1
|
<app-add-paste (addPasteSuccess)= ‘onAddPaste($event)’> </app-add-paste>
|
И обновите pastebin.component.ts с помощью приведенного ниже кода.
1
2
3
4
|
//This will be invoked when the child emits addPasteSuccess event
public onAddPaste(newPaste: Pastebin) {
this.pastebin.push(newPaste);
}
|
Если вы столкнулись с ошибкой, это потому, что вы не объявили компонент AddPaste
в файле спецификации компонента Pastebin. Разве не было бы замечательно, если бы мы могли объявлять все, что требуется нашим тестам, в одном месте и импортировать это в наши тесты? Чтобы это произошло, мы могли бы либо импортировать AppModule
в наши тесты, либо создать новый модуль для наших тестов. Создайте новый файл и назовите его app-testing- module.ts :
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
import { BrowserModule } from ‘@angular/platform-browser’;
import { NgModule } from ‘@angular/core’;
//Components
import { AppComponent } from ‘./app.component’;
import { PastebinComponent } from ‘./pastebin/pastebin.component’;
import { AddPasteComponent } from ‘./add-paste/add-paste.component’;
//Service for Pastebin
import { PastebinService } from «./pastebin.service»;
//Modules used in this tutorial
import { HttpModule } from ‘@angular/http’;
import { FormsModule } from ‘@angular/forms’;
//In memory Web api to simulate an http server
import { InMemoryWebApiModule } from ‘angular-in-memory-web-api’;
import { InMemoryDataService } from ‘./in-memory-data.service’;
@NgModule({
declarations: [
AppComponent,
PastebinComponent,
AddPasteComponent,
],
imports: [
BrowserModule,
HttpModule,
FormsModule,
InMemoryWebApiModule.forRoot(InMemoryDataService),
],
providers: [PastebinService],
bootstrap: [AppComponent]
})
export class AppTestingModule { }
|
Теперь вы можете заменить:
1
2
3
4
5
6
7
8
|
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AddPasteComponent ],
imports: [ HttpModule, FormsModule ],
providers: [ PastebinService ],
})
.compileComponents();
}));
|
с:
1
2
3
4
5
6
|
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule]
})
.compileComponents();
}));
|
Метаданные, которые определяют providers
и declarations
, исчезли, и вместо этого AppTestingModule
импортируется. Это аккуратно! TestBed.configureTestingModule()
выглядит более TestBed.configureTestingModule()
чем раньше.
Просмотр, редактирование и удаление вставки
Компонент ViewPaste обрабатывает логику для просмотра, редактирования и удаления вставки. Дизайн этого компонента аналогичен тому, что мы сделали с компонентом AddPaste.
Цели компонента ViewPaste перечислены ниже:
- Шаблон компонента должен иметь кнопку под названием View Paste .
- При нажатии кнопки View Paste должно появиться модальное окно с идентификатором «source-modal».
- Данные вставки должны распространяться от родительского компонента к дочернему компоненту и должны отображаться в модальном окне.
- Нажатие кнопки редактирования должно установить для
component.editEnabled
значение true (editEnabled
используется для переключения между режимом редактирования и режимом просмотра) - Нажатие кнопки « Сохранить» должно вызвать метод
updatePaste()
службы Pastebin. -
deletePaste()
на кнопку Удалить должно вызвать методdeletePaste()
службы Pastebin. - Успешные операции обновления и удаления должны генерировать событие для уведомления родительского компонента о любых изменениях в дочернем компоненте.
Давайте начнем! Первые две спецификации идентичны тестам, которые мы написали для компонента AddPaste ранее.
1
2
3
4
5
6
7
|
it(‘should show a button with text View Paste’, ()=> {
expect(element.textContent).toContain(«View Paste»);
});
it(‘should not display the modal until the button is clicked’, () => {
expect(element.textContent).not.toContain(«source-modal»);
});
|
Подобно тому, что мы делали ранее, мы создадим новый блок описания и поместим остальные спецификации в него. Таким образом, вложение блоков описания делает файл спецификации более читабельным, а наличие функции описания — более осмысленным.
Вложенный блок описания будет иметь функцию beforeEach()
которой мы будем инициализировать двух шпионов, одного для updatePaste(
), а другого для deletePaste()
. Не забудьте создать объект mockPaste
так как наши тесты полагаются на него.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
beforeEach(()=> {
//Set showPasteModal to true to ensure that the modal is visible in further tests
component.showPasteModal = true;
mockPaste = {id:1, title:»New paste», language:Languages[2], paste: «console.log()»};
//Inject PastebinService
pastebinService = fixture.debugElement.injector.get(PastebinService);
//Create spies for deletePaste and updatePaste methods
spyOnDelete = spyOn(pastebinService,’deletePaste’).and.returnValue(Promise.resolve(true));
spyOnUpdate = spyOn(pastebinService, ‘updatePaste’).and.returnValue(Promise.resolve(mockPaste));
//component.paste is an input property
component.paste = mockPaste;
fixture.detectChanges();
})
|
Вот тесты.
01
02
03
04
05
06
07
08
09
10
11
12
|
it(‘should display the modal when the view Paste button is clicked’,() => {
fixture.detectChanges();
expect(component.showPasteModal).toBeTruthy(«Show should be true»);
expect(element.innerHTML).toContain(«source-modal»);
})
it(‘should display title, language and paste’, () => {
expect(element.textContent).toContain(mockPaste.title, «it should contain title»);
expect(element.textContent).toContain(mockPaste.language, «it should contain the language»);
expect(element.textContent).toContain(mockPaste.paste, «it should contain the paste»);
});
|
Тест предполагает, что компонент имеет свойство paste
которое принимает входные данные от родительского компонента. Ранее мы видели пример того, как события, генерируемые дочерним компонентом, можно тестировать без необходимости включать логику хост-компонента в наши тесты. Аналогично, для тестирования входных свойств это проще сделать, установив свойство для фиктивного объекта и ожидая, что значения фиктивного объекта будут отображаться в коде HTML.
В модальном окне будет много кнопок, и было бы неплохо написать спецификацию, чтобы гарантировать, что кнопки доступны в шаблоне.
1
2
3
4
5
|
it(‘should have all the buttons’,() => {
expect(element.innerHTML).toContain(‘Edit Paste’);
expect(element.innerHTML).toContain(‘Delete’);
expect(element.innerHTML).toContain(‘Close’);
});
|
Давайте исправим неудачные тесты перед тем, как приступить к более сложным тестам.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<!— view-paste.component.html —>
<div class=»view-paste»>
<button class=»text-primary button-text» (click)=»showPaste()»> View Paste </button>
<div *ngIf=»showPasteModal» id=»source-modal» class=»modal fade in»>
<div class=»modal-dialog»>
<div class=»modal-content»>
<div class=»modal-header»>
<button type=»button» class=»close» (click)=’onClose()’ aria-hidden=»true»>×</button>
<h4 class=»modal-title»>{{paste.title}} </h4>
</div>
<div class=»modal-body»>
<h5> {{paste.language}} </h5>
<pre><code>{{paste.paste}}</code></pre>
</div>
<div class=»modal-footer»>
<button type=»button» class=»btn btn-default» (click)=»onClose()» data-dismiss=»modal»>Close</button>
<button type=»button» *ngIf=»!editEnabled» (click) = «onEdit()» class=»btn btn-primary»>Edit Paste</button>
<button type = «button» (click) = «onDelete()» class=»btn btn-danger»> Delete Paste </button>
</div>
</div>
</div>
</div>
</div>
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
/* view-paste.component.ts */
export class ViewPasteComponent implements OnInit {
@Input() paste: Pastebin;
@Output() updatePasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>();
@Output() deletePasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>();
showPasteModal:boolean ;
readonly languages = Languages;
constructor(private pasteServ: PastebinService) { }
ngOnInit() {
this.showPasteModal = false;
}
//To make the modal window visible
public showPaste() {
this.showPasteModal = true;
}
//Invoked when edit button is clicked
public onEdit() { }
//invoked when save button is clicked
public onSave() { }
//invoked when close button is clicked
public onClose() {
this.showPasteModal = false;
}
//invoked when Delete button is clicked
public onDelete() { }
}
|
Возможность просмотра пасты недостаточно. Компонент также отвечает за редактирование, обновление и удаление вставки. Компонент должен иметь свойство editEnabled
, которое будет иметь значение true, когда пользователь нажимает кнопку « Редактировать вставку» .
1
2
3
4
5
6
7
8
9
|
it(‘and clicking it should make the paste editable’, () => {
component.onEdit();
fixture.detectChanges();
expect(component.editEnabled).toBeTruthy();
//Now it should have a save button
expect(element.innerHTML).toContain(‘Save’);
});
|
Добавить editEnabled=true;
в метод onEdit()
для очистки первого ожидаемого оператора.
Шаблон ниже использует директиву ngIf
для переключения между режимом просмотра и режимом редактирования. <ng-container>
— это логический контейнер, который используется для группировки нескольких элементов или узлов.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
<div *ngIf=»showPasteModal» id=»source-modal» class=»modal fade in» >
<div class=»modal-dialog»>
<div class=»modal-content»>
<!—View mode —>
<ng-container *ngIf=»!editEnabled»>
<div class=»modal-header»>
<button type=»button» class=»close» (click)=’onClose()’ data-dismiss=»modal» aria-hidden=»true»>×</button>
<h4 class=»modal-title»> {{paste.title}} </h4>
</div>
<div class=»modal-body»>
<h5> {{paste.language}} </h5>
<pre><code>{{paste.paste}}</code>
</pre>
</div>
<div class=»modal-footer»>
<button type=»button» class=»btn btn-default» (click)=»onClose()» data-dismiss=»modal»>Close</button>
<button type=»button» (click) = «onEdit()» class=»btn btn-primary»>Edit Paste</button>
<button type = «button» (click) = «onDelete()» class=»btn btn-danger»> Delete Paste </button>
</div>
</ng-container>
<!—Edit enabled mode —>
<ng-container *ngIf=»editEnabled»>
<div class=»modal-header»>
<button type=»button» class=»close» (click)=’onClose()’ data-dismiss=»modal» aria-hidden=»true»>×</button>
<h4 class=»modal-title»> <input *ngIf=»editEnabled» name=»title» [(ngModel)] = «paste.title»> </h4>
</div>
<div class=»modal-body»>
<h5>
<select name=»category» [(ngModel)]=»paste.language»>
<option *ngFor =»let language of languages» value={{language}}> {{language}} </option>
</select>
</h5>
<textarea name=»paste» [(ngModel)] = «paste.paste»>{{paste.paste}} </textarea>
</div>
<div class=»modal-footer»>
<button type=»button» class=»btn btn-default» (click)=»onClose()» data-dismiss=»modal»>Close</button>
<button type = «button» *ngIf=»editEnabled» (click) = «onSave()» class=»btn btn-primary»> Save Paste </button>
<button type = «button» (click) = «onDelete()» class=»btn btn-danger»> Delete Paste </button>
</div>
</ng-container>
</div>
</div>
</div>
|
Компонент должен иметь два источника событий Output()
, один для свойства updatePasteSuccess
а другой для deletePasteSuccess
. Тест ниже подтверждает следующее:
- Шаблон компонента принимает входные данные.
- Входные данные шаблона связаны со свойством
paste
компонента. - Если операция обновления прошла успешно,
updatePasteSuccess
событие с обновленнойupdatePasteSuccess
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
it(‘should take input values’, fakeAsync(() => {
component.editEnabled= true;
component.updatePasteSuccess.subscribe((res:any) => {response = res},)
fixture.detectChanges();
inputTitle= element.querySelector(«input»);
inputTitle.value = mockPaste.title;
inputTitle.dispatchEvent(new Event(«input»));
expect(mockPaste.title).toEqual(component.paste.title);
component.onSave();
//first round of detectChanges()
fixture.detectChanges();
//the tick() operation.
tick();
//Second round of detectChanges()
fixture.detectChanges();
expect(response.title).toEqual(mockPaste.title);
expect(spyOnUpdate.calls.any()).toBe(true, ‘updatePaste() method should be called’);
}))
|
Очевидное отличие этого теста от предыдущих заключается в использовании функции fakeAsync
. fakeAsync
сравним с асинхронным, потому что обе функции используются для запуска тестов в асинхронной тестовой зоне. Однако, fakeAsync
делает ваш тест на внешний вид более синхронным.
Метод tick()
заменяет fixture.whenStable().then()
, и код становится более читабельным с точки зрения разработчика. Не забудьте импортировать fakeAsync
и галочку из @angular/core/testing
.
Наконец, вот спецификация для удаления пасты.
01
02
03
04
05
06
07
08
09
10
|
it(‘should delete the paste’, fakeAsync(()=> {
component.deletePasteSuccess.subscribe((res:any) => {response = res},)
component.onDelete();
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(spyOnDelete.calls.any()).toBe(true, «Pastebin deletePaste() method should be called»);
expect(response).toBeTruthy();
}))
|
Мы почти закончили с компонентами. Вот окончательный вариант компонента ViewPaste
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
/*view-paste.component.ts*/
export class ViewPasteComponent implements OnInit {
@Input() paste: Pastebin;
@Output() updatePasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>();
@Output() deletePasteSuccess: EventEmitter<Pastebin> = new EventEmitter<Pastebin>();
showPasteModal:boolean ;
editEnabled: boolean;
readonly languages = Languages;
constructor(private pasteServ: PastebinService) { }
ngOnInit() {
this.showPasteModal = false;
this.editEnabled = false;
}
//To make the modal window visible
public showPaste() {
this.showPasteModal = true;
}
//Invoked when the edit button is clicked
public onEdit() {
this.editEnabled=true;
}
//Invoked when the save button is clicked
public onSave() {
this.pasteServ.updatePaste(this.paste).then( () => {
this.editEnabled= false;
this.updatePasteSuccess.emit(this.paste);
})
}
//Invoked when the close button is clicked
public onClose() {
this.showPasteModal = false;
}
//Invoked when the delete button is clicked
public onDelete() {
this.pasteServ.deletePaste(this.paste).then( () => {
this.deletePasteSuccess.emit(this.paste);
this.onClose();
})
}
}
|
Родительский компонент ( pastebin.component.ts ) необходимо обновить методами для обработки событий, генерируемых дочерним компонентом.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
/*pastebin.component.ts */
public onUpdatePaste(newPaste: Pastebin) {
this.pastebin.map((paste)=> {
if(paste.id==newPaste.id) {
paste = newPaste;
}
})
}
public onDeletePaste(p: Pastebin) {
this.pastebin= this.pastebin.filter(paste => paste !== p);
}
|
Вот обновленный pastebin.component.html :
01
02
03
04
05
06
07
08
09
10
|
<tbody>
<tr *ngFor=»let paste of pastebin»>
<td> {{paste.id}} </td>
<td> {{paste.title}} </td>
<td> {{paste.language}} </td>
<td> <app-view-paste [paste] = paste (updatePasteSuccess)= ‘onUpdatePaste($event)’ (deletePasteSuccess)= ‘onDeletePaste($event)’> </app-view-paste></td>
</tr>
</tbody>
<app-add-paste (addPasteSuccess)= ‘onAddPaste($event)’> </app-add-paste>
|
Настройка маршрутов
Для создания маршрутизируемого приложения нам потребуется еще пара стандартных компонентов, чтобы мы могли создавать простые маршруты, ведущие к этим компонентам. Я создал компонент About и компонент Contact, чтобы мы могли разместить их внутри панели навигации. AppComponent
будет хранить логику для маршрутов. Мы напишем тесты для маршрутов после того, как закончим с ними.
Сначала импортируйте RouterModule
и Routes
в AppModule
(и AppTestingModule
).
1
|
import { RouterModule, Routes } from ‘@angular/router’;
|
Затем определите ваши маршруты и передайте определение маршрута в метод RouterModule.forRoot
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
const appRoutes :Routes = [
{ path: », component: PastebinComponent },
{ path: ‘about’, component: AboutComponent },
{ path: ‘contact’, component: ContactComponent},
];
imports: [
BrowserModule,
FormsModule,
HttpModule,
InMemoryWebApiModule.forRoot(InMemoryDataService),
RouterModule.forRoot(appRoutes),
],
|
Любые изменения, внесенные в AppModule
также должны быть внесены в AppTestingModule
. Но если при выполнении тестов вы столкнулись с ошибкой « Нет базового набора ссылок», добавьте следующую строку в массив providers
вашего AppTestingModule.
1
|
{provide: APP_BASE_HREF, useValue: ‘/’}
|
Теперь добавьте следующий код в app.component.html .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
<nav class=»navbar navbar-inverse»>
<div class=»container-fluid»>
<div class=»navbar-header»>
<div class=»navbar-brand» >{{title}}</div>
</div>
<ul class=»nav navbar-nav bigger-text»>
<li>
<a routerLink=»» routerLinkActive=»active»>Pastebin Home</a>
</li>
<li>
<a routerLink=»/about» routerLinkActive=»active»>About Pastebin</a>
</li>
<li>
<a routerLink=»/contact» routerLinkActive=»active»> Contact </a>
</li>
</ul>
</div>
</nav>
<router-outlet></router-outlet>
|
routerLink
— это директива, которая используется для связывания HTML-элемента с маршрутом. Мы использовали его с тегом привязки HTML здесь. RouterOutlet
— это еще одна директива, которая отмечает место в шаблоне, где должен отображаться вид маршрутизатора.
Тестирование маршрутов немного сложнее, так как требует большего взаимодействия с пользовательским интерфейсом. Вот тест, который проверяет, работают ли якорные ссылки.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
describe(‘AppComponent’, () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule],
}).compileComponents();
}));
it(`should have as title ‘Pastebin Application’`, async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual(‘Pastebin Application’);
}));
it(‘should go to url’,
fakeAsync((inject([Router, Location], (router: Router, location: Location) => {
let anchorLinks,a1,a2,a3;
let fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
//Create an array of anchor links
anchorLinks= fixture.debugElement.queryAll(By.css(‘a’));
a1 = anchorLinks[0];
a2 = anchorLinks[1];
a3 = anchorLinks[2];
//Simulate click events on the anchor links
a1.nativeElement.click();
tick();
expect(location.path()).toEqual(«»);
a2.nativeElement.click();
tick()
expect(location.path()).toEqual(«/about»);
a3.nativeElement.click();
tick()
expect(location.path()).toEqual(«/contact»);
}))));
});
|
Если все идет хорошо, вы должны увидеть что-то вроде этого.
Последние штрихи
Добавьте красивый дизайн Bootstrap в ваш проект и обслуживайте его, если вы этого еще не сделали.
1
|
ng serve
|
Резюме
Мы написали полное приложение с нуля в тестовой среде. Разве это не что-то? В этом уроке мы узнали:
- как разработать компонент, используя тестовый подход
- как написать модульные тесты и базовые тесты пользовательского интерфейса для компонентов
- об инструментах тестирования Angular и о том, как включить их в наши тесты
- об использовании
async()
иfakeAsync()
для запуска асинхронных тестов - основы маршрутизации в Angular и написание тестов для маршрутов
Я надеюсь, вам понравился рабочий процесс TDD. Пожалуйста, свяжитесь через комментарии и дайте нам знать, что вы думаете!