这是使用Jasmine在Angular中进行测试的系列文章的第二部分。 在本教程的第一部分中,我们为Pastebin类和Pastebin组件编写了基本的单元测试。 最初失败的测试后来又变成绿色。
总览
这是本教程第二部分中我们将要从事的工作的概述。
在本教程中,我们将是:
- 创建新组件并编写更多的单元测试
- 为组件的UI编写测试
- 为Pastebin服务编写单元测试
- 使用输入和输出测试组件
- 使用路线测试组件
让我们开始吧!
添加粘贴(续)
我们已经完成了为AddPaste组件编写单元测试的过程。 这是我们在系列文章的第一部分中停下来的地方。
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. It shouldn't show up unless create button is clicked
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");
})
})
如前所述,我们将不会编写严格的UI测试。 相反,我们将为UI编写一些基本测试,并寻找测试组件逻辑的方法。
单击动作是使用DebugElement.triggerEventHandler()
方法触发的,该方法是Angular测试实用程序的一部分。
AddPaste组件本质上是关于创建新的粘贴。 因此,组件的模板应具有创建新粘贴的按钮。 单击该按钮应会产生一个ID为“ source-modal”的“模态窗口”,否则应保持隐藏状态。 模态窗口将使用Bootstrap设计; 因此,您可能会在模板中找到许多CSS类。
添加粘贴组件的模板应如下所示:
<!--- 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的修订版。
it('should not display the modal unless the button is clicked', () => {
//source-model is an id for the modal. It shouldn't show up unless create button is clicked
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组件及其模板。
<!--- 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>
/* 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并添加一些内容。
编写服务测试
在继续编写更多测试之前,让我们向Pastebin服务中添加一些代码。
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()
方法将其转换为一个toPromise()
。 响应被转换为JSON格式, handleError()
捕获并报告任何运行时异常。
您可能会问,我们不应该编写服务测试吗? 我的回答是肯定的。 通过依赖注入(DI)注入到Angular组件中的服务也容易出错。 此外,对Angular服务的测试相对容易。 PastebinService中的方法应类似于四个CRUD操作,并带有处理错误的其他方法。 方法如下:
- handleError()
- getPastebin()
- addPaste()
- updatePaste()
- deletePaste()
我们已经实现了列表中的前三种方法。 让我们尝试为他们编写测试。 这是describe块。
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)
将真实的服务注入到我们的测试中。
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
返回一个getPastebin
对象数组。 TypeScript的编译时类型检查不能用于验证返回的值确实是Pastebin对象数组。 因此,我们使用Object.getOwnPropertNames()
来确保两个对象具有相同的属性名称。
第二项测试如下:
it('#addPaste should return async paste', async() => {
testService.addPaste(mockPaste).then(value => {
expect(value).toEqual(mockPaste);
})
})
两项测试均应通过。 这是剩余的测试。
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);
})
})
使用updatePaste()
和deletePaste()
方法的代码修改pastebin.service.ts 。
//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()
服务的addPaste()
方法。 - 如果
addPaste
操作成功,则组件应发出一个事件以通知父组件。 - 单击“ 关闭”按钮应从DOM中删除ID“源模式”,并将
showModal
属性更新为false。
由于上述测试用例与模式窗口有关,因此最好使用嵌套的描述块。
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));
});
});
});
出于两个原因,在describe块的根部声明所有变量是一个好习惯。 这些变量将在声明它们的describe块内部访问,这使测试更具可读性。
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);
});
上面的测试使用querySelector()
方法分配inputTitle
, SelectLanguage
和textAreaPaste
各自HTML元素( <input>
, <select>
和<textArea>
)。 接下来,将这些元素的值替换为mockPaste
的属性值。 这等效于用户通过浏览器填写表单。
element.dispatchEvent(new Event("input"))
触发一个新的输入事件,以使模板知道输入字段的值已更改。 该测试期望输入值应传播到组件的newPaste
属性中。
声明newPaste
属性,如下所示:
newPaste: Pastebin = new Pastebin();
并使用以下代码更新模板:
<!--- 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)]
是一个实现双向数据绑定的Angular指令。 (click) = "onClose()"
和(click) = "onSave()"
是用于将click事件绑定到组件中方法的事件绑定技术的示例。 您可以在Angular的官方模板语法指南中阅读有关不同数据绑定技术的更多信息。
如果您遇到模板解析错误, 这是因为您尚未将FormsModule
导入到AppComponent中。
让我们为测试添加更多规格。
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()
类似于在Save按钮元素上调用triggerEventHandler()
。 由于我们已经为按钮添加了UI,因此调用component.save()
听起来更有意义。 Expect语句检查是否对间谍进行了任何调用。 这是AddPaste组件的最终版本。
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. This shows the Modal
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
装饰器装饰的事件属性,用于此目的。
测试发出输出事件的组件很容易。
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中取消注释此行:
<app-add-paste (addPasteSuccess)= 'onAddPaste($event)'> </app-add-paste>
并使用以下代码更新pastebin.component.ts 。
//This will be invoked when the child emits addPasteSuccess event
public onAddPaste(newPaste: Pastebin) {
this.pastebin.push(newPaste);
}
如果遇到错误,那是因为尚未在AddPaste
组件的spec文件中声明AddPaste
组件。 如果可以在一个地方声明测试所需要的所有内容并将其导入到测试中,那不是很好吗? 为此,我们可以将AppModule
导入测试中,也可以为测试创建一个新的Module。 创建一个新文件并将其命名为app- testing - module.ts :
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 { }
现在您可以替换:
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AddPasteComponent ],
imports: [ HttpModule, FormsModule ],
providers: [ PastebinService ],
})
.compileComponents();
}));
与:
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [AppTestingModule]
})
.compileComponents();
}));
定义providers
和declarations
的元数据已消失,而是导入了AppTestingModule
。 那很整齐! TestBed.configureTestingModule()
看起来比以前更时尚。
查看,编辑和删除粘贴
ViewPaste组件处理用于查看,编辑和删除粘贴的逻辑。 该组件的设计类似于我们对AddPaste组件所做的设计。
下面列出了ViewPaste组件的目标:
- 组件的模板应具有一个名为“ 查看粘贴”的按钮。
- 单击“ 查看粘贴”按钮应显示ID为“源模态”的模态窗口。
- 粘贴数据应从父组件传播到子组件,并应显示在模式窗口内。
- 按下编辑按钮应将
component.editEnabled
设置为true(editEnabled
用于在编辑模式和视图模式之间切换) - 单击“ 保存”按钮应调用Pastebin服务的
updatePaste()
方法。 - 单击Delete按钮应调用
deletePaste()
服务的deletePaste()
方法。 - 成功的更新和删除操作应发出一个事件,以将子组件中的任何更改通知父组件。
让我们开始吧! 前两个规范与我们之前为AddPaste组件编写的测试相同。
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");
});
与我们之前所做的类似,我们将创建一个新的describe块并将其余的规范放在其中。 嵌套描述块使这种方式的规范文件更具可读性,并且描述函数的存在更加有意义。
嵌套的describe块将具有beforeEach()
函数,在该函数中,我们将初始化两个间谍,一个用于updatePaste(
)方法,另一个用于deletePaste()
方法。 不要忘了创建一个mockPaste
对象,因为我们的测试依赖它。
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();
})
这是测试。
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代码中来实现。
模态窗口将包含许多按钮,编写规范以确保模板中的按钮可用并不是一个坏主意。
it('should have all the buttons',() => {
expect(element.innerHTML).toContain('Edit Paste');
expect(element.innerHTML).toContain('Delete');
expect(element.innerHTML).toContain('Close');
});
在进行更复杂的测试之前,让我们先修复失败的测试。
<!--- 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>
/* 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。
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()
方法以清除第一个onEdit()
语句。
下面的模板使用ngIf
指令在视图模式和编辑模式之间切换。 <ng-container>
是一个逻辑容器,用于对多个元素或节点进行分组。
<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
会发出带有已更新粘贴的事件。
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. Don't forget to import tick
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
可与async媲美,因为这两个函数均用于在异步测试区域中运行测试。 但是, fakeAsync
使外观测试看起来更加同步。
tick()
方法替换了fixture.whenStable().then()
,从开发人员的角度来看,代码更具可读性。 不要忘记导入fakeAsync
并从@angular/core/testing
打勾。
最后,这是删除粘贴的规范。
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
组件的最终草案。
/*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 )需要使用处理子组件发出的事件的方法进行更新。
/*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 :
<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
)。
import { RouterModule, Routes } from '@angular/router';
接下来,定义您的路由并将路由定义传递给RouterModule.forRoot
方法。
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
。 但是,如果在执行测试时遇到No base href set错误,请将以下行添加到AppTestingModule的providers
数组中。
{provide: APP_BASE_HREF, useValue: '/'}
现在,将以下代码添加到app.component.html中 。
<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
是另一个指令,用于在模板中标记应显示路由器视图的位置。
测试路由有些棘手,因为它涉及更多的UI交互。 这是检查锚链接是否正常工作的测试。
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设计,如果尚未完成,请为您的项目提供服务。
ng serve
摘要
我们在测试驱动的环境中从头开始编写了完整的应用程序。 那不是吗 在本教程中,我们了解到:
- 如何使用测试优先方法设计组件
- 如何编写组件的单元测试和基本UI测试
- 关于Angular的测试实用程序以及如何将它们合并到我们的测试中
- 关于使用
async()
和fakeAsync()
运行异步测试 - Angular路由的基础知识并编写路由测试
希望您喜欢TDD工作流程。 请通过评论联系我们,让我们知道您的想法!