前端自动化测试概览

14,794 阅读19分钟
原文链接: alonecat.com

前端自动化测试

为什么需要自动化测试

项目经过不断的开发,最终肯定会趋于稳定,在适当的时机下引入自动化测试能及早发现问题,保证产品的质量。

自动化的收益 = 迭代次数 * 全手动执行成本 - 首次自动化成本 - 维护次数 * 维护成本

测试

测试作为完整的开发流程中最后的一环,是保证产品质量重要的一环。而前端测试一般在产品开发流程中属于偏后的环节,在整个开发架构中属于较高层次,前端测试更加偏向于GUI的特性,因此前端的测试难度很大。

测试方法

黑盒测试

黑盒测试一般也被称为功能测试,黑盒测试要求测试人员将程序看作一个整体,不考虑其内部结构和特性,只是按照期望验证程序是否能正常工作。黑盒测试更接近用户使用的真实场景,因为对于用户来说,程序的内部是不可见的。

以下是黑盒测试中常用的测试方法:

  • 等价类划分

    等价类划分主要是在已有输入规则的情况下,确定合法输入与非法输入区间来设计测试用例

    如:如网站登录密码必须由6位数字构成

    有效等价类:6位数字

    无效等价类:位数>6,位数<6,全角数字,字母、特殊字符等……

  • 边界值分析

    顾名思义,主要是根据输入输出范围的边界值进行测试用例的设计。原因是大量的错误往往发生在输入或输出范围的边界上(程序员往往在容易在这些地方犯错),边界值分析一般结合等价类划分进行使用,等价类划分区间边界一般就是边界值。

    如:如网站登录密码长度必须为6-12位

    有效等价类:位数[6-12]

    无效等价类:位数<6 位数>12

    边界值:6 12

  • 错误推测、异常分析等

    黑盒测试还包含一些其他的测试方式,由于测试往往是不可穷举性的,因此如何如何设计测试用例保证测试覆盖尽可能多的场景,不仅仅是依靠这些总结出来的方法,也考验测试人员自身的天赋

白盒测试

白盒测试是基于代码本身的测试,一般指对代码逻辑结构的测试。白盒测试是在了解代码结构的前提下进行的测试,目的是遍历尽可能多的可执行路径,得出测试数据。白盒测试方法比较多,主要是逻辑覆盖,即检查代码的每一行、每一次判断结果。

逻辑覆盖方式从发现错误能力上排序主要有以下几种:

  1. 语句覆盖 (让程序执行到每一行语句)
  2. 判定覆盖 (让每一个判断语句满足真假)
  3. 条件覆盖 (让每一个判断语句里面的每一个条件都取到真假值)
  4. 判定/条件覆盖 (同时满足2和3)
  5. 条件组合覆盖 (判断语句中条件的每种组合至少出现一次)
  6. 路径覆盖 (覆盖程序每一条执行路径)

测试分类

按照软件工程自底而上的概念,前端测试一般分为单元测试(Unit Testing )、集成测试(Integration Testing)和端到端测试(E2E Testing)。从下面的图可以看出,从底向上测试的复杂度将不断提高,另一方面测试的收益反而不断降低的。

前端测试模型前端测试模型

单元测试(Unit Testing)

单元测试是指对程序中最小可测试单元进行的测试,一般而言是指对函数进行的测试。单元测试混合了编程和测试,由于是对代码内部逻辑进行测试,因此更多的使用白盒的测试方式。单元测试能强迫开发者写出更可测试的代码,一般而言这样的代码可读性也会高很多,同时良好的单元测试可以作为被测代码的文档使用。

函数可测性:可测试性高的函数一般而言是纯函数,即输入输出可预测的函数。即在函数内部不修改传入参数,不执行API请求或者IO请求,不调用其他非纯函数如Math.random()等

单元测试最大的特点是测试对象的细颗粒度性,即被测对象独立性高、复杂度低。

前端单元测试

前端单元测试和后端单元测试最大的区别在于,前端单元测试无法避免的会存在兼容性问题,如调用浏览器兼容性API,以及对BOM(浏览器对象模型)API的调用,因此前端单元测试需要运行在(伪)浏览器环境下。

就测试运行环境分类,主要有以下几种测试方案:

  • 基于JSDOM
    • 优点:快,执行速度最快,因为不需要浏览器启动
    • 缺点:无法测试如seesion或cookie等相关操作,并且由于不是真实浏览器环境因此无法保证一些如Dom相关和BOM相关的操作的正确性,并且JSDOM未实现localStorage,如果需要进行覆盖,只能使用第三方库如node-localStorage (这个库本身对与执行环境的判断有一些问题)进行模拟。
  • 基于PhantomJs等无头浏览器
    • 优点: 相对较快,并且具有真实的DOM环境
    • 缺点: 同样不在真实浏览器中运行,难以调试,并且项目issue非常多,puppeteer发布后作者宣布不再维护
  • 使用Karma或puppeteer等工具,调用真实的浏览器环境进行测试
    • 优点:配置简单,能在真实的浏览器中运行测试,并且karma能将测试代码在多个浏览器中运行,同时方便调试
    • 缺点: 唯一的缺点就是相对前两者运行稍慢,但是在单元测试可接受范围内
前端单元测试工具

前端在近几年如雨后春笋一般涌现出非常多的测试框架和相关工具。

  • 测试平台
    • karma – Google Angular团队开发的测试运行平台,配置简单灵活,能够很方便将测试在多个真实浏览器中运行。
  • 测试框架
    • mocha – Tj大神开发的很优秀的测试框架,有完善的生态系统,简单的测试组织方式,不对断言库和工具做任何限制,非常灵活。
    • jasmine – 和Mocha语法非常相似,最大的差别是提供了自建的断言和Spy和Stub
    • jest – facebook出品的大而全的测试框架,React官方推荐的单元测试框架,配置简单运行速度快。(备注:无法和Karma进行集成)
    • AVA – 和上面的测试框架最大的区别在于多线程,运行速度更快。
    • 其他 – 还有一些其他的前端测试框架,但是相似度比较高,无非是对断言和测试桩等工具的集成度不同,如果考虑稳定以及成熟度建议选择Mocha,对测试运行速度有非常高的要求可以考虑jest和AVA
  • 测试辅助工具
    • 断言库 – Chai 如果单元测试不跑在真实的浏览器环境中,可以简单使用node的assert,但是建议使用Chai作为断言库(提供了TDD和BDD风格多种断言方式,并且生态系统繁荣)。
    • 测试桩(又称为测试替身) – SinontestDouble等工具提供了如测试桩、拦截模拟请求、”时间旅行“等功能,主要用于解决”函数不纯”(比如测试回调是否被正确调用,XHR是否正确发起请求,时间延迟后函数行为是否正确)的测试问题。
  • 测试覆盖率工具
    • istanbul istanbul的基础实现,提供了命令行等工具,但是无法解决代码编译打点的问题
    • istanbul-instrumenter-loader istanbul的Webpack插件,能解决代码编译打点和测试报告输出的问题

其他参考

常见单元测试栗子

在框架选型的时候考虑到以下条件或问题:

  1. 测试需要在真实浏览器中运行
  2. 测试执行需要足够迅速
  3. 被测代码为Typescript,因此使用需要解决编译和打点的问题
  4. 方便持续集成

最后选择使用Karma+Webpack+Mocha+Chai+Sion+istanbul-instrumenter-loader的解决方案。

项目结构:

karma可以很方便的与Webpack进行集成,只需要指定需要预编译的文件和使用的编译工具即可。

karma.conf.js配置Webpack编译

module.export=funciton(config){
    config.set({
        frameworks: ['mocha', 'chai'],
        files: [
      		'tests/setup.js'
    	],
        preprocessors: {
          'tests/setup.js': ['webpack']
        },
        webpack: {
          resolve: {
            extensions: ['.js','.ts'],
          },
          module: {
            rules: [{
                test: /\.ts$/,
                loader: 'ts-loader',
                query: {
                  transpileOnly: true
                }
              },
              {
                test: /\.ts$/,
                exclude: /(node_modules|libs|\.test\.ts$)/,
                loader: 'istanbul-instrumenter-loader',
                enforce: 'post',
                options: {
                  esModules: true,
                  produceSourceMap: true
                }
              }
            ]
          },
          devtool: 'inline-source-map',
        },
        // karma-server support ts/tsx mime 
        mime: {
          'text/x-typescript': ['ts', 'tsx']
        },
    })
}

通过上面的配置中需要注意几个地方:

  • setup.js

    // 导入所有测试用例
    const testsContext = require.context("../src", true, /\.test\.ts$/);
    testsContext.keys().forEach(testsContext);
    

    使用webpack提供的require.context()将所有测试文件统一导入,然后karma将setup.js作为入口调用webpack进行编译,然后测试执行。

  • mime配置对于ts/tsx的支持

  • istanbul-instrumenter-loader loader调用顺序必须在ts或babel等编译工具编译之前否则将不准确,排除测试文件本身和依赖库覆盖率计算

    • 使用enforce: ‘post’,确保loader调用顺序
    • 使用exclude排除第三方库和测试文件本身覆盖率计算

其他配置请参考karma和webpack,以及相关插件的文档

  • 纯函数测试

    /**
     * 验证邮箱
     * @params input 输入值
     * @return 返回是否是邮箱
     */
    export function mail(input: any): boolean {
        return /^[\w\-]+(\.[\w\-]+)*@[\w\-]+(\.[\w\-]+)+$/.test(input);
    }
    
    /* ↑↑↑↑被测函数↑↑↑↑ */
    /* ↓↓↓↓测试代码↓↓↓↓ */
    
    describe('验证邮箱#mail', () => {
        it('传入邮箱字符串,返回true', () => {
            expect(mail('abc@123.com')).to.be.true;
            expect(mail('abc@123.com.cn')).to.be.true;
            expect(mail('a-b-c@123.com.cn')).to.be.true;
            expect(mail('a_b_c@123.com.cn')).to.be.true;
            expect(mail('a_b_c@123-4-5-6.com.cn')).to.be.true;
        });
    
        it('传入非邮箱字符串,返回true', () => {
            expect(mail('')).to.be.false;
            expect(mail('abc@')).to.be.false;
            expect(mail('@123.com')).to.be.false;
            expect(mail('abc@123')).to.be.false;
            expect(mail('123.com')).to.be.false;
        });
    });
    
  • Promise或其他异步操作的测试

    Mocha支持异步测试,主要有下面三种方式:

    • 使用async await

      it(async ()=>{
          await asynchronous()
          // 一些断言
      })
      
    • 使用回调函数

      it(done=>{
          Promise.resolve().then(()=>{
              // 断言
              done() // 测试结束后调用回调标识测试结束
          })
      })
      
    • 返回一个Promise

      it(()=>{
      	// 直接返回一个Promise,Mocha会自动等待Promise resolve
          return Promise.resolve().then(()=>{
              // 断言
          })
      })
      
  • 含HTTP请求的测试

    function http(method,url,body){
        // 使用XHR发送ajax请求
    }
    
    /* ↑↑↑↑被测函数↑↑↑↑ */
    /* ↓↓↓↓测试代码↓↓↓↓ */
    
    it('method为GET', (done) => {
        const xhr=fake.useFakeXMLHttpRequest()
        const request = []
        xhr.onCreate = xhr => {
            requests.push(xhr);
        };
        requests[0].respond(200, { "Content-Type": "application/json" },'[{ "id": 12, "comment": "Hey there" }]');
        get('/uri').then(() => {
                xhr.restore();
                done();
        });
        expect(request[0].method).equal('GET');
    })
    

    或者封装一下fakeXMLHttpRequest

    /**
     * 使用fakeXMLHttpRequest
     * @param callback 回调函数,接受请求对象作为参数
     */
    export function useFakeXHR(callback) {
        const fakeXHR = useFakeXMLHttpRequest();
        const requests: Array<any> = []; // requests会将引用传递给callback,因此使用const,避免指针被改写。
    
        fakeXHR.onCreate = request => requests.push(request);
    
        callback(requests, fakeXHR.restore.bind(fakeXHR));
    }
    
    it('method为GET', (done) => {
        useFakeXHR((request, restore) => {
            get('/uri').then(() => {
                restore();
                done();
            });
            expect(request[0].method).equal('GET');
            request[0].respond();
        })
    })
    

    useFakeXHR封装了对Sinon的useFakeXMLHttpRequest,实现对XHR请求的fake

    Fake XHR and server - Sinon.JS 官方文档参考

    Fake XHR和 Fake server的区别:后者是对前者的更高层次的封装,并且Fake server的颗粒度更大

  • 时间相关的测试

    // 延迟delay毫秒后调用callback
    function timer(delay,callback){
        setTimeout(callback,delay);
    }
    
    /* ↑↑↑↑被测函数↑↑↑↑ */
    /* ↓↓↓↓测试代码↓↓↓↓ */
    
    it('timer',()=>{
        const clock=sinon.useFakeTimers()
        const spy=sinon.spy() // 测试替身
        timer(1000,spy) 
        clock.tick(1000) // "时间旅行,去到1000ms后"
        expect(spy.called).to.be.true
        clock.restore() // 恢复时间,否则将影响到其他测试
    })
    
  • 浏览器相关API调用的测试

    • session的栗子
    /**
     * WebStorage封装
     * @param storage 存储对象,支持localStorage和sessionStorage
     * @return 返回封装的存储接口
     */
    export function Storage(storage: Storage) {
        /**
         * 获取session
         * @param key 
         * @return 返回JSON解析后的值
         */
        function get(key: string) {
            const item = storage.getItem(key);
    
            if (!item) {
                return null;
            } else {
                return JSON.parse(item);
            }
        }
    }
    export default Storage(window.sessionStorage);
    
    /* ↑↑↑↑被测函数↑↑↑↑ */
    /* ↓↓↓↓测试代码↓↓↓↓ */
    
    describe('session', () => {
        beforeEach('清空sessionStorage', () => {
            window.sessionStorage.clear();
        });
    
        describe('获取session#get', () => {
            it('获取简单类型值', () => {
                window.sessionStorage.setItem('foo', JSON.stringify('foo'))
                expect(session.get('foo')).to.equal('foo');
            });
    
            it('获取引用类型值', () => {
                window.sessionStorage.setItem('object', JSON.stringify({}))
                expect(session.get('object')).to.deep.equal({});
            });
    
            it('获取不存在的session', () => {
                expect(session.get('aaa')).to.be.null;
            });
        });
    })
    
    • 设置title的栗子
    /**
     * 设置tab页title
     * @param title 
     */
    export function setTitle(title) {
        // 当页面中嵌入了flash,并且页面的地址中含有“片段标识”(即网址#之后的文字)IE标签页标题被自动修改为网址片段标识
        if (userAgent().app === Browser.MSIE || userAgent().app === Browser.Edge) {
            setTimeout(function () {
                document.title = title
            }, 1000)
        } else {
            document.title = title
        }
    }
    
    /* ↑↑↑↑被测函数↑↑↑↑ */
    /* ↓↓↓↓测试代码↓↓↓↓ */
    
    it('设置tab页title#setTitle', () => {
        // 这个测试不是个好的测试,测试代码更改了页面DOM结构
        const originalTitle = document.title; // 保存原始title,测试结束后恢复
        const clock = sinon.useFakeTimers()
        setTitle('test title');
        clock.tick(1000)
        expect(document.title).to.equal('test title')
    
        setTitle(originalTitle); // 恢复原本的title
        clock.tick(1000)
        clock.restore()
    })
    
React 组件单元测试

React组件的测试本质上和其他单元测试区别不大,但是React 组件单元测试由于涉及到“浏览器渲染”问题,在复杂度上高很多,并且React组件除基础UI组件(如Button、Link)等底层组件外,其实很难保持其“规模”在“最小测试单元”这一范围内,虽然可以通过“浅渲染”(不渲染子组件)降低组件的复杂度。从单元测试的范畴和意义出发来看,其实大多数UI组件的测试很难归入单元测试中。由于“大家貌似都觉得理所应当”,本文档依然将UI组件测试作为单元测试介绍。

没有打开盒子时,我们无法决定猫是否还活着。

React并非一个黑盒,因此想要测试React,必须先了解React是如何渲染的。当然,咱们不需要去了解React的源码,知道个大概就好了。

简单来讲,一个React组件渲染到页面中需要下面几步。

  1. 根据状态计算出虚拟DOM对象
  2. 根据虚拟DOM生成真实的Dom结构
  3. 挂载Dom到页面中

但是在测试中,我们不需要挂载组件到页面中,只需要有一个Dom环境即可,如果你不需要完整的渲染组件甚至可以没有Dom环境。

Enzyme

Enzyme(酶,催化剂)是Airbnb公司提供的React 测试工具,提供了Shallow Rendering(浅渲染) | Full Rendering (全渲染)| Static Rendering (静态渲染)几种方式来渲染方式来测试React组件,一般常用浅渲染的方式进行测试。

使用Enzyme进行测试前需要先初始化React适配器,具体配置请查看官方手册

  • Shallow Rendering

    只渲染组件最外层结构,不会渲染子组件

    一个完整的Shallow渲染测试的实例:

    import * as React from 'react'
    import * as classnames from 'classnames'
    import { ClassName } from '../helper'
    import * as styles from './styles.desktop.css'
    
    const AppBar: React.StatelessComponent<any> = function AppBar({ className, children, ...props } = {}) {
        return (
            <div className={classnames(styles['app-bar'], ClassName.BorderTopColor, className)} {...props}>{children}</div>
        )
    }
    
    export default AppBar
    
    import * as React from 'react';
    import { shallow } from 'enzyme';
    import { expect } from 'chai';
    import AppBar from '../ui.desktop'
    
    describe('ShareWebUI', () => {
        describe('AppBar@desktop', () => {
            describe('#render', () => {
                it('默认渲染', () => {
                    shallow(<AppBar></AppBar>) 
                });
                it('正确渲染子节点', () => {
                    const wrapper = shallow(<AppBar><div>test</div></AppBar>)
                    expect(wrapper.contains(<div>test</div>)).to.be.true
                });
                it('允许设置ClassName', () => {
                    const wrapper = shallow(<AppBar className='test'></AppBar>)
                    expect(wrapper.hasClass('test')).to.be.true
                });
                it('允许设置其他自定义props', () => {
                    const wrapper = shallow(<AppBar name='name' age={123}></AppBar>)
                    expect(wrapper.props()).to.be.include({ name: 'name', age: 123 })
                });
            });
        });
    });
    
  • Full Rendering

    渲染组件真实的Dom结构,如果需要测试原生事件,则必须使用这种渲染方式。

  • Static Rendering

    类似于爬虫拿到的结果,拿到页面的HTML字符串,使用Cheerio进行操作。

Enzyme的浅渲染模式下的事件模拟并非是真的事件触发,实际上它是一种“障眼法”实现,例如buttonWrapper.simulate(‘click’)只是简单了调用传给Button组件的onClick参数的那个函数而已。

具体描述:airbnb.io/enzyme/docs…

Enzyme的很多坑:airbnb.io/enzyme/docs…

如果在异步操作中调用setState(),则测试时需要在下一个时钟周期中进行断言(类似的问题较多):

> /* 模拟点击 */
> wrapper.find('UIIcon').simulate('click')
> expect(wrapper.state('loadStatus')).to.equal(1) // 点击立即改变加载状态为正在加载
> /* 在promise.then 中设置setState,因此需要在下一个时钟进行断言 */
> setTimeout(() => {
>     expect(wrapper.state('loadStatus')).to.equal(2)
>     done()
> }, 0)
>

集成测试(Integration Testing)

集成测试是指在单元测试的基础上,将已测试过的单元测试函数进行组合集成暴露出的高层函数或类的封装,对这些函数或类进行的测试。

集成测试最大的难点就是颗粒度较大,逻辑更加复杂,外部因素更多,无法保证测试的可控和独立性。解决方式是使用测试桩(测试替身),即将调用的子函数或模块替换掉,即可以隐藏子模块的细节并且可以控制子模块的行为以达到预期的测试。(这里的前提是子模块已经经过完整的单元测试进行覆盖,因此可以假定为子模块状态可知。)

Typescript编译成Commonjs:

// 编译前
import * as B from 'B'
import {fn} from 'C'

export function A() { 
    B.fn.name()
    fn.name()
}

export class A1 {
    
}

// 编译后
define(["require", "exports", "B", "C"], function (require, exports, B, C_1) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    function A() {
        B.fn.name();
        C_1.fn.name();
    }
    exports.A = A;
    var A1 = /** @class */ (function () {
        function A1() {
        }
        return A1;
    }());
    exports.A1 = A1;
});

导出的所有函数和类都在exports对象下,由于在调用时其实是调用的exports.exportName,如果希望Stub则需要Stub exports下的属性即可,Sinon提供的stub功能允许我们修改一个对象下的任意属性,注意必须是在一个对象下,也就是说不能直接sinon.stub(fn),只能sinon.stub(obj,'fnName')。而在ES6中可以通过import * as moduleName from ‘moduleName ’ 将整个模块导出到单个对象上,就可以解决Stub的问题。

考虑有下面的模块依赖:

其中模块A依赖模块B和C,模块B又依赖模块C,这种情况下我们可以选择只Stub模块C,这时候在模块B中的C模块也同样会被Stub影响,最好的方式是同时Stub模块B和模块C。

煎蛋的栗子
import { clientAPI } from '../../../clientapi/clientapi';

/**
 * 通过相对路径获取缓存信息
 * @param param0 参数对象
 * @param param0.relPath 相对路径
 */
export const getInfoByPath: Core.APIs.Client.Cache.GetInfoByPath = function ({ relPath }) {
    return clientAPI('cache', 'GetInfoByPath', { relPath });
}

/* ↑↑↑↑被测函数↑↑↑↑ */
/* ↓↓↓↓测试代码↓↓↓↓ */

import { expect } from 'chai'
import { stub } from 'sinon'
import * as clientapi from '../../../clientapi/clientapi';

import { getInfoByPath, getUnsyncLog, getUnsyncLogNum } from './cache'

describe('cache', () => {
    beforeEach('stub', () => {
        stub(clientapi, 'clientAPI')
    })
    afterEach('restore', () => {
        clientapi.clientAPI.restore()
    })

    it('通过相对路径获取缓存信息#getInfoByPath', () => {
        getInfoByPath({ relPath: 'relPath' })
        expect(clientapi.clientAPI.args[0][0]).to.equal('cache') // 请求资源正确
        expect(clientapi.clientAPI.args[0][1]).to.equal('GetInfoByPath') // 请求方法正确
        expect(clientapi.clientAPI.args[0][2]).to.deep.equal({ relPath: 'relPath' }) // 请求体正确
    })
})

或者使用Sinon提供的Sandbox,在restore时更加简单,不需要单独restore每一个被Stub的对象。

import { rsaEncrypt } from '../../util/rsa/rsa';
import { getNew} from '../apis/eachttp/auth1/auth1';
/**
 * 认证用户
 * @param account {string}
 * @param password {string}
 */
export function auth(account: string, password: string, ostype: number, vcodeinfo?: Core.APIs.EACHTTP.Auth1.VcodeInfo): Promise<Core.APIs.EACHTTP.AuthInfo> {
    return getNew({
        account,
        password: encrypt(password),
        deviceinfo: {
            ostype: ostype
        },
        vcodeinfo
    });
}

/* ↑↑↑↑被测函数↑↑↑↑ */
/* ↓↓↓↓测试代码↓↓↓↓ */
import { createSandbox } from 'sinon'
import * as auth1 from '../apis/eachttp/auth1/auth1'
import * as rsa from '../../util/rsa/rsa'
const sandbox = createSandbox()
describe('auth', () => {
    beforeEach('stub',()=>{
        sandbox.stub(rsa,'rsaEncrypt')
        sandbox.stub(auth1,'getNew')
    })
    
    afterEach('restore',()=>{
        sandbox.restore()
    })
    
    it('认证用户#auth', () => {
        auth('account', 'password', 1, { uuid: '12140661-e35b-4551-84cf-ce0e513d1596', vcode: '1abc', ismodif: false })
        rsa.rsaEncrypt.returns('123') // 控制返回值
		expect(rsa.rsaEncrypt.calledWith('password')).to.be.true
        expect(auth1.getNew.calledWith({
            account: 'account',
            password: '123',
            deviceinfo: {
                ostype: 1
            },
            vcodeinfo: {
                uuid: '12140661-e35b-4551-84cf-ce0e513d1596',
                vcode: '1abc',
                ismodif: false
            }
        })).to.be.true
    })
}

参考文档:

端到端测试(E2E Testing)

端到端测试是最顶层的测试,即完全作为一个用户一样将程序作为一个完全的黑盒,打开应用程序模拟输入,检查功能以及界面是否正确。

端到端测试需要解决的一些问题:

  • 环境问题

    即如何保证每次执行测试前的环境是“干净的”,比如需要检查列表为空的表现,如果上一次测试新增了列表,则后一次测试将无法得到列表为空的状态。

    最简单的解决方式是在所有测试执行前或测试执行后调用外部脚本清除数据库等,或者可以通过拦截请求并自定义响应的方式来解决(这样会导致测试复杂度变高,并且不够”真实“)。

  • 元素查找

    如果代码经常变动,组件结构经常变化,如果根据DOM结构来查找操作元素,那么你将陷入维护选择器的地狱中。最佳实践是使用test-id的方式,但是这种方式需要开发人员和测试人员配合,在可操作元素上定义语义化的test-id。

  • 操作等待

    诸如异步网络请求导致界面变化,或界面动画等,将使得获取操作元素的时机未知。解决方案持续等待直到监听的请求完成,期望的元素成功获取到。

  • 使用操作而不是断言

    应该更多的依赖操作,而不是依赖断言。例如如果某个操作依赖元素A存在,你不需要”判断元素A在页面中是否存在”,而应该去”直接获取元素A,并操作”,因为如果元素A不存在,那么肯定将获取不到,断言后的操作将没有意义,因此可以直接使用操作取代断言等待功能。

参考端到端测试文档