本文作者:伏晓,声网Agora Web引擎研发工程师。本文先简单介绍了Cypress测试平台,之后介绍基于模型的测试(Model-Based Testing)方法,并使用其对流行的TodoMVC应用进行建模和测试。最后指出这一测试Demo的可改进之处,同时展望了基于模型测试的未来发展。

Cypress介绍

Cypress是为现代Web App打造的下一代前端测试工具。使用现代JavaScript框架构建Web应用程序的开发人员或QA工程师都可以采用Cypress快速编写和运行E2E(端到端)测试、集成测试和单元测试。
Cypress 的 Time travel 功能是它的最大亮点,支持回退至任意时间的 Snapshot, 像是在回放电影一样,将测试运行过程中的每个细节重现出来 。可以非常快速的定位问题,极大的提高了调试自动化测试的体验。让它与众不同的是它的运行机制:大多数测试工具(如Selenium)通过在浏览器外部运行并在网络上执行远程命令来运行,Cypress恰恰相反——Cypress在与你的应用程序相同的生命周期里执行。其他方面优势可以在https://docs.cypress.io/zh-cn/guides/overview/key-differences.html 看到。
在它的运行界面中可以看到每一步的操作,只需点击你想重现的步骤即可看到该步的截屏。如下图所示,点击“找到包含 type 的元素”这一行代码,右侧就会呈现出此时的场景,并高亮出这个元素。

具体的介绍可以到Cypress.io官网查看。也可以直接下载CypressReal World App进行真实应用场景的测试体验。在这一展示场景中,运行100个测试(包括约30个API测试和70个端到端测试)需要5分30秒,这一速度已经比很多手工冒烟测试还要快。而他是完全自动并可调式的。

需要注意的是必须等待Node.js服务器和React开发服务器启动完毕后才能运行Cypress进行测试。

基于模型的测试

基于模型的测试(Model-based Testing)属于软件测试领域的一种测试方法。按照此方法,测试用例可以完全或部分的利用模型自动产生。 MBT 则允许软件开发人员和测试人员,只关注建立系统的正确性以及模型的规范性,再通过专门的 MBT 工具根据不同的测试用例设计策略从系统模型生成可靠的测试用例。
测试Case是由模型生成,而不是由源代码生成。因此,基于模型的测试又常常被当作黑盒测试的一种形式。对于复杂的软件系统中,如何应用基于模型的测试还在探索中,在测试学术界已经有一些关于自动生成测试模型的论文发表。

优点

  1. 测试用例的维护更轻松:维护好模型,无需关注测试用例的细节
  2. 软件缺陷发现的更早:在构建被测系统模型的过程中,需要对被测系统有比较全面的理解,那么在早期建模过程中就可以发现被测系统中的一些问题,不需要等到执行大量用例时才发现;
  3. 测试自动化的水平更高:建模后,MBT可以自动生成测试用例,不需要人工编写测试文档;
  4. 测试覆盖率变得更高,使得彻底的测试(即:穷尽测试)成为可能:传统测试设计的过程中依赖人工,MBT生成测试路径时会避免人工设计测试用例时的思维局限性,生成更丰富的测试路径用例,但是是否需要执行“彻底”的测试由漏测对业务的影响决定。MBT只是从技术上提供了穷尽测试可能性。
  5. 基于模型间接维护测试用例的方式更高效:传统维护时需要依赖人工,需要耗费大量的人力和时间成本,重新测试设计。使用MBT只需要维护好测试模型即可,生成测试用例的工作可以由MBT工具自动完成;

缺点

  1. 学习成本较高:要求开发人员、测试人员都精通建模,和工具的选型;
  2. 使用MBT的初期投资较大:已有的MBT工具不一定适合自己产品,定制的话需要考虑扩展性,和处理复杂逻辑,也就是要花费大量时间和精力;
  3. 用例爆炸

使用@xstate/test对TodoMVC进行基于模型的测试

Todo MVC是前端流行的一个用于展示现代JavaScript 框架和状态管理功能的Web App。它的Spec可以在https://github.com/tastejs/todomvc/blob/master/app-spec.md找到。
@xstate/test是由微软的David Khourshid编写的一个基于模型的测试生成工具,它是基于Statecharts这一有限状态机模型衍生出的工具。但它不仅能用于测试使用xstate编写的软件,对所有能被模型化的Web App基本都能通过这一工具进行基模测试。

使用@xstate/test对某一系统进行基于模型的测试有如下几个步骤:

  1. 创建对该应用描述的Statecharts抽象模型
  2. 通过可视化工具等方法确保模型的一致性
  3. 在模型的状态中添加确保系统处在该状态的断言
  4. 提供应用状态转换的事件与真实操作
  5. 使用测试框架运行自动生成的测试

下面我尝试使用xstate软件对有一个Todo项目的情况进行简单建模。该TodoMVC代码fork自Cypress的官方示例:GitHub - cypress-io/cypress-example-todomvc: The official TodoMVC tests written in Cypress.

对Todo Item进行建模

其中Unknown是一个中间过渡状态,它可以确保该Todo状态机正确保存了内部的状态并进行转换。可以参考:

TodoMVC的Todo Item的Statecharts模型——通过xstate可视化工具生成

添加状态的断言

在要测试的状态节点(Unknown的过渡状态除外)中添加确保App处在这一状态的断言:

Editing: {  meta: {    test: () => {      cy.get('.todo-list li').should('have.class', 'editing');      cy.get('.main').should('be.visible');      cy.get('.footer').should('be.visible');    },  },}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这里用到了Cypress的选择器和断言工具,确保在编辑状态时的UI状态是正确的。这样依次添加完成所有状态的断言。

添加状态转换事件

在状态模型中定义的抽象状态转换事件并不能使测试App的真实状态发生转换,所以要在测试模型中注入能使该事件正确发生的UI方法调用。

使用Cypress提供的函数产生转换事件:

const testModel = createModel(todoViewMachine).withEvents({  FILTER_CHANGE: {    exec: (_, event) => {      return cy.get('.filters').contains(event.value).click();    },    cases: [{ value: 'Completed' }, { value: 'All' }, { value: 'Active' }],  },  TODO_TOGGLE: () => {    return cy.get('.todo-list li').eq(0).find('.toggle').click();  },  CLEAR_COMPLETED: () => {    return cy.get('.clear-completed').click();  },  TODO_DELETE: () => {    return cy.get('.destroy').click({ force: true });  },  TODO_EDIT: () => {    return cy.get('.todo-list li').eq(0).find('label').dblclick();}});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

生成用例和运行测试

有了上面这些准备,现在@xstate/test就可以自动生成并运行测试:

const testPlans = testModel.getSimplePathPlans();testPlans.forEach((plan) => {  plan.paths.forEach((path) => {    it(path.description, () => {      return cy        .get('.new-todo')        .type('Hello World!{enter}')        .then(() => {          return path.test(cy);        });    });  });});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

同时你也可以调用Test Model上的testCoverage方法获得不同状态的覆盖率报告:

it('coverage', () => {  testModel.testCoverage({    filter: (stateNode) => !stateNode.id.startsWith('Todo.Unknown'),  });});
  • 1
  • 2
  • 3
  • 4
  • 5

使用@xstate/test + cypress进行测试的界面

问题与展望

因为对该应用定义的状态非常复杂,所以只能执行最短路径用例生成,不能执行简单路径用例生成,否则会由于状态太多生成过多测试用例,导致应用卡死。

简单路径的图示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RMDFP56O-1604997416625)(upload://An4zaeXMTtWb6MKEdeAs4c99GqO.png)]
最短路径的图示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GwZFXxbi-1604997416629)(upload://e5YlhKQdx63EDAXrcNMy7wmgy60.png)]

但这也只是Todo MVC最简单(只有一个Todo项目)的状态机,如果Todo数量增加,状态数量将呈指数增长。所以如果要将该测试方法应用到实际,还需要更高阶层的抽象或者更加自动化的方法来生成测试对象的状态模型。

推荐资源

👉 访问「声网开发者社区」,阅读更多 RTC 技术干货。