前端工程质量保障:测试保障

62 阅读19分钟

在规定的条件下进行软件测试,并根据其结果来判断软件是否符合预期,可以发现程序的错误,衡量软件的质量。软件测试是保障软件工程的正确性、完整性、安全性和质量的手段。

1. 测试流程

1.1 单元测试

单元测试是对软件中的最小可测试单元进行检查和验证。这个最小单元通常是一个函数、一个方法或者一个类。其主要目的是确保目标函数在特定的输入条件下,输出的结果与开发人员的预期一致。它的作用是保障每个单元的功能正确性,独立于其他组件进行测试,方便在开发过程中快速定位问题。

单元测试的质量好坏可以通过代码覆盖率来判断,具体包括:

  • 语句覆盖率(Statement Coverage):衡量被执行到的语句数量占总语句数量的百分比。例如,一个函数中有 10 条语句,在单元测试中有 7 条语句被执行到了,那么语句覆盖率就是 70%。
  • 行覆盖率(Line Coverage):衡量被执行的代码行数占总代码行数的百分比。它与语句覆盖率十分接近,但在实际中还是有差异的,比如一行代码可能包含多条语句。
  • 函数覆盖率(Function Coverage):衡量被调用执行的函数数量占总函数数量的百分比。它的重点在于确保代码中的每个函数都能在测试过程中被执行,如果函数覆盖率较低,说明有部分功能可能没有被测试到,增加了潜在的风险。
  • 分支覆盖率(Branch Coverage):衡量代码中条件语句的分支被执行的百分比。它侧重于测试代码中的逻辑路径,能够更有效地发现代码中的逻辑错误,尤其是与条件判断相关的错误。

单元测试的代码覆盖率可以作为衡量项目质量的指标之一,但并不是全部。代码覆盖率100%只能保证代码的每个语句、分支都被运行个过了,不能确保所有执行结果都符合预期,还需要在不同场景下设计测试用例来尽量覆盖边界情况。

在实际中,前端页面逻辑往往经常变化,但是其背后依赖的组件和工具函数都是比较稳定的,开发人员可以通过单元测试提升它们的可靠性和稳定性。当项目重构的时候,单元测试可以有效保证代码重构后逻辑的一致性,大大减少在测试环节中投入的精力。

1.2 功能测试

功能测试主要关注软件的功能是否符合需求文档的要求,验证程序的运行结果是否符合预期。当开发人员研发完某个功能后,需要通过所有冒烟测试。当所有主流程都“跑通”后,就会由测试人员进行功能测试。功能测试可以发现页面中的功能遗漏、功能错误及样式错误等问题,从而避免将这些问题带到生产环境。

在进行功能测试之前,测试人员会根据产品的需求文档和页面设计稿编写相关的测试用例,然后组织该功能模块相关的产品经理和开发人员共同进行测试用例评审,确保测试用例能有效覆盖大部分场景。

除了业务逻辑相关的测试,功能测试还包括浏览器兼容性数据交互页面展示等方面的测试。

  • 浏览器兼容性:确保页面在要求的浏览器版本中都能正常显示和使用
  • 数据交互:保证页面的数据交互逻辑正常。比如用户修改数据时可能会重复点击提交按钮,需要避免数据重复提交
  • 页面展示:确保应用的用户界面(UI)符合设计要求并且易于使用。这包括检查页面元素的布局、颜色、字体、图标等是否正确显示,以及页面的响应式设计是否能在不同设备屏幕尺寸下正常工作。

功能测试是测试流程中耗时最多且最重要的环节,它确保了功能的基本可用性。开发人员不能完全依赖测试人员,自己也要做好功能测试。

1.3 集成测试

集成测试是在功能测试的基础上,将所有功能子模块合并成一个模块,再进行整体的测试,验证它们组合在一起时是否能够正确工作。

  • 从研发的角度来看,集成测试就是将所有的功能分支合并成一个集成分支,然后部署到测试环境中进行测试。
  • 从测试人员的角度来看,集成测试除了要将功能测试的测试用例再执行一遍,还要对涉及跨功能模块交互的地方重点进行回归测试,保证本次涉及的功能模块的主流程没有阻塞性问题。

1.4 端到端测试

端到端测试是从用户的实际使用场景出发,对整个软件系统进行的全面测试。它模拟用户的完整操作流程,从软件的入口点开始,到最终输出结果,涵盖了软件系统的各个层面,包括前端后端数据库以及它们之间的交互。

根据测试的方向不同,端到端测试可以分为水平测试垂直测试

  • 水平端到端测试:模拟用户在系统的同一层次或功能模块之间的操作流程,能够快速发现同一层次功能模块之间的协作问题,例如页面跳转错误、数据在页面间传递丢失等问题。为了排除测试环境的干扰,一般会选择在生产环境下进行灰度发布,使用生产环境的数据进行测试,当所有测试通过后再进行灰度放量和全量发布。
  • 垂直端到端测试:它涉及系统的多个层次,如从用户在前端界面发起请求,经过后端业务逻辑处理,再到数据库的存储和读取等完整的流程。这种测试更像是沿着系统的垂直方向,贯穿所有相关层次进行深度测试。它的实施成本比较高,通常由开发人员进行。

端到端测试将系统作为一个整体来进行测试,因此可以排查出各个模块依赖间的隐患。它要求测试人员对系统有整体的认知,熟悉整个业务的操作流程,从而有效发现测试过程中的问题。

2. 测试方式

2.1 白盒测试

白盒测试基于代码的测试,也称为结构测试逻辑驱动测试。它是基于对程序内部结构(代码逻辑、控制流、数据流等)的了解来设计测试用例。测试人员需要查看程序的源代码,分析程序的逻辑结构,包括语句、分支、循环、函数调用等,然后根据这些内部细节来设计测试用例,以确保程序的内部逻辑能够正确执行。

白盒测试的方法包括静态分析动态分析两大类。

  • 静态分析:在不执行程序代码的情况下,对程序的源代码进行检查和分析。其主要目的是发现程序中的语法错误、结构缺陷、潜在的逻辑问题以及不符合编码规范的地方。例如,使用lint工具检测代码是否符合规范,通过代码评审排查代码存在的问题,使用tsc扫描类型问题等。通过对代码的静态结构进行审查,能够在早期阶段发现一些较为明显的问题,从而提高代码质量。

  • 动态分析:通过编写测试用例执行要测试的代码,判断结果是否符合预期。动态分析方法主要包括6种测试方法,分别是语句覆盖判定覆盖条件覆盖分支覆盖分支组合覆盖路径覆盖。其中路径覆盖考虑程序中所有可能的执行路径,发现错误的概率最高。

白盒测试只能确保代码的语法和逻辑正常,对代码的分支路径进行覆盖测试并不能确保功能的正确性。

2.2 黑盒测试

黑盒测试基于功能的测试,又称为功能测试数据驱动测试。它把程序看作一个黑盒子,不考虑程序内部的逻辑结构和实现细节,只关注程序的输入和输出。测试人员根据需求文档,设计测试用例来验证软件的功能是否符合预期。

从理论上来讲,黑盒测试只有穷举输入测试,才能查出程序中的所有测试。实际上,测试情况有无穷多种,包括合法的输入和不合法的输入,因此不可能穷举测试,要进行有针对性的测试。

黑盒测试行为需要能够被量化,才能真正保证软件质量,而测试用例就是将测试行为量化的方法之一。测试人员可以使用等价类划分法边界值分析法错误推测法三种常用的方法来设计测试用例,从而尽可能覆盖主要的测试情况。

  • 等价类划分法:将程序的输入域划分为若干个等价类,如果一个测试用例在某个等价类中能够正确执行,那么可以合理地推测这个等价类中的其他数据也能使程序正确执行。等价类通常可以划分为有效等价类无效等价类。比如要求用户名是长度为1-10 位,那么所有满足这个条件的用户名就是一个有效等价类,不满足条件的输入可以归为无效等价类

  • 边界值分析法:主要关注输入或输出的边界情况。边界值是指输入域或输出域边界上的值或者边界附近的值,比如最大最小值、略小于最小值、略大于最大值等。因为程序在处理边界值时往往更容易出现错误,因此针对各种边界情况设计测试用例可以查出更多错误。

  • 错误推测法:基于测试人员的经验、直觉和对程序可能出现错误的推测来设计测试用例。比如,在页面提交数据时多次点击确定按钮、超长文本展示是否会溢出等情况,都是前端页面中经常遇到的异常情况,可以有针对性地结合这些情况编写测试用例。

黑盒测试不仅包括软件的正确性、可用性等功能测试,也包括非功能性测试,例如兼容性测试、性能测试、压力测试和安全性测试等。

黑盒测试站在用户的角度,从输入数据和输出数据的关系出发进行测试,能更好、更真实地从用户角度考察被测系统的功能性需求实现情况。

3. 测试手段

3.1 手工测试

手工测试指测试人员手动执行测试用例,通过手工操作软件的用户界面或其他接口,输入测试数据,并观察和记录软件的输出结果来进行测试。这种测试方式依赖于测试人员的操作和判断,具有很强的灵活性。

手工测试的优点在于门槛较低,测试人员熟悉产品设计文档后,就可以快速进行测试。它的缺点在于测试过程中存在很多重复的回归测试工作,会浪费大量时间,并且测试质量很大程度上依赖测试人员的能力。手工测试是最原始的测试手段,但它依然是目前软件测试应用最多、发现问题最有效的手段。

3.2 自动化测试

自动化测试是通过编写脚本或使用自动化测试工具,让计算机自动执行测试用例。它的目的是节省人力、时间或硬件资源,从而提高测试效率。

在实施自动化测试之前,需要对测试对象进行分析,判断其是否适合进行自动化测试。判断依据主要有三点:

  • 需求变更不能太频繁:频繁的需求变更会增加自动化测试的维护成本,导致自动化测试的投资回报率降低。可以对项目中的相对稳定的模块可以进行自动化测试,而对变动较大的模块继续采用手工测试。

  • 测试对象生存周期长:自动化测试的前期投入较大,用例编写和开发也需要一定的时间。如果测试对象的生命周期比较短,就没必要浪费时间做自动化测试。比如各种营销页面,只会在活动期间使用,这种功能模块就没必要进行自动化测试。

  • 自动化测试的脚本可重复利用率高:高可重复利用率的脚本可以提高自动化测试的效率和效益。

前端的自动化测试手段主要包括工具函数单元自动化测试UI自动化测试两类。

  • 工具函数单元自动化测试:编写单元测试用例,以代码覆盖率来判断测试的质量情况
  • UI 自动化测试:对功能变更前后的页面进行对比测试以判断变化情况,常用的手段有像素对比DOM diff等。

自动化测试虽然有很多优点,但并不难完全取代手工测试。手工测试往往能发现一些自动化测试发现不了的问题;另外,完全使用自动化测试代替手工测试也存在成本问题,尤其是对于快速迭代的页面。

4. 常用测试工具

4.1 Jest

Jest 是一个流行的 JavaScript 测试框架,主要用于单元测试。它由 Facebook 开发并开源,被广泛应用于 React 项目,但也适用于其他 JavaScript 项目。Jest 内置了常用的测试工具,真正做到了开箱即用.

Jest采用了简洁的测试结构,通过describe函数来组织测试套件,用it(或test)函数来定义单个测试用例。

describe('MathUtils', () => {
  it('should add two numbers correctly', () => {
      const add = (a, b) => a + b;
      expect(add(2, 3)).toBe(5);
  });
});

Jest还提供了一套简单易用的 API,使得编写和运行测试用例变得方便快捷。比如用于测试用例不同阶段的全局API:

  • beforeAll:在一个测试套件中的所有测试用例开始执行之前运行一次。通常用于进行一些一次性的设置操作,比如初始化数据等。
  • beforeEach:在每个测试用例执行之前运行。它主要用于设置每个测试用例所需的初始状态,例如重置对象的属性、清除缓存等。因为每个测试用例应该是相互独立的,beforeEach可以确保每个测试用例在相同的初始条件下开始执行。
  • afterAll:与beforeAll相对应,它会在所有测试用例执行完毕后运行一次。主要用于清理资源,如停止模拟服务器等操作。这样可以确保测试结束后不会遗留任何可能影响其他测试或者系统的资源占用情况。
  • afterEach:与beforeEach相对应,它会在每个测试用例执行完毕后运行。用于清理每个测试用例可能产生的临时资源或者状态变化,比如删除测试过程中创建的临时文件、恢复被修改的对象状态等。

Jest内置了很多测试功能,比如:

  • 断言库:提供语义化方法,对参与测试的值做各种判断,如toBetoEqualtoBeTruthy等。这些断言方法可以方便地验证函数返回值、变量值等是否符合预期。
  • 模拟(Mock)功能:能够模拟函数、模块等,这在测试中非常有用。通过模拟 API 的返回值或者其他依赖模块的值,可以隔离外部因素,专注于测试函数内部的逻辑。
    import { getUserInfo } from './api.js';
    // 使用jest.mock模拟整个模块,指定模拟的实现
    jest.mock('./api.js', () => ({
        getUserInfo: jest.fn(() => Promise.resolve({
            name: 'Mocked User',
            age: 25
        }))
    }));
    
    describe('API Tests', () => {
        it('should return mocked user info', async () => {
            const result = await getUserInfo();
            expect(result).toEqual({
                name: 'Mocked User',
                age: 25
            });
            // 也可以验证函数是否被调用
            expect(getUserInfo).toHaveBeenCalled();
        });
    });
    
  • 快照测试(Snapshot Testing) :这是 Jest 的一个特色功能,可用于测试各种UI组件。它可以对组件(特别是 React 组件)的渲染输出或者其他数据结构进行快照,然后在后续测试中比较快照是否发生变化。如果快照发生了变化,这可能意味着出现了错误。比如:
    import React from 'react';
    import renderer from 'react-test-renderer';
    import MyComponent from './MyComponent';
    
    test('MyComponent snapshot', () => {
        const component = renderer.create(<MyComponent />).toJSON();
        expect(component).toMatchSnapshot();
    });
    

4.2 Cypress

Cypress 是一个专门用于前端应用 UI 自动化测试的工具。它提供了一个直观的图形界面,并且可以直接在浏览器中运行测试,这使得测试过程更加可视化和易于理解。

Cypress测试执行流程如下:

  • 启动测试:首先在本地启动 Cypress 应用程序,它会自动打开一个浏览器窗口,创建一个iframe作为沙箱,加载要测试的 Web 应用页面。
  • 加载测试脚本Cypress 会加载测试脚本,这些脚本包含了一系列的测试用例,每个测试用例都是由一系列的 Cypress 命令组成的。例如,一个测试用例可能会先访问应用的首页,然后查找并点击一个登录按钮,接着输入用户名和密码并提交登录表单。
  • 命令执行:在测试脚本执行过程中,Cypress 命令会按照顺序在浏览器中执行。比如cy.visit('/')会告诉浏览器导航到应用的根目录;当执行cy.get('button')时,Cypress 会在浏览器的 DOM 中查找所有的button元素,并返回一个包含这些元素的对象,后续可以通过click操作来模拟用户点击按钮。在整个测试过程中,Cypress 会完全接管浏览器的控制权,因此可以对页面进行各种自定义操作,从而精准地执行自动化脚本并得到稳定可靠的测试结果。
  • 断言验证:在执行操作的同时,Cypress 还支持各种断言来验证操作的结果是否符合预期。例如,在登录操作后,可以使用cy.url().should('include', '/dashboard')来断言页面的 URL 是否包含/dashboard,以此来确定登录是否成功并跳转到了正确的页面。
  • 测试结束:当一个测试用例中的所有命令都执行完毕并且所有断言都通过时,这个测试用例就成功完成。Cypress 会继续执行下一个测试用例,直到所有测试用例都执行完毕。如果断言失败,Cypress 会停止当前测试用例的执行,并在测试报告中记录错误信息。

Cypress作为一款优秀的自动化测试工具,可以以下几个方面帮助开发人员提升自动化测试的体验:

  • 时间旅行Cypress在测试过程中会进行快照记录。用户可在 Cypress 提供的 Test Runner 里,通过鼠标悬停在命令上查看运行时每一步都发生了什么。这对于调试复杂的测试用例非常有帮助,通过日志快照可以精确地查看是在哪一个步骤出现了问题。

  • 可调试性:在测试运行过程中,开发人员可以直接在浏览器的开发者工具中进行调试。它能够暂停测试执行,查看变量的值、DOM 元素的状态以及网络请求等信息。当测试运行失败时,可以在开发者工具中根据错误堆栈记录快速排查问题。

  • 实时刷新:当测试代码或者被测试的应用代码发生变化时,测试可以自动重新加载并运行。

  • 自动等待Cypress 会自动等待元素至可靠操作状态时才执行命令,还会等命令和断言执行完成再执行下一个命令,无需手动处理等待。

  • 丰富的测试函数Cypress 提供了大量丰富的测试函数,涵盖了从简单的元素查找(如cy.get用于获取元素)、操作(如cy.click用于点击元素、cy.type用于输入文本)到复杂的断言(如cy.should用于验证各种条件)等各个方面。这些函数简单易懂,开箱即用。

  • 网络流量控制Cypress代理了网络请求,可以实现网络流量控制,以及mock网络请求的返回结果。

  • 结果一致性Cypress 不依赖 SeleniumWebDriver,它的性能更好、质量更稳定,得到的测试结果更有保障。

  • 支持截图和视频Cypress 能够在测试过程中自动截图和录制视频。当测试失败时,这些截图和视频可以作为重要的参考资料,帮助开发人员快速了解测试失败时的场景。

除了Cypress,开发人员还可以使用PuppeteerSelenium等工具进行自动化测试,这些工具各有千秋,可以根据实际情况灵活选用。

4.3 LambdaTest

LambdaTest 是一个云端的跨浏览器测试平台,它提供了大量的真实浏览器环境和移动设备环境,用于测试 Web 应用和移动应用。通过 LambdaTest,开发者可以在云端的各种设备、浏览器和操作系统组合上运行自动化测试和手动测试,从而对 Web 应用和移动应用进行全面的兼容性测试。