使用Jest和Puppeteer进行E2E测试

4,391 阅读3分钟

测试是每个软件开发过程的关键部分。它可以大大降低项目成本,并可以提高团队的生产力。基本上有三种主要的测试类型:

  • 单元测试—通过单元测试,可以测试代码中独立小片段,例如一个组件,一个函数。
  • 集成测试—在这种类型的测试中,我们组合并测试各个单元,并将它们作为一个整体进行测试。
  • 端到端(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();