昨天帮我们用 Reactive Forms 所撰写的被保人表单写完单元测试之後,今天则是要来为它写整合测试。
大家还记得整合测试的目标是要测什麽吗?我帮大家复习一下:
整合测试的测试目标是要测是两个或是两个以上的类别之间的互动是否符合我们的预期。
首先我们先增加一个 Integration testing
的区块,有关於整合测试的程序码接下来都会放在这里面,至於昨天的就放在 Unit testing
的区块:
describe('TemplateDrivenFormsAsyncInsuredComponent', () => {
// 其他省略...
describe('Unit testing', () => {
// 昨天写的单元测试...
});
describe('Integration testing', () => {
// 今天要写的整合测试
});
});
跟之前样先打开 .html
来看一下目前的程序码:
<form *ngIf="formGroup" [formGroup]="formGroup" (submit)="submit()">
<ng-container
formArrayName="insuredList"
*ngFor="let control of formArray.controls; let index = index"
>
<fieldset [formGroupName]="index">
<legend>被保人</legend>
<p>
<label [for]="'name-' + index">姓名:</label>
<input type="text" [id]="'name-' + index" formControlName="name" />
<span class="error-message">{{ getErrorMessage("name", index) }}</span>
</p>
<p>
性别:
<input
type="radio"
[id]="'male-' + index"
value="male"
formControlName="gender"
/>
<label [for]="'male-' + index">男</label>
<input
type="radio"
[id]="'female-' + index"
value="female"
formControlName="gender"
/>
<label [for]="'female-' + index">女</label>
</p>
<p>
<label [for]="'age-' + index">年龄:</label>
<select name="age" [id]="'age-' + index" formControlName="age">
<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">{{ getErrorMessage("age", index) }}</span>
</p>
<p><button type="button" (click)="deleteInsured(index)">删除</button></p>
</fieldset>
</ng-container>
<p>
<button type="button" (click)="addInsured()">新增被保险人</button>
<button type="submit" [disabled]="isFormInvalid">送出</button>
</p>
</form>
大家有看出来要测什麽了吗?我来帮大家整理一下要测的项目:
type
的值要是 text
formControlName
的值要是 name
pristine
时,则不会有错误讯息pristine
且栏位的值为空字串时,则显示 此栏位必填
的错误讯息pristine
且栏位的值只有一个字时,则显示 姓名至少需两个字以上
的错误讯息pristine
且栏位的值超过十个字时,则显示 姓名至多只能输入十个字
的错误讯息type
的值要是 radio
value
的值要是 male
formControlName
的值要是 gender
type
的值要是 radio
value
的值要是 male
formControlName
的值要是 gender
formControlName
的值要是 age
pristine
时,则不会有错误讯息pristine
且栏位的值为空字串时,则显示 此栏位必填
的错误讯息addInsured
deleteInsured
type
的值要是 submit
submit
把要测的项目都列出来之後,有没有觉得要测的项目很多阿?哈哈!
再次跟大家说明,虽然上面这些项目有些其实并不真的属於整合测试的范围,但我个人会在这时候一起测,因为这样可以省下一些重复的程序码。
大家应该还记得怎麽测吧?忘记的赶快回去看一下之前的文章!
此外,开始之前也别忘记先做以下程序码所展示的前置作业,後面将不再赘述:
describe('Integration testing', () => {
let compiledComponent: HTMLElement;
beforeEach(() => {
compiledComponent = fixture.nativeElement;
});
// 案例写在这边
});
复习一下姓名栏位的验证项目:
type
的值要是 text
formControlName
的值要是 name
pristine
时,则不会有错误讯息pristine
且栏位的值为空字串时,则显示 此栏位必填
的错误讯息pristine
且栏位的值只有一个字时,则显示 `姓名至少程序码如下:
describe('the insured fields', () => {
let formGroup: FormGroup;
beforeEach(() => {
const nameControl = new FormControl('', [
Validators.required,
Validators.minLength(2),
Validators.maxLength(10)
]);
const genderControl = new FormControl('', Validators.required);
const ageControl = new FormControl('', Validators.required);
formGroup = new FormGroup({
name: nameControl,
gender: genderControl,
age: ageControl
});
component.formArray.push(formGroup);
fixture.detectChanges();
});
describe('the name input field', () => {
let nameInputElement: HTMLInputElement;
beforeEach(() => {
nameInputElement = compiledComponent.querySelector('#name-0')!;
});
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 "formControlName" and the value is "name"', () => {
// Arrange
const attributeName = 'formControlName';
const attributeValue = 'name';
// Assert
expect(nameInputElement.getAttribute(attributeName)).toBe(attributeValue);
});
describe('Error Messages', () => {
let nameFormControl: FormControl;
beforeEach(() => {
nameFormControl = formGroup.get('name') as FormControl;
});
it('should be empty string when property "pristine" of the "formControl" is `true`', () => {
// Arrange
const targetElement = compiledComponent.querySelector('#name-0 + .error-message');
// Assert
expect(targetElement?.textContent).toBe('');
});
describe('when the field is dirty', () => {
beforeEach(() => {
nameFormControl.markAsDirty();
fixture.detectChanges();
});
it('should be "此栏位必填" when the value is empty string', () => {
// Arrange
const errorMessage = '此栏位必填';
const targetElement = compiledComponent.querySelector('#name-0 + .error-message');
// Assert
expect(targetElement?.textContent).toBe(errorMessage);
});
it('should be "姓名至少需两个字以上" when the value\'s length less than 2', () => {
// Arrange
nameFormControl.setValue('A')
const errorMessage = '姓名至少需两个字以上';
const targetElement = compiledComponent.querySelector('#name-0 + .error-message');
// Act
fixture.detectChanges();
// Assert
expect(targetElement?.textContent).toBe(errorMessage);
});
it('should be "姓名至多只能输入十个字" when the value\'s length greater than 10', () => {
// Arrange
nameFormControl.setValue('ABCDE123456')
const errorMessage = '姓名至多只能输入十个字';
const targetElement = compiledComponent.querySelector('#name-0 + .error-message');
// Act
fixture.detectChanges();
// Assert
expect(targetElement?.textContent).toBe(errorMessage);
});
it('should be empty string when there are not any errors', () => {
// Arrange
nameFormControl.setValue('ABCDE123456')
const errorMessage = '姓名至多只能输入十个字';
const targetElement = compiledComponent.querySelector('#name-0 + .error-message');
// Act
fixture.detectChanges();
// Assert
expect(targetElement?.textContent).toBe(errorMessage);
});
});
});
});
});
测试结果:
这段程序码中有两个重点:
为了之後测其他栏位,我多新增了一个 test insured fields
的 describe
。这是因为要验证这些栏位之前,一定要先让被保人的表单长出来,所我才会多包一层,并把大家都会做的事情拉到这层的 beforeEach
来做。
切记不要使用 component.addInsured()
来新增被保人。
性别栏位要验证的部份非常简单,项目如下:
type
的值要是 radio
value
的值要是 male
formControlName
的值要是 gender
type
的值要是 radio
value
的值要是 male
formControlName
的值要是 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 "formControlName" and the value is "gender"', () => {
// Arrange
const attributeName = 'formControlName';
const attributeValue = 'gender';
// 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);
});
});
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 "formControlName" and the value is "gender"', () => {
// Arrange
const attributeName = 'formControlName';
const attributeValue = 'gender';
// 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);
});
});
});
测试结果:
年龄栏位要验证的项目如下:
formControlName
的值要是 age
pristine
时,则不会有错误讯息pristine
且栏位的值为空字串时,则显示 此栏位必填
的错误讯息程序码如下:
describe('the age field', () => {
const key = 'age-0'
let ageSelectElement: HTMLSelectElement;
beforeEach(() => {
ageSelectElement = compiledComponent.querySelector(`#${key}`)!;
});
it('should have attribute "formControlName" and the value is "age"', () => {
// Arrange
const attributeName = 'formControlName';
const attributeValue = 'age';
// Assert
expect(ageSelectElement.getAttribute(attributeName)).toBe(attributeValue);
});
describe('Error Messages', () => {
let ageFormControl: FormControl;
beforeEach(() => {
ageFormControl = formGroup.get('age') as FormControl;
});
it('should be empty string when property "pristine" of the "formControl" is `true`', () => {
// Arrange
const targetElement = compiledComponent.querySelector('#age-0 + .error-message');
// Assert
expect(targetElement?.textContent).toBe('');
});
describe('when the field is dirty', () => {
beforeEach(() => {
ageFormControl.markAsDirty();
fixture.detectChanges();
});
it('should be "此栏位必填" when the value is empty string', () => {
// Arrange
const errorMessage = '此栏位必填';
const targetElement = compiledComponent.querySelector('#age-0 + .error-message');
// Assert
expect(targetElement?.textContent).toBe(errorMessage);
});
});
});
});
年龄栏位的验证跟姓名的验证有 87% 像,复制过来再稍微调整一下即可。
测试结果:
删除被保人按钮要验证的是:按下按钮要能触发函式 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 there are not any insureds', () => {
// Assert
expect(buttonElement.hasAttribute('disabled')).toBe(true);
});
describe('When there is a insured', () => {
let formGroup: FormGroup;
beforeEach(() => {
const nameControl = new FormControl('', [
Validators.required,
Validators.minLength(2),
Validators.maxLength(10)
]);
const genderControl = new FormControl('', Validators.required);
const ageControl = new FormControl('', Validators.required);
formGroup = new FormGroup({
name: nameControl,
gender: genderControl,
age: ageControl
});
component.formArray.push(formGroup);
fixture.detectChanges();
});
it('should be disabled when there ara any verifying errors that insured\'s data', () => {
// Arrange
compiledComponent.querySelector('button[type="submit"]')
// Act
fixture.detectChanges();
// Assert
expect(buttonElement.hasAttribute('disabled')).toBe(true);
})
it('should be enabled when there ara any verifying errors that insured\'s data', () => {
// Arrange
formGroup.patchValue({
name: 'Leo',
gender: 'male',
age: '18',
});
// Act
fixture.detectChanges();
// Assert
expect(buttonElement.hasAttribute('disabled')).toBe(false);
})
});
});
测试结果:
至此,我们就完成了整合测试的部份罗!
今天所有的测试结果:
今天一样主要是让大家练习,提昇撰写测试的熟悉度,该讲的重点应该在之前的文章都有提到。
不过我相信大家应该写差不多类型的测试写到有点索然无味了,所以我明天不会让大家写测试,而是会总结一下 Template Driven Forms 与 Reactive Forms 这两种开发方式的优缺点,敬请期待。
今天的实作程序码会放在 Github - Branch: day15 供大家参考,建议大家在看我的实作之前,先按照需求规格自己做一遍,之後再跟我的对照,看看自己的实作跟我的实作不同的地方在哪里、有什麽好处与坏处,如此反覆咀嚼消化後,我相信你一定可以进步地非常快!
如果有任何的问题或是回馈,也都非常欢迎留言给我让我知道噢!
标题参考来源 大家好~ 如果有个表单验证需要大量重复使用的话, 我们可以为此表单验证建立一个 For...
SharedPreferences 有时候我们在应用程序会需要保存登入Session资料、个人偏好设...
今天要来用 Template Driven Forms 的方式实作一个简单的登入系统,撇开 UI ...
前言 在 上一篇文章 中,我提到使用 Obsidian 处理笔记的过程,但在「纪录资讯」这一段没有多...
第一次写文章,其实我没有什麽自信能够写得好,毕竟我一直都不够厉害,所以也觉得自己没能写出什麽;後来真...