articles
articles copied to clipboard
利用端到端测试实现自动批改作业
背景
目前就职的培训机构,老师会给学生布置每日作业或者阶段考试。检查学生作业时非常耗时的,你要不断的打开学生完成的页面,验证功能的完整性,例如一个涉及到ajax请求的增删改查功能,平均需要花费5分钟检查一个,一个班平均50人每次检查作业就要250分钟,如果持续做这件事情,会耗费太多时间。
问题分析
检查作业的目的就是验证功能的完整性,那在企业中验证功能的完整性,一般会利用单元测试或者e2e测试验证。单元测试多用于对函数或者代码块的验证,在该场景中不适用,因为作业中包含html页面,页面中包含js逻辑,所以e2e测试会更适合该场景的适用。我们可以把学生的代码当成黑盒子,只验证最终实现效果即可。
解决方案
e2e测试框架有很多,但是由于我以前只使用过jest编写单元测试,对jest的语法比较熟悉,所以这次编写e2e测试,也使用jest这个框架来实现。由于需要进行dom操作,jest提供了一个叫jest-puppeteer
的库用来处理dom,它其实是基于puppeteer做的二次封装,使用API和puppeteer差不多,但是做了一些优化。
jest.config.js配置运行环境
jest-puppeteer给我们提供了jest与puppeteer快速整合的能力,它会自动在我们的测试文件自动注入page
和browser
对象,你只需要调用page对象打开页面即可,省去不少麻烦事,当然如果你需要自行配置,也是可以的。
如果你需要让jest-puppeteer帮你配置,需要在根目录创建一个jest.config.js
文件,写入preset: "jest-puppeteer"
这句即可。
module.exports = {
preset: "jest-puppeteer",
testEnvironment: "./custom-environment.js"
};
testEnvironment自定义启动脚本
在运行测试前,往往需要一些准备工作,例如,我测试的内容是一些静态页面,而puppeteer只能访问http页面(可能访问本地路径也可以,没试过),我就需要在测试前启动一个静态服务,那我们可以利用setup
设置运行脚本前做的事情,teardown
设置结束脚本时做的事情。
const PuppeteerEnvironment = require('jest-environment-puppeteer')
const express = require('express');
class CustomEnvironment extends PuppeteerEnvironment {
async setup() {
const app = express();
app.use(express.static('英雄作业'));
this.server = app.listen(3000);
await super.setup()
}
async teardown() {
// Your teardown
this.server.close();
await super.teardown()
}
}
module.exports = CustomEnvironment
测试首页脚本
下面脚本演示首页功能的测试,首页包含列表和删除两个功能。每个功能的测试其实只是模拟人为的点击或输入,然后判定得到的行为是否符合预期。 写脚本的时候需要注意每个断言应该是无依赖的,否则容易因为移动代码而导致测试无法通过,你可以在执行断言前初始化数据,在测试完毕清理数据。
const axios = require('axios');
const { stringify } = require('querystring');
const fs = require('fs');
const path = require('path');
// 动态获取参数。例如jest ----runInBand --verbose --name='陈莲'
const name = require("minimist")(process.argv.slice(2))["name"]; // "陈莲"
const baseUrl = 'http://localhost:3000/' + name;
const mockData = {
name: 'test',
gender: '男',
img: 'http://test.com/test.png'
}
describe('英雄首页', () => {
beforeAll(async () => {
// 测试前准备环境,先给系统添加一个模拟数据
await axios.post('http://127.0.0.1:3001/addHero', stringify(mockData), {
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
});
await page.goto(baseUrl + '/index.html');
await page.waitFor(1000);
});
it('显示英雄列表', async () => {
// 判断是否正确循环数据
const herosElementLength = await page.evaluate(
() => document.querySelectorAll('tbody tr').length
);
expect(herosElementLength).toBe(1);
// 判断元素是否正确渲染出来
const bodyHTML = await page.evaluate(() => document.body.innerHTML);
expect(bodyHTML).toContain(mockData.name);
expect(bodyHTML).toContain(mockData.img);
expect(bodyHTML).toContain(mockData.gender);
});
it('删除英雄', async () => {
// 如果点击删除后,有提示框,可以通过监听dialog事件处,dialog.accpet方法用于模拟点击确定
page.on('dialog', async dialog => {
await dialog.accept();
});
await page.evaluate(() => {
var aTags = document.getElementsByTagName('a');
var searchText = '删除';
var found;
for (var i = 0; i < aTags.length; i++) {
if (aTags[i].textContent == searchText) {
found = aTags[i];
break;
}
}
found.click()
});
await page.waitFor(1000);
const res = await axios.get('http://127.0.0.1:3001/getHeroList')
const { data } = res.data;
expect(data.length).toBe(0);
});
afterAll(async () => {
// 注意测试完毕,需要清除数据,否则会导致影响下一个测试
fs.writeFileSync(path.join(__dirname, '../server/heimaHero.json'), '[]')
})
}, 20000);
运行测试脚本
jest ----runInBand --verbose --name='陈莲'
----runInBand jest为了提升执行测试用例的速度,会把不同的测试文件放在不同线程同时执行,但这如果你的测试是有依赖关系,不能同时执行的,可以加入这个参数。 --verbose 表示输出详细的测试结果 --name 是自定义参数
测试结果
截图显示,该同学完成了6个需求,1个需求没有通过。经过复查该同学代码,发现和我们jest测试结果一致
老师牛逼