articles icon indicating copy to clipboard operation
articles copied to clipboard

利用端到端测试实现自动批改作业

Open zhengguorong opened this issue 4 years ago • 1 comments

背景

目前就职的培训机构,老师会给学生布置每日作业或者阶段考试。检查学生作业时非常耗时的,你要不断的打开学生完成的页面,验证功能的完整性,例如一个涉及到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快速整合的能力,它会自动在我们的测试文件自动注入pagebrowser 对象,你只需要调用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测试结果一致 image

zhengguorong avatar Aug 05 '20 05:08 zhengguorong

老师牛逼

cbb-git avatar Aug 20 '20 10:08 cbb-git