Angular 深入浅出三十天:表单与测试 Day21 - E2E 测试实作 - 被保人表单

Day21

大家如果对於昨天的 E2E 测试如果没有什麽问题的话,今天就来为我们的被保人表单撰写 E2E 测试吧!

实作开始

撰写测试前的准备昨天有说过了,今天就不再赘述罗!不知道该干嘛的朋友可以参考昨天实作开始的一开始的做了些什麽事情。

首先我们一样先建立一个测试档 insured-form.spec.js,然後打开刚建立的测试档加上此句语法让编辑器可以知道我们在写 Cypress 以方便撰写测试程序码:

/// <reference types="cypress" />

原理昨天一样有介绍过了,忘记或不知道的朋友可以复习一下昨天的文章

被保人表单的第一个 E2E 测试的测试案例

接着我们打开刚建立的测试档,来写我们的第一个 E2E 测试的测试案例,以验证我们的环境已准备好。

程序码如下:

describe('Insured Form', () => {
  beforeEach(() => {
    cy.visit('http://localhost:4200');
    cy.get('ul li').contains(title).click();
  });

  it('have title "Reactive Forms 实作 ─ 被保险人"', () => {
    // Arrange
    const title = 'Reactive Forms 实作 ─ 被保险人';
    // Assert
    cy.get('h1').should('have.text', title);
  });
});

执行结果:

Testing Result

还记得之前在介绍 Test Runner 的时候有稍稍带过 contains 这个 Command 吗?

确切是在第 19 天的文章: 与 Cypress 的初次见面(下)

这次特别使用一次给大家看,因为如果不使用这个方式, CSS Selector 可能就要写成: cy.get('ul li:last-child > a').click(); ,满丑的。

当然根据官方的 Best Practice ,直接在上面加个 data-cy="insured-form-page-link" 的属性是最好的。

原因一样在第 19 天的文章: 与 Cypress 的初次见面(下) 有说明过,不知道的朋友可以回去复习一下。

撰写测试案例

藉由第一个测试案例来验证环境没问题後,我们就可以正式来写需求的测试案例了。

复习并整理一下要验的案例:

  • 要可以新增被保险人
  • 要可以删除被保险人
  • 输入正确姓名与选择年龄後,但没选择性别,送出按钮为 disabled 的状态
  • 输入正确姓名与选择性别後,但没选择年龄,送出按钮为 disabled 的状态
  • 选择性别与年龄後,但没输入姓名,送出按钮为 disabled 的状态

程序码如下:

describe('Insured Form', () => {
  beforeEach(() => {
    cy.visit('http://localhost:4200');
    cy.get('ul li').contains('Reactive Forms 实作 ─ 被保险人').click();
  });

  it('have title "Reactive Forms 实作 ─ 被保险人"', () => {
    // Arrange
    const title = 'Reactive Forms 实作 ─ 被保险人';
    // Assert
    cy.get('h1').should('have.text', title);
  });

  it('should can add the insured', () => {
    // Arrange
    const name = 'Leo';
    const gender = 'male';
    const age = '18';
    // Act
    cy.get('[type="button"]').click();
    cy.get('#name-0').type(name);
    cy.get(`[for="${gender}-0"]`).click();
    cy.get('#age-0').select(age);
    // Assert
    cy.get('[type="submit"]').should('be.enabled');
  });

  it('should can delete the insured', () => {
    // Act
    cy.get('[type="button"]').click();
    cy.get('fieldset').contains('删除').click();
    // Assert
    cy.get('fieldset').should('have.length', 0);
  });

  it('should can not add the insured when the age is not valid', () => {
    // Arrange
    const name = 'Leo';
    const gender = 'male';
    // Act
    cy.get('[type="button"]').click();
    cy.get('#name-0').type(name);
    cy.get(`[for="${gender}-0"]`).click();
    // Assert
    cy.get('[type="submit"]').should('be.disabled');
  });

  it('should can not add the insured when the gender is not valid', () => {
    // Arrange
    const name = 'Leo';
    const age = '18';
    // Act
    cy.get('[type="button"]').click();
    cy.get('#name-0').type(name);
    cy.get('#age-0').select(age);
    // Assert
    cy.get('[type="submit"]').should('be.disabled');
  });

  it('should can not add the insured when the name is not valid', () => {
    // Arrange
    const gender = 'male';
    const age = '18';
    // Act
    cy.get('[type="button"]').click();
    cy.get(`[for="${gender}-0"]`).click();
    cy.get('#age-0').select(age);
    // Assert
    cy.get('[type="submit"]').should('be.disabled');
  });
});

执行结果:

Testing Result

大家有觉得昨天写过一次後,今天再写一次有比较熟悉一点了吗?

虽然这次验的情境比较多,但我觉得如果大多的情境都已经有被整合测试覆盖到的话,或许只需要验证第一个情境就好。

不过在现实中,写整合测试的人不一定跟写 E2E 测试的人是同一个,所以写 E2E 的人照着需求规格写,多验一点情境也是很好的。

在今天的测试程序码中,比较值得一提的是使用 cy.select() 的使用,它的参数可以欲选择选项的 value 值,或者是选项的名称,更可以是选项的 index ,是非常方便的一个 Command 。

此外,在选性别时,如果大家不是跟我一样是点击 Label ,而是直接点选 Radio Button 的话,记得要使用 cy.check() 的 Command。

Cypress 的错误讯息

不过就算写错也无所谓,因为 Cypress 这个贴心鬼其实都会跟你说你哪里写错、可以怎麽写。

例如刚刚说的 cy.select() ,如果我们使用 cy.click() , Cypress 就会跟你说你可以用 cy.select() 来替代唷!而且还会跟你说你写错的地方是在哪一行:

Error Message

又或者你使用了 cy.select() ,但忘记带参数,它也会跟你说你漏了什麽参数:

Error Message

Cypress 真是个贴心鬼

撰写了两次的 E2E 测试之後,也累积了不少测试案例,这时候大家应该会发现有一些重复的东西散落在不同的测试档案之中,又或者会有某些 Hard Code 在测试程序码里的东西应该要被抽出来,以利後续维护。

这时我们就可以善用在第 18 天的文章里曾经提过 fixtures 与 Cypress 的 cypress.json 的配置来达成。

E2E 测试小技巧 ─ 环境变数

举例来说,如果你的 E2E 的测试专案都是在测同一个网域的网页,那我们就可以在 cypress.json 加上 baseUrl 的设置:

{
  "baseUrl": "http://localhost:4200"
}

如此就可让我们後续使用 cy.visit()cy.request() 或是 cy.intercept() 时,就可以不用再传入一样的字串。

而且这个用法还会有一个好处,就是当需要执行不同环境的测试时,我们可以用像是这样子的方式来替换掉该变数:

$ CYPRESS_BASE_URL=https://product.domain.com cypress run

更多的环境变数小技巧请详阅官方的 Environment Variables 文件。

E2E 测试小技巧 ─ fixtures

上述提到的环境变数一般常用在会因为测试环境改变时需要改变的值上,但其实还有很多值是不会因为环境改变而改变的,这时就可以用上现在这个小技巧。

这个小技巧其实我也有在第 18 天的文章 ─ 与 Cypress 的初次见面(上) 里稍微提到过,就是我们可以在 /fixtures 的资料夹底下新增 .json 档,然後我们可以将值放在里面,需要的时候再从里面拿。

像现在我们可以在 /fixtures 里新增一个 insured-form.json 的档案,然後内容大概会是这样:

{
  "title": "Reactive Forms 实作 ─ 被保险人",
  "name": "Leo",
  "gender": "male",
  "age": "18"
}

然後在 insured-form.spec.js 就可以改成这样:

import insuredForm from '../fixtures/insured-form.json';

describe('Insured Form', () => {
  beforeEach(() => {
    cy.visit('');
    cy.get('ul li').contains(insuredForm.title).click();
  });

  it('have title "Reactive Forms 实作 ─ 被保险人"', () => {
    // Arrange
    const title = insuredForm.title;
    // Assert
    cy.get('h1').should('have.text', title);
  });

  it('should can add the insured', () => {
    // Arrange
    const name = insuredForm.name;
    const gender = insuredForm.gender;
    const age = insuredForm.age;
    // Act
    cy.get('[type="button"]').click();
    cy.get('#name-0').type(name);
    cy.get(`[for="${gender}-0"]`).click();
    cy.get('#age-0').select(age);
    // Assert
    cy.get('[type="submit"]').should('be.enabled');
  });

  // 以下省略...
});

如此一来,未来当验证的资料需要改变时,就只要到 /fixtures 里的 insured-form.json 改就好,维护起来就更加轻松愉快罗!

今天我故意没有用自订 Command 的技巧来重构我的测试程序码,大家不妨试着自己自订看看吧!

本日小结

今天的重点主要是後面的两个小技巧,这两个小技巧对於日後大家真的在自己的专案或为公司专案撰写 E2E 测试会非常有帮助,请务必多加熟悉。

不过平常都用 TypeScript 写的我觉得很不习惯,明天就来分享怎麽样把它变成 TypeScript 的版本吧!

今天的实作程序码会放在 Github - Branch: day21 上供大家参考,建议大家在看我的实作之前,先按照需求规格自己做一遍,之後再跟我的对照,看看自己的实作跟我的实作不同的地方在哪里、有什麽好处与坏处,如此反覆咀嚼消化後,我相信你一定可以进步地非常快!

如果你有任何的问题或是回馈,还请麻烦留言给我让我知道!


<<:  Day21 - _ document 可以做什麽呢?

>>:  Day36 ( 电子元件 ) LCD1602 显示温湿度

[DAY8]k8s必学的设定档-yaml (上)

YAML(/ˈjæməl/,尾音类似camel骆驼)是一个可读性高,用来表达资料序列化的格式。YA...

[Angular] Day6. Sharing data between child and parent directives and components

我们了解到 Angular app 是由无数个大大小小的 Component 所组成的,所以就会常常...

[教学] ASP NET Core将HighChart图片插入到Word中并提供下载

大家好,我是一名菜鸟工程师,这篇文章用来记录我工作遇到的需求及解决方式,如果有更好的解决方式,也欢迎...

裁切,调整大小,旋转

提取ROI 在影像处理中是一个重要技能 像是在行人中要做人脸辨识 就必须找出人脸的位置座标 roi ...

Day21 Redis架构实战-高可用性

Redis高可用性 前面的说明与范例都是透过单台的Redis Server的方式进行,这样的配置下当...