测试是每个软件开发过程的关键部分。它可以大大降低项目成本,并可以提高团队的生产力。基本上有三种主要的测试类型:
- 单元测试—通过单元测试,可以测试代码中独立小片段,例如一个组件,一个函数。
- 集成测试—在这种类型的测试中,我们组合并测试各个单元,并将它们作为一个整体进行测试。
- 端到端(E2E)测试-定义为某些应用程序完整功能的测试。
这篇内容主要介绍使用Jest和Puppeteer 对react项目进行e2e测试。
我们将使用两个强大的工具Jest和Pupeeteer编写测试:
- Jest:是功能齐全的测试框架,由Facebook开发。它只需要很少的配置就可以直接使用。
- Puppeteer:Google创建的Node.js库,它提供了方便的API来控制Headless Chrome。
最后,我们需要使用jest-puppeteer preset,将这两个框架组合在一起。
安装工具
npm install puppeteer jest jest-puppeteer
配置
Jest Configuration
"preset": "jest-puppeteer"让我们可以将Jest与Puppeteer一起使用
{
"globals": {
"ts-jest": {
"diagnostics": false },
"URL": "http://192.168.199.56:3000/" },
"modulePaths": [ "<rootDir>" ],
"transform": { "^.+\\.tsx?$": "ts-jest" },
"setupFiles": [ "./test/e2e/setup.ts" ],
"moduleFileExtensions": [ "ts", "tsx", "js", "jsx" ],
"moduleDirectories": [ "node_modules" ],
"preset": "jest-puppeteer",
"testPathIgnorePatterns": [
"<rootDir>/test/unit",
"<rootDir>/src",
"<rootDir>/scripts" ],
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/test/__mocks__/fileMock.js",
"\\.(css|less|scss)$": "identity-obj-proxy", "^./style$": "identity-obj-proxy",
"^react$": "preact-compat",
"^react-dom$": "preact-compat"
}
}
Configuration for jest-puppeteer preset
在项目的根目录中创建jest-puppeteer.config.js文件,并使用以下代码:
module.exports = {
launch: {
headless: process.env.HEADLESS !== 'false',
slowMo: process.env.SLOWMO ? process.env.SLOWMO : 0,
devtools: true
},
}
运行测试代码
"scripts": { "e2e": "jest --config jest-e2e.config.json --runInBand"}
根据个人需要运行以下命令行:
yarn e2e
yarn e2e t- 'Test Name' // 单独运行某个测试用例
HEADLESS="false" SLOWMO=100 yarn e2e //以非无头模式启动,并降低puppeteer速度,适用于调试
编写测试代码
基本测试
const timeout = process.env.SLOWMO ? 30000 : 10000;
describe('Test Title of the page', () => {
beforeAll(async () => {
await page.goto(`${URL}/home`, {waitUntil: 'domcontentloaded'});
});
test('Title of the page', async () => {
await expect(page.title()).resolves.toMatch('e2e testing');
}, timeout);
test('Header of the page', async () => {
await page.waitForSelector('.header');
const html = await page.$eval('.header', ele => ele.innerHTML);
expect(html).toBe('Header');
}, timeout);
})
timeout为超时时间,在slowMo中运行Puppeteer时,会将超时时间从10000毫秒增加到30000毫秒,以确保测试不会超时。
表单提交
test('Form submit', async () => {
await page.waitForSelector('.app > form');
await page.type('#name', 'Lisa');
await page.type('#password','123456');
await page.click('[type="submit"]');
const response = await page.waitForResponse(response => response.url().indexOf('/api/login') > -1);
await response.json().then(res => expect(res.code).toBe(0))
},timeout)
状态改变
test('Test Tab Changed', async () => {
const preActive = await page.evaluate(() => document.querySelector('.tab-active').innerText);
await page.click('.tabs .tab-item:nth-child(2)');
const hasChanged = await page.waitForFunction(preActive => {
return document.querySelector('.tab-active').innerText !== preActive
}, timeout, preActive)
expect(hasChanged).toBeTruthy();
})
截屏
const path = require('path')
const devices = require('puppeteer/DeviceDescriptors');
const timeout = process.env.SLOWMO ? 30000 : 10000;
describe('Test Title and Header of the page', () => {
const iPhonex = devices['iPhone X'];
beforeAll(async () => {
await page.goto(`${URL}/home`, {waitUntil: 'domcontentloaded'});
await page.emulate(iPhonex);
await page.setViewport({ width: 375, height: 812, isMobile: true});
});
test('Take screenshot of home pag', async () => {
await page.screenshot({
path: path.join(__dirname, `./screenshots/home.jpg`),
fullpage: true, type: 'jpeg' });
}, timeout)})
pc端截图的话,不需要模拟设置,直接设置viewport即可。
扩展:直接设置viewport,窗口固定,对于超出视图外的内容无法截取到,如何截取整个页面内容?
global.getPageSize = () => {
return page.evaluate(() => {
return [
document.getElementsByTagName('html')[0].offsetHeight,
document.getElementsByTagName('html')[0].offsetWidth
]
})}
通过上面函数可获取文档尺寸,然后将改尺寸设置为viewport
beforeAll(async () => {
await page.goto(`${URL}/home`, {waitUntil: 'domcontentloaded'});
await page.emulate(iPhonex);
const [height, width] = await getPageSize();
await page.setViewport({ width, height, isMobile: true});
});
请求拦截
test('Intercept Request', async () => {
await page.setRequestInterception(true);
page.on('request', request => {
if (request.url().endsWith('.png')) {
request.abort();
} else {
request.continue();
}
});
await page.reload({waitUntil: 'networkidle0'});
await page.setRequestInterception(false);
}, timeout);
当前测试执行完毕后,一定要page.setRequestInterception(true),不然会影响后面的测试并带来诸多问题
扩展:测试过程中为了避免真实接口环境带来的影响,我们可以在请求拦截中更改请求响应,以达到我们想要的效果
test('Intercept Request', async () => {
await page.setRequestInterception(true);
page.on('request', request => {
if(request.url().indexOf('/api/user') > -1 && request.resourceType() == "xhr"){
request.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
code: 0,
data: {
name: 'Lynne'
}
})
});
return;
}
request.continue();
});
await page.setRequestInterception(false);
}, timeout);
定位新打开的页面
test('Target newly opened page', async () => {
await page.click('.link');
const newPage = await new Promise(resolve => browser.once('targetcreated', target => resolve(target.page())));
const title = await newPage.title();
await newPage.close();
expect(title).toBe('New Page');
}, timeout);
Puppeteer 常用API
1. 设置cookie
await page.setCookie([{'name': 'session', 'value': 'ajhjdhj'}]);
2. 等待
await page.waitFor(200);
await page.waitForSelector('.app');
await page.waitForFunction(`document.querySelector(".title").innerHTML.includes("首页")`);await page.waitForResponse(response => response.url().indexOf('/api/user') > -1)await page.waitForNavigation();
3. 执行脚本
const btn = await page.$('.join'); //获取元素;document.querySelector;返回ElementHandle,可以传递使用
const btns = await page.?('.join'); //获取元素;document.querySelectorAll
const innerText = await page.$eval('.join', ele => ele.innerHTML); //将document.querySelector获取的结果传递给pageFunctionconst innerText = await page.$$eval('.join', eles => eles[0].innerHTML);
const title = await page.evaluate(() => document.querySelector('.title').innerText);
4.模拟动作
await page.click('#btn')
await page.type('input')
const input = await page.$('input[type="file"]');
await input.uploadFile('/Users/Pictures/test.jpg');
await page.focus('input[type="text"]');
await page.select('select#example', 'JACK');
5.页面跳转
page.goto();
page.close();
page.reload();
page.goBack();
page.goForward();
6. 获取页面内容
page.screenshot();
page.pdf();
page.content();
page.title();
page.url();
page.cookies();
page.viewport();