昨天帮我们用 Template Driven Forms 所撰写的被保人表单写完单元测试之後,今天则是要来为它写整合测试。
大家还记得整合测试的目标是要测什麽吗?我帮大家复习一下:
整合测试的测试目标是要测是两个或是两个以上的类别之间的互动是否符合我们的预期。
首先我们先增加一个 Integration testing
的区块,有关於整合测试的程序码接下来都会放在这里面,至於昨天的就放在 Unit testing
的区块:
describe('TemplateDrivenFormsAsyncInsuredComponent', () => {
// 其他省略...
describe('Unit testing', () => {
// 昨天写的单元测试...
});
describe('Integration testing', () => {
// 今天要写的整合测试
});
});
跟之前样先打开 .html
来看一下目前的程序码:
<form>
<fieldset *ngFor="let insured of insuredList; let index = index; trackBy: trackByIndex">
<legend>被保人</legend>
<p>
<label [for]="'name-' + index">姓名:</label>
<input
type="text"
[name]="'name-' + index"
[id]="'name-' + index"
required
maxlength="10"
minlength="2"
#nameNgModel="ngModel"
[ngModel]="insured.name"
(ngModelChange)="insuredNameChange(nameNgModel.control, insured)"
/>
<span class="error-message">{{ insured.nameErrorMessage }}</span>
</p>
<p>
性别:
<input
type="radio"
[name]="'gender-' + index"
[id]="'male-' + index"
value="male"
required
[(ngModel)]="insured.gender"
>
<label [for]="'male-' + index">男</label>
<input
type="radio"
[name]="'gender-' + index"
[id]="'female-' + index"
value="female"
required
[(ngModel)]="insured.gender"
>
<label [for]="'female-' + index">女</label>
</p>
<p>
<label [for]="'age-' + index">年龄:</label>
<select
[name]="'age-' + index"
[id]="'age-' + index"
required
#ageNgModel="ngModel"
[ngModel]="insured.age"
(ngModelChange)="insuredAgeChange(ageNgModel.control, insured)"
>
<option value="">请选择</option>
<option value="18">18岁</option>
<option value="20">20岁</option>
<option value="70">70岁</option>
<option value="75">75岁</option>
</select>
<span class="error-message">{{ insured.ageErrorMessage }}</span>
</p>
<p><button type="button" (click)="deleteInsured(index)">删除</button></p>
</fieldset>
<p>
<button type="button" (click)="addInsured()">新增被保险人</button>
<button type="submit" [disabled]="isFormInvalid">送出</button>
</p>
</form>
大家有看出来要测什麽了吗?我来帮大家整理一下要测的项目:
type
的值要是 text
name
的值要是 name-N
minlength
的值要是 2
maxlength
的值要是 10
required
name
的值绑定到此栏位上insuredNameChange
type
的值要是 radio
name
的值要是 gender-N
required
gender
的值绑定到此栏位上name
的值要是 age-N
required
age
的值绑定到此栏位上insuredAgeChange
nameErrorMessage
的值绑定到画面上ageErrorMessage
的值绑定到画面上addInsured
deleteInsured
type
的值要是 submit
submit
把要测的项目都列出来之後,有没有觉得要测的项目很多阿?哈哈!
再次跟大家说明,虽然上面这些项目有些其实并不真的属於整合测试的范围,但我个人会在这时候一起测,因为这样可以省下一些重复的程序码。
此外,开始之前也别忘记先做以下程序码所展示的前置作业:
describe('Integration testing', () => {
let compiledComponent: HTMLElement;
beforeEach(() => {
fixture.detectChanges();
compiledComponent = fixture.nativeElement;
});
// 案例写在这边
});
复习一下姓名栏位的验证项目:
type
的值要是 text
name
的值要是 name-N
minlength
的值要是 2
maxlength
的值要是 10
required
name
的值绑定到此栏位上insuredNameChange
接下来就把姓名栏位要验证的项目写成测试案例:
describe('the insured fields', () => {
beforeEach(() => {
component.insuredList = [{
name: '',
gender: '',
age: '',
nameErrorMessage: '',
ageErrorMessage: ''
}];
fixture.detectChanges();
});
describe('the name input field', () => {
const key = 'name-0'
let nameInputElement: HTMLInputElement;
beforeEach(() => {
nameInputElement = compiledComponent.querySelector(`#${key}`)!;
});
it('should have attribute "type" and the value is "text"', () => {
// Arrange
const attributeName = 'type';
const attributeValue = 'text';
// Assert
expect(nameInputElement.getAttribute(attributeName)).toBe(attributeValue);
});
it('should have attribute "name" and the value is "name-0"', () => {
// Arrange
const attributeName = 'ng-reflect-name';
const attributeValue = key;
// Assert
expect(nameInputElement.getAttribute(attributeName)).toBe(attributeValue);
});
it('should have attribute "minlength" and the value is "2"', () => {
// Arrange
const attributeName = 'minlength';
const attributeValue = '2';
// Assert
expect(nameInputElement.getAttribute(attributeName)).toBe(attributeValue);
});
it('should have attribute "maxlength" and the value is "10"', () => {
// Arrange
const attributeName = 'maxlength';
const attributeValue = '10';
// Assert
expect(nameInputElement.getAttribute(attributeName)).toBe(attributeValue);
});
it('should have attribute "required"', () => {
// Arrange
const attributeName = 'required';
// Assert
expect(nameInputElement.hasAttribute(attributeName)).toBe(true);
});
it('should binding the value of the insured\'s property "name"', () => {
// Arrange
const name = 'whatever';
// Act
component.insuredList[0].name = name;
fixture.detectChanges();
// Assert
expect(nameInputElement.getAttribute('ng-reflect-model')).toBe(name);
});
it('should trigger function "insuredNameChange" when the value be changed', () => {
// Arrange
spyOn(component, 'insuredNameChange');
const nameNgModel = component.nameNgModelRefList.get(0)!;
// Act
nameInputElement.value = 'whatever';
nameInputElement.dispatchEvent(new Event('ngModelChange'));
// Assert
expect(component.insuredNameChange).toHaveBeenCalledWith(nameNgModel.value, nameNgModel.errors, component.insuredList[0]);
});
});
});
测试结果:
这段程序码中有几个重点:
为了之後测其他栏位,我多新增了一个 test insured fields
的 describe
。这是因为要验证这些栏位之前,一定要先让被保人的表单长出来,所我才会多包一层,并把大家都会做的事情拉到这层的 beforeEach
来做。
should have attribute "name" and the value is "name-0"
这个测试案例要记得我们在 Template 绑定时是用 [name]
的方式绑定,所以在验证的时候是抓 ng-reflect-name
,如果单纯抓 name
来验是会报错的噢!
should trigger function "insuredNameChange" when the value be changed
最後这个测试案例比较特别,不知道大家还记不记得上次写这里的时候,我有介绍过关於 Spy 的事情与怎麽用 @ViewChild
抓 Template 中的 nameFormControl
?
如果不记得的话,赶快回去第七天的文章复习一下!
上次用的 @ViewChild
是抓取单一的元素,但这次是复数的怎办?
答案是 ─ @ViewChildren
。
有没有一种写 Angular 还可以学英文的感觉?
只要我们像这样在程序码中加上这个 Angular 的装饰器:
export class TemplateDrivenFormsAsyncInsuredComponent {
@ViewChildren('nameNgModel') nameNgModelRefList!: QueryList<NgModel>;
// ...
}
Angular 就会在每次渲染完画面之後,帮我们抓取有在 HTML 的属性中加上 #nameNgModel
的所有元素,而抓出来的元素会用 Angular 所包装的类别 ─ QueryList
包起来,以利我们使用。
性别栏位的验证项目如下:
type
的值要是 radio
name
的值要是 male-N
value
的值要是 male
required
gender
的值绑定到此栏位上type
的值要是 radio
name
的值要是 female-N
value
的值要是 female
required
gender
的值绑定到此栏位上测试程序码如下:
describe('the gender radio buttons', () => {
let radioButtonElement: HTMLInputElement;
describe('male', () => {
beforeEach(() => {
radioButtonElement = compiledComponent.querySelector(`#male-0`)!;
});
it('should have attribute "type" and the value is "radio"', () => {
// Arrange
const attributeName = 'type';
const attributeValue = 'radio';
// Assert
expect(radioButtonElement.getAttribute(attributeName)).toBe(attributeValue);
});
it('should have attribute "name" and the value is "gender-0"', () => {
// Arrange
const attributeName = 'ng-reflect-name';
const attributeValue = 'gender-0';
// Assert
expect(radioButtonElement.getAttribute(attributeName)).toBe(attributeValue);
});
it('should have attribute "value" and the value is "male"', () => {
// Arrange
const attributeName = 'value';
const attributeValue = 'male';
// Assert
expect(radioButtonElement.getAttribute(attributeName)).toBe(attributeValue);
});
it('should have attribute "required"', () => {
// Arrange
const attributeName = 'required';
// Assert
expect(radioButtonElement.hasAttribute(attributeName)).toBe(true);
});
it('should binding the value of the insured\'s property "gender"', () => {
// Arrange
const gender = 'male';
// Act
component.insuredList[0].gender = gender;
fixture.detectChanges();
// Assert
expect(radioButtonElement.getAttribute('ng-reflect-model')).toBe(gender);
});
});
describe('female', () => {
beforeEach(() => {
radioButtonElement = compiledComponent.querySelector(`#female-0`)!;
});
it('should have attribute "type" and the value is "radio"', () => {
// Arrange
const attributeName = 'type';
const attributeValue = 'radio';
// Assert
expect(radioButtonElement.getAttribute(attributeName)).toBe(attributeValue);
});
it('should have attribute "name" and the value is "gender-0"', () => {
// Arrange
const attributeName = 'ng-reflect-name';
const attributeValue = 'gender-0';
// Assert
expect(radioButtonElement.getAttribute(attributeName)).toBe(attributeValue);
});
it('should have attribute "value" and the value is "female"', () => {
// Arrange
const attributeName = 'value';
const attributeValue = 'female';
// Assert
expect(radioButtonElement.getAttribute(attributeName)).toBe(attributeValue);
});
it('should have attribute "required"', () => {
// Arrange
const attributeName = 'required';
// Assert
expect(radioButtonElement.hasAttribute(attributeName)).toBe(true);
});
it('should binding the value of the insured\'s property "gender"', () => {
// Arrange
const gender = 'female';
// Act
component.insuredList[0].gender = gender;
fixture.detectChanges();
// Assert
expect(radioButtonElement.getAttribute('ng-reflect-model')).toBe(gender);
});
});
});
这边的测试虽然简单,但我还是遇到了一个问题:「怎麽验双向绑定里,关於
ngModelChange
的部份」。我的预期是我点击了某个性别的单选钮之後,它会把值指定给被保人的
gender
栏位。但我试了好几种验法,也查了老半天资料,就是没办法成功(摊手),如果有朋友成功验出来,请麻烦在下方留言分享一下,感谢!
测试结果:
年龄栏位的验证项目如下:
name
的值要是 age-N
required
age
的值绑定到此栏位上insuredAgeChange
程序码如下:
describe('the age field', () => {
const key = 'age-0'
let ageSelectElement: HTMLSelectElement;
beforeEach(() => {
ageSelectElement = compiledComponent.querySelector(`#${key}`)!;
});
it('should have attribute "name" and the value is "age-0"', () => {
// Arrange
const attributeName = 'ng-reflect-name';
const attributeValue = key;
// Assert
expect(ageSelectElement.getAttribute(attributeName)).toBe(attributeValue);
});
it('should have attribute "required"', () => {
// Arrange
const attributeName = 'required';
// Assert
expect(ageSelectElement.hasAttribute(attributeName)).toBe(true);
});
it('should binding the value of the insured\'s property "age"', () => {
// Arrange
const age = '18';
// Act
component.insuredList[0].age = age;
fixture.detectChanges();
// Assert
expect(ageSelectElement.getAttribute('ng-reflect-model')).toBe(age);
});
it('should trigger function "insuredAgeChange" when the value be changed', () => {
// Arrange
spyOn(component, 'insuredAgeChange');
const ageNgModel = component.ageNgModelRefList.get(0)!;
// Act
ageSelectElement.value = '18';
ageSelectElement.dispatchEvent(new Event('ngModelChange'));
// Assert
expect(component.insuredAgeChange).toHaveBeenCalledWith(ageNgModel.value, ageNgModel.errors, component.insuredList[0]);
});
});
年龄栏位的验证跟姓名的验证有 87% 像,复制过来再稍微调整一下即可。
测试结果:
错误讯息要验证的项目是:
nameErrorMessage
的值绑定到画面上ageErrorMessage
的值绑定到画面上测试程序码如下:
describe('Error Messages', () => {
it('should binding the value of the insured\'s property "nameErrorMessage" in the template', () => {
// Arrange
const insured = component.insuredList[0];
const errorMessage = 'account error';
const targetElement = compiledComponent.querySelector('#name-0 + .error-message');
// Act
insured.nameErrorMessage = errorMessage;
fixture.detectChanges();
// Assert
expect(targetElement?.textContent).toBe(errorMessage);
});
it('should binding the value of the insured\'s property "ageErrorMessage" in the template', () => {
// Arrange
const insured = component.insuredList[0];
const errorMessage = 'password error';
const targetElement = compiledComponent.querySelector('#age-0 + .error-message');
// Act
insured.ageErrorMessage = errorMessage;
fixture.detectChanges();
// Assert
expect(targetElement?.textContent).toBe(errorMessage);
});
});
错误讯息的验证也非常简单,大家应该都能轻松验证!
测试结果:
删除被保人按钮要验证的是:按下按钮要能触发函式 deleteInsured
。这部份大家只要使用 Spy
的技巧来验证即可,也是颇为简单。
程序码如下:
describe('Delete insured button', () => {
it('should trigger function `deleteInsured` after being clicked', () => {
// Arrange
const index = 0;
const deleteButtonElement = compiledComponent.querySelector('fieldset button[type="button"]') as HTMLElement;
spyOn(component, 'deleteInsured');
// Act
deleteButtonElement.click();
// Assert
expect(component.deleteInsured).toHaveBeenCalledWith(index);
});
});
测试结果:
新增被保人按钮要验证的是:按下按钮要能触发函式 addInsured
,跟删除被保人的按钮要验证的项目几乎是一模一样,复制过来稍微修改一下即可。
程序码如下:
describe('add insured button', () => {
it('should trigger function `addInsured` after being clicked', () => {
// Arrange
const addButtonElement = compiledComponent.querySelector('p:last-child button[type="button"]') as HTMLElement;
spyOn(component, 'addInsured');
// Act
addButtonElement.click();
// Assert
expect(component.addInsured).toHaveBeenCalled();
});
});
测试结果:
最後,送出按钮要验证的项目是:
type
的值要是 submit
submit
程序码如下:
describe('submit button', () => {
let buttonElement: HTMLButtonElement;
beforeEach(() => {
buttonElement = compiledComponent.querySelector('button[type="submit"]') as HTMLButtonElement;
});
it('should be existing', () => {
// Assert
expect(buttonElement).toBeTruthy();
});
it('should be disabled when "insuredList" is empty array', () => {
// Assert
expect(buttonElement.hasAttribute('disabled')).toBe(true);
});
it('should be disabled when there are any verifying errors that insured\'s data', () => {
// Arrange
component.insuredList = [{
name: 'A',
gender: '',
age: '',
nameErrorMessage: '',
ageErrorMessage: ''
}];
compiledComponent.querySelector('button[type="submit"]')
// Act
fixture.detectChanges();
// Assert
expect(buttonElement.hasAttribute('disabled')).toBe(true);
})
it('should be enabled when there are any verifying errors that insured\'s data', () => {
// Arrange
component.insuredList = [{
name: 'Leo',
gender: 'male',
age: '18',
nameErrorMessage: '',
ageErrorMessage: ''
}];
// Act
fixture.detectChanges();
// Assert
expect(buttonElement.hasAttribute('disabled')).toBe(false);
})
});
测试结果:
咦?怎麽会有 Error 咧?原来这个问题跟上次我们写登入表单的整合测试所遇到的情况一样。
所以我们目前先在这个案例的 it
的前面加上一个 x
,代表我们要 ignore
这个案例的意思,像这样:
xit('should be disabled when there are any verifying errors that insured\'s data', () => {
// 省略...
})
测试结果:
至此,我们就完成了整合测试的部份罗!
今天所有的测试结果:
其实今天用所有用到的测试手法与概念都在之前的的文章就已经分享过了,今天主要是让大家练习,提昇撰写测试的熟悉度。
明天我们要为用 Reactive Forms 所撰写的被保人表单来撰写单元测试,我觉得大家可以在看我的文章之前先自己写写看,之後再参考我的文章,一定会有更多的收获!
今天的实作程序码会放在 Github - Branch: day13 供大家参考,建议大家在看我的实作之前,先按照需求规格自己做一遍,之後再跟我的对照,看看自己的实作跟我的实作不同的地方在哪里、有什麽好处与坏处,如此反覆咀嚼消化後,我相信你一定可以进步地非常快!
如果有任何的问题或是回馈,也都非常欢迎留言给我让我知道噢!
>>: Day 23:专案05 - KKBOX风云榜02 | AJAX
昨天教到使用cookie让服务器记得我们曾经做过哪些事,但缺点就是每次Request都要加上cook...
回圈 想要重复做一件事,会依据条件而有不同的执行次数 for 回圈 写法如下 for (let i ...
大家好! 文章到今天也快要写一半了,谢谢各位的阅读。 我们进入今天的主题吧! 控制物件 先建立一个物...
###本文章内容皆由我本人开发撰写与分享 在变更单建立时,经常会花时间编辑受影响物件 经常是为了从E...
删除作者 作法1 : 直接修改 删除作者,删掉,从这边删掉,按确定就可以了 作法2 : 移除档案属性...