网站首页 > 技术文章 正文
前端自动化测试
“测试”一个让程序员爱恨交加的阶段,每每提到“测试”,伴随着的总是“BUG”、“缺陷”这些名词。那测试又是什么人来做呢?程序员应该写测试吗?又应该如何做好测试呢?
什么是测试?
在规定的条件下对程序进行操作,以发现程序错误,衡量软件质量,并对其是否满足设计要求进行评估的过程。换句话说就是,测试是一种实际输出与预期输出间的审核或者比较过程。
为什么要做测试?
刚才也说到了,测试时比较实际输出与预期输出的过程。在这个过程中我们可以:
- 1、保证最终成果符合预期
- 2、提高代码的质量
- 3、帮助需求参与者重新梳理流程
- 4、准确定位问题
- 5、提升开发者信心和安全感
这是产品上线前的最后一道关卡,如果能这个环节严格把关,及时发现错误,提高代码质量,就能尽量得减少线上的故障率,以及代码的整洁度。
谁来做测试?
在学习前端自动化测试之前我们首先需要搞清楚这个问题。一问到这个问题,90%的人都第一反应都是测试同学进行测试。测试同学应该做测试,这是没错的,但是测试真的仅仅只是测试同学的事吗?
我们仔细想一下,程序员开发出来的软件,谁是第一位接触者。在我们每次提测前,是不是都会自己先把代码跑一遍,至少保证提交的代码基本的功能是正常的,这个其实就是最最基本的一个测试,但是我们通常不怎么当回事。
我们平时可以观察一下测试同学是如何做测试?他们更多的是通过输入A,然后观察是否能够得到结果B,但是对于代码内部的运行逻辑是无法考察的,测试的同学只能从外部的现象来保障正确性,他们没法去review我们的代码,因此所能达到的效果是有限的。那么谁有能填补上这一个空缺的呢——开发者自己。
但是很多又会说,自己开发自己测,你这不是“掩耳盗铃”吗?那就要说到测试模型和开发如何编写测试用例了。
测试模型:蛋卷和金字塔
我在测试时通常按照不同的层次进行划分:将测试分成关注最小程序模块的单元测试、将多个模块组合在一起的集成测试,将整个系统组合在一起的系统测试。
蛋卷模型
有一种直接的做法是,既然更加高层的测试覆盖面越广,那么我们就得写更多的高层测试,比如大量的系统测试,这样就能大范围的覆盖掉底层测试。当然依然有一些底层测试是覆盖不了的,所以我们又单独去写单元测试进行补充,这样我们就会形成这样一个模型:冰淇淋蛋卷
但是这种费时费力,有时一个复杂系统的测试链路是异常麻烦的,这时候我们需要的是换一种思路。
金字塔模型
我们可以看出金字塔模型对比与蛋卷模型来说,是完全反过来的。它有大量的单元测试,然后越往上越少。这时候我们就需要思考为什么会有这种模型了?越是底层的测试,涉及到的逻辑越少,越好写,反而更高层测试涉及得更广,逻辑更复杂。
但是这种模型依然存在问题,因为顶层的系统测试是由基础的单元测试逐步构建成的,但是在实际的工作中,如果底层的模块做了调整,都有可能破坏高层测试,所以高层测试通常是相对脆弱的。
在我们实际测试过程中,高层测试的测试量不会太多,测试覆盖率无论如何都上不来,并且一旦测试错误,想要通过测试结果溯源是非常麻烦的,因为我们的是测试环节多且复杂难以准确定位问题。
如何编写测试
讲了那么多关于测试概念,那开发又应该如何写测试呢?下面我就用一个React项目来介绍一下。
事例项目链接:github.com/Pengjee/pul…
搭建测试环境
create-react-app jest-demo --template-typescript
复制代码
或者直接偷懒,拉事例项目的源码也行。
官方事例讲解
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
// 获取DOM节点
render(<App />);
// 获取屏幕中文本内容为‘learn react’的节点
const linkElement = screen.getByText(/learn react/i);
// 判断节点是否在App中
expect(linkElement).toBeInTheDocument();
});
复制代码
简单组件
下面这是一个简单的计数器的模块代码
const TestDemo = () => {
const [num, setNum] = useState(0);
return <div>
<p>{ num }</p>
<button onClick={() => setNum(num + 1)}>增加</button>
<button onClick={() => setNum(num - 1)}>减少</button>
</div>
}
复制代码
那么先想一下需要哪些测试case呢?
1、初始值为0
2、点击 “增加按钮”,p标签内变为1
3、点击“减少按钮”,p标签内变为0
我们用上面的测试cast来写一下测试脚本:
import React from 'react'
import { render, screen, RenderResult } from '@testing-library/react';
import TestDemo from './TestDemo';
let wrapper: RenderResult
// 每次进入新的测试用例都重新渲染一次组件
beforeEach(() => {
wrapper = render(<TestDemo />)
})
describe('计数器测试', () => {
test('测试初始值是否为0', () => {
// 获取id是num的dom节点
const numDom = wrapper.getByTestId('num')
// 判断dom节点是否在dom树中中
expect(numDom).toBeInTheDocument();
// 判断节点是否为p标签
expect(numDom.tagName).toEqual('P')
// 判断节点内容是否为0
expect(numDom.textContent).toEqual('0')
});
test('测试增加(减少)逻辑是否为准确', () => {
const numDom = wrapper.getByTestId('num')
// 获取增加按钮的dom节点
const increaseDom = wrapper.getByText(/增加/i)
// 判断节点内容是否为0
expect(numDom.textContent).toEqual('0')
// 触发点击事件
fireEvent.click(increaseDom)
// 判断节点值是否为1
expect(numDom.textContent).toEqual('1')
const decreaseDom = wrapper.getByText(/减少/i)
// 触发点击事件
fireEvent.click(decreaseDom)
// 判断节点值是否为0
expect(numDom.textContent).toEqual('0')
})
})
复制代码
这样一个稍微复杂一点的测试用例就写好了,我们可以发现,在测试用例中我们把每一步都拆解得特别的细,因为我们无法保证我的测试用例写得没有问题,总不可能再写一个测试去测试这个测试用例没有问题吧,既然无法用写程序的方式保证测试的正确性,那我只有一个办法:把测试写简单,简单到一目了然,不需要证明它的正确性。
组件测试
import React, { useState } from 'react'
import { render, fireEvent, screen, act } from '@testing-library/react'
import PullToRefresh from '../index'
const Demo = (props) => {
const [loading, setLoading] = useState(false);
const handleRefresh = () => {
setLoading(true)
setTimeout(() => {
setLoading(false)
}, 1000)
}
return <PullToRefresh
loading={loading}
onRefresh={handleRefresh}
data-testid="pull-to-refresh"
successText="更新成功"
{...props}
>
<div style={{ height: '100vh' }} data-testid="content">
测试下拉
</div>
</PullToRefresh>
}
describe('Basic Case', () => {
it('Render success', () => {
const { getByTestId } = render(<Demo />)
expect(() => getByTestId('content')).not.toThrow(/Unable to find PullToRefresh/)
})
it('Pulling Status', () => {
const { getByTestId } = render(<Demo />)
const contentDom = getByTestId('content')
fireEvent.touchStart(contentDom, { touches: [{ clientX: 0, clientY: 0 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 10 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 20 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 30 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 40 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 49 }] })
expect(screen.queryByText('下拉更新')).not.toBeNull()
})
it('Loosing Status', () => {
const { getByTestId } = render(<Demo />)
const contentDom = getByTestId('content')
fireEvent.touchStart(contentDom, { touches: [{ clientX: 0, clientY: 0 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 10 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 20 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 30 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 40 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 50 }] })
expect(screen.queryByText('松开更新')).not.toBeNull()
})
it('Loading Status', () => {
const { getByTestId } = render(<Demo />)
const contentDom = getByTestId('content')
fireEvent.touchStart(contentDom, { touches: [{ clientX: 0, clientY: 0 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 10 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 20 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 30 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 40 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 51 }] })
fireEvent.touchEnd(contentDom)
expect(screen.queryByText('更新中...')).not.toBeNull()
})
it('Success Status', async () => {
jest.useFakeTimers();
const wrapper = render(<Demo />)
const contentDom = wrapper.getByTestId('content')
fireEvent.touchStart(contentDom, { touches: [{ clientX: 0, clientY: 0 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 10 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 20 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 30 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 40 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 51 }] })
fireEvent.touchEnd(contentDom)
// 由于改变了state,所以需要重新获得渲染后的结果,确保在进行任何断言之前,与这些“单元”相关的所有更新都已处理并应用于 DOM
act(() => {
jest.advanceTimersByTime(1000);
});
expect(screen.queryByText('更新成功')).not.toBeNull()
})
})
describe('Props Case', () => {
it("Change 'disabled' props", () => {
const { getByTestId } = render(<Demo disabled />)
const contentDom = getByTestId('content')
fireEvent.touchStart(contentDom, { touches: [{ clientX: 0, clientY: 0 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 10 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 20 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 30 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 40 }] })
expect(screen.queryByText('下拉更新')).toBeNull()
})
it("Change 'text' props", () => {
jest.useFakeTimers();
const { getByTestId } = render(<Demo
successText="测试成功文本"
pullingText="测试下拉文本"
loosingText="测试松开文本"
loadingText="测试加载中文本"
/>)
const contentDom = getByTestId('content')
fireEvent.touchStart(contentDom, { touches: [{ clientX: 0, clientY: 0 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 10 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 20 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 30 }] })
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 40 }] })
expect(screen.queryByText('测试下拉文本')).not.toBeNull()
fireEvent.touchMove(contentDom, { touches: [{ clientX: 0, clientY: 50 }] })
expect(screen.queryByText('测试松开文本')).not.toBeNull()
fireEvent.touchEnd(contentDom)
expect(screen.queryByText('测试加载中文本')).not.toBeNull()
act(() => {
jest.advanceTimersByTime(1000);
});
expect(screen.queryByText('测试成功文本')).not.toBeNull()
})
});
- 上一篇: 从一次故障聊聊前端 UI 自动化测试
- 下一篇: WEB前端线上系统课(20k+标准)
猜你喜欢
- 2025-05-22 一天涨 23k Star 的开源项目「GitHub 热点速览」
- 2025-05-22 如何选择VueJS的两个API Composition API或者Options API
- 2025-05-22 「评测」 声色——海贝 Crystal6 多单元动铁耳机
- 2025-05-22 常用的七种性能测试
- 2025-05-22 接口测试及其测试流程
- 2025-05-22 Java开发中的自动化测试框架:从零开始玩转测试工具
- 2025-05-22 别克君越1.5t机电单元维修
- 2025-05-22 前端代码Review,一次性掰扯明白!
- 2025-05-22 C++语言的单元测试与代码覆盖率
- 2024-09-22 vue入门:使用mockjs搭建vue项目测试服务器
你 发表评论:
欢迎- 05-24网络信息安全之敏感信息在传输、显示时如何加密和脱敏处理
- 05-24常见加密方式及Python实现
- 05-24pdf怎么加密
- 05-24aes256 加密 解密 (python3) 「二」
- 05-24深入理解Python3密码学:详解PyCrypto库加密、解密与数字签名
- 05-24Springboot实现对配置文件中的明文密码加密
- 05-24JavaScript常规加密技术
- 05-24信息安全人人平等 谷歌推出低性能安卓手机加密技术
- 最近发表
- 标签列表
-
- 前端设计模式 (75)
- 前端性能优化 (51)
- 前端模板 (66)
- 前端跨域 (52)
- 前端缓存 (63)
- 前端react (48)
- 前端aes加密 (58)
- 前端md5加密 (49)
- 前端路由 (55)
- 前端数组 (65)
- 前端定时器 (47)
- 前端接口 (46)
- Oracle RAC (73)
- oracle恢复 (76)
- oracle 删除表 (48)
- oracle 用户名 (74)
- oracle 工具 (55)
- oracle 内存 (50)
- oracle 导出表 (57)
- oracle 中文 (51)
- oracle链接 (47)
- oracle的函数 (57)
- mac oracle (47)
- 前端调试 (52)
- 前端登录页面 (48)
本文暂时没有评论,来添加一个吧(●'◡'●)