- 原文地址:Testing your React App with Puppeteer and Jest
- 原文做者:Rajat S
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:jonjia
- 校對者:sunhaokk 老教授
端到端測試能夠幫助咱們確保 React 應用中全部的組件都能像咱們預期的那樣工做,而單元測試和集成測試作不到這樣。css
Puppeteer 是 Google 官方提供的一個端到端測試的 Node 庫,它向咱們提供了基於 Dev Tools 協議封裝的上層 API 接口來控制 Chromium。有了 Puppeteer,咱們能夠打開應用、執行測試。html
在這篇文章中,我將展現如何使用 Puppeteer 和 Jest 在一個簡單的 React 應用上執行不用類型的測試。前端
咱們先來建立一個 React 項目。而後安裝其它依賴項,好比 Puppeteer 和 Faker。node
使用 create-react-app
命令來建立 React 應用並命名爲 testing-app
。react
create-react-app testing-app
複製代碼
而後,來安裝開發依賴。android
yarn add faker puppeteer --dev
複製代碼
咱們並不須要安裝 Jest,由於它已經內置在 React 包中了。若是你再次安裝的話,那接下來的測試不能順利進行了,由於這兩個不一樣版本的 Jest 會相互衝突。ios
接下來,咱們須要更新下 package.json
中的 test
腳本去調用 Jest。還須要添加一個新的 debug
腳本。這個腳本用來把咱們的 Node 環境設置爲調試模式並調用 npm test
。git
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "jest",
"debug": "NODE_ENV=debug npm test",
"eject": "react-scripts eject",
}
複製代碼
使用 Puppeteer,咱們能夠選擇使用無頭模式運行測試,也能夠選擇在 Chromium 中打開。這是一個很棒的功能,由於咱們能夠看到測試中具體的頁面、使用開發者工具、查看網絡請求。惟一的缺點就是它會使持續集成(CI)測試變的很是慢。github
咱們能夠配置環境變量來決定是否使用無頭模式來運行測試。當我須要看到測試執行的具體狀況,我就會經過運行 debug
腳原本關閉無頭模式。當我不須要時,就會運行 test
腳本。web
如今打開 src
目錄下的 App.test.js
文件,用下面的代碼替換原來的:
const puppeteer = require('puppeteer')
const isDebugging = () => {
const debugging_mode = {
headless: false,
slowMo: 250,
devtools: true,
}
return process.env.NODE_ENV === 'debug' ? debugging_mode : {}
}
describe('on page load', () => {
test('h1 loads correctly', async() => {
let browser = await puppeteer.launch({})
let page = await browser.newPage()
page.emulate({
viewport: {
width: 500,
height: 2400,
}
userAgent: ''
})
})
})
複製代碼
咱們首先在應用中使用 require
引入 puppeteer。而後用 describe
描述第一個測試,用來測試頁面的初始化加載。在這裏我測試 h1
元素是否包含正確的文本。
在咱們的測試描述中,須要定義 browser
和 page
變量。整個測試中都須要它們。
launch
方法能夠傳遞配置選項給瀏覽器,讓咱們使用不一樣的瀏覽器設置來測試應用。甚至能夠設置仿真選項來更改頁面的設置。
咱們先來設置瀏覽器。在文件頂部建立了一個名爲 isDebugging
的函數。咱們會在 launch 方法中調用這個函數。這個函數內定義了一個名爲 debugging_mode
的對象,這個對象包括下面三個屬性:
headless: false
— 使用無頭模式執行測試(true
)或者使用 Chromium 執行測試(false
)slowMo: 250
— 延遲 250 毫秒執行設置 Puppeteer 選項。devtools: true
— 打開應用時,瀏覽器是否打開開發者工具。這個 isDebugging
函數會返回一個基於環境變量的三元表達式。三元語句決定是返回 debugging_mode
,仍是返回一個空對象。
回到咱們的 package.json
文件,咱們建立了一個 debug
腳本,它會把 Node 設置爲調試環境。和上面的測試(使用瀏覽器默認選項)不一樣,若是咱們的環境變量爲 debug
,isDebugging
函數就會返回咱們自定義的瀏覽器選項。
接下來,對咱們的頁面進行配置。在 page.emulate
方法內完成。咱們設置 viewport
屬性中的 width
和 height
,並將 userAgent
設置爲空字符串。
page.emulate
方法很是有用,由於經過它咱們能夠在各類瀏覽器設置下執行測試,也能夠複製不一樣頁面的屬性。
咱們已經準備好來爲 React 應用編寫測試了。在這一節中,我會測試 <h1>
標籤和導航內容,確保它們能正常工做。
打開 App.test.js
文件,在 test
語句塊內 page.emulate
語句的下方,添加以下代碼:
await page.goto('http://localhost:3000/');
const html = await page.$eval('.App-title', e => e.innerHTML);
expect(html).toBe('Welcome to React');
browser.close();
},
16000
);
});
複製代碼
基本上,咱們告訴 Puppeteer 打開 [http://localhost:3000/](http://localhost:3000/.)
。Puppeteer 會執行 App-title
這個類。而咱們的 h1
標籤上設置了這個類。
這個 $.eval
方法實際上就是在調用對象上執行 document.querySelector
方法。
Puppeteer 會找到和這個類選擇器匹配的元素,而後做爲參數傳給 e => e.innerHTML
這個回調函數。在這裏,Puppeteer 能選出 <h1>
元素,並檢查這個元素的內容是不是 Welcome to React
。
一旦 Puppeteer 完成了測試,browser.close
方法就會關閉瀏覽器。
打開命令終端,執行 debug
腳本吧。
yarn debug
複製代碼
若是你的應用經過了測試,你會在終端中看到相似下面的內容:
接下來,在 App.js
文件中建立 nav
元素,具體以下:
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to React</h1>
<nav className='navbar' role='navigation'>
<ul>
<li className="nal-li"><a href="#">Batman</a></li>
<li className="nal-li"><a href="#">Supermman</a></li>
<li className="nal-li"><a href="#">Aquaman</a></li>
<li className="nal-li"><a href="#">Wonder Woman</a></li>
</ul>
</nav>
</header>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
</div>
);
}
}
export default App;
複製代碼
注意,全部的 <li>
元素都具備相同的類,回到 App.test.js
文件來編寫導航的測試。
在那以前,來重構下咱們前面的代碼。在 isDebugging
函數聲明下面,定義兩個全局變量:browser
和 page
。而後,調用beforeAll
方法,以下所示:
let browser
let page
beforeAll(async () => {
browser = await puppeteer.launch(isDebugging())
page = await browser.newPage()
await page.goto(‘http://localhost:3000/') page.setViewport({ width: 500, height: 2400 }) }) 複製代碼
早些時候,我並不須要設置 userAgent
。因此我沒使用 beforeAll
方法,而只用了 setViewport
方法。如今,我能夠擺脫localhost
和 browser.close
,使用 afterAll
方法替代。若是應用處於調試模式,(測試結束後)就須要關閉瀏覽器。
afterAll(() => {
if (isDebugging()) {
browser.close()
}
})
複製代碼
如今咱們能夠編寫導航測試了。在 describe
語句塊內部,建立一個新的 test
語句,以下:
test('nav loads correctly', async () => {
const navbar = await page.$eval('.navbar', el => el ? true : false)
const listItems = await page.$$('.nav-li')
expect(navbar).toBe(true)
expect(listItems.length).toBe(4)
}
複製代碼
在這裏,我首先給 $eval
方法傳入 .navbar
參數選取 navbar
元素。而後使用三元運算符返回這個元素是否存在(true
或 false
)。
接下來,須要選取列表項。和以前同樣,給 $eval
方法傳入 .nav-li
參數選取列表元素。咱們用 expect
方法斷言 navbar
元素存在(true
),而且列表項的個數爲 4。
你可能注意到了我在選取列表項上使用了 $$
方法。這是在頁面內運行 document.querySelector
方法的快捷方式。當 eval
和 $ 符號沒有一塊兒使用時,就不能傳遞迴調函數。
運行調試腳本,看看你的代碼可否經過兩個測試。
讓咱們看看如何經過模擬鍵盤輸入、鼠標點擊和觸摸事件來測試表單提交活動。咱們會使用 Faker 隨機生成的用戶信息來完成。
在 src
目錄下新建一個名爲 Login.js
的文件。這個組件包含四個輸入框和一個提交按鈕。
import React from 'react';
import './Login.css';
export default function Login(props) {
return (
<div className="form">
<div className="form">
<form onSubmit={props.submit} className="login-form">
<input data-testid="firstName" type="text" placeholder="first name"/>
<input data-testid="lastName" type="text" placeholder="last name"/>
<input data-testid="email" type="text" placeholder="Email"/>
<input data-testid="password" type="password" placeholder="password"/>
<button data-testid="submit">Login</button>
</form>
</div>
</div>
)
}
複製代碼
另外建立一個 Login.css
文件,源碼。
下面是經過 Bit 共享的組件,你可使用 NPM 安裝它,或者在你本身的項目中導入開發。
若是用戶點擊了 Login
按鈕,應用須要顯示一個 Success Message。因此要在 src
目錄下新建一個名爲 SucessMessage.js
的文件。另外建立一個 [SuccessMessage.css](https://gist.github.com/rajatgeekyants/1a77cdf44f296f2399d4b63f40a4900f)
文件。
import React from 'react';
import './SuccessMessage.css';
export default function Success() {
return (
<div>
<div className="wincc">
<div className="box" />
<div className="check" />
</div>
<h3 data-testid="success" className="success">
Success!!
</h3>
</div>
);
}
複製代碼
而後在 App.js
文件中導入它們。
import Login from './Login.js import SuccessMessage from './SuccessMessage.js
複製代碼
接下來,爲 App
組件添加一個 state
狀態。另外添加 handleSubmit
方法,它會阻止默認事件,並將 complete
屬性的值設爲 true
。
state = { complete: false }
handleSubmit = e => {
e.preventDefault()
this.setState({ complete: true })
}
複製代碼
而後在這個組件的底部添加一個三元語句。它會決定是顯示 Login
組件,仍是 SuccessMessage
組件。
{ this.state.complete ?
<SuccessMessage/>
:
<Login submit={this.handleSubmit} />
}
複製代碼
運行 yarn start
命令來確保你的應用能夠正常運行。
如今使用 Puppeteer 來編寫端到端測試,確保上面的功能能夠正常工做。在 App.test.js
文件中引入 faker
。而後建立一個 user
對象,以下:
const faker = require('faker')
const user = {
email: faker.internet.email(),
password: 'test',
firstName: faker.name.firstName(),
lastName: faker.name.lastName()
}
複製代碼
Faker 在測試中很是有用,每次測試,它都會生成不一樣的數據。
在 describe
語句塊中編寫一個新的 test
語句來測試登陸表單。測試會點擊輸入框並鍵入內容。而後會模擬點擊提交按鈕並等待成功信息組件的顯示。我也會給這個 test
增長一個超時。
test('login form works correctly', async () => {
await page.click('[data-testid="firstName"]')
await page.type('[data-testid="lastName"]', user.firstName)
await page.click('[data-testid="firstName"]')
await page.type('[data-testid="lastName"]', user.lastName)
await page.click('[data-testid="email"]')
await page.type('[data-testid="email"]', user.email)
await page.click('[data-testid="password"]')
await page.type('[data-testid="password"]', user.password)
await page.click('[data.testid="submit"]')
await page.waitForSelector('[data-testid="success"]')
}, 1600)
複製代碼
執行 debug
腳本,看看 Puppeteer 是如何來執行測試的!
我如今但願應用在提交表單時能將信息保存到 cookie。這些信息包括用戶的名字。
爲了簡單,我會重構 App.test.js
文件只打開一個頁面。這個頁面的客戶端會模擬爲 iPhone 6。
const puppeteer = require('puppeteer');
const faker = require('faker');
const devices = require('puppeteer/DeviceDescriptors');
const iPhone = devices['iPhone 6'];
const user = {
email: faker.internet.email(),
password: 'test',
firstName: faker.name.firstName(),
lastName: faker.name.lastName(),
};
const isDebugging = () => {
let debugging_mode = {
headless: false,
slowMo: 50,
devtools: true,
};
return process.env.NODE_ENV === 'debug' ? debugging_mode : {};
};
let browser;
let page;
beforeAll(async () => {
browser = await puppeteer.launch(isDebugging());
page = await browser.newPage();
await page.goto('http://localhost:3000/');
page.emulate(iPhone);
});
describe('on page load ', () => {
test(
'h1 loads correctly',
async () => {
const html = await page.$eval('.App-title', e => e.innerHTML);
expect(html).toBe('Welcome to React');
},
1600000
);
test('nav loads correctly', async () => {
const navbar = await page.$eval('.navbar', el => (el ? true : false));
const listItems = await page.$$('.nav-li');
expect(navbar).toBe(true);
expect(listItems.length).toBe(4);
});
test(
'login form works correctly',
async () => {
const firstNameEl = await page.$('[data-testid="firstName"]');
const lastNameEl = await page.$('[data-testid="lastName"]');
const emailEl = await page.$('[data-testid="email"]');
const passwordEl = await page.$('[data-testid="password"]');
const submitEl = await page.$('[data-testid="submit"]');
await firstNameEl.tap();
await page.type('[data-testid="firstName"]', user.firstName);
await lastNameEl.tap();
await page.type('[data-testid="lastName"]', user.lastName);
await emailEl.tap();
await page.type('[data-testid="email"]', user.email);
await passwordEl.tap();
await page.type('[data-testid="password"]', user.password);
await submitEl.tap();
await page.waitForSelector('[data-testid="success"]');
},
1600000
);
});
afterAll(() => {
if (isDebugging()) {
browser.close();
}
});
複製代碼
我想在提交表單時保存 cookie,咱們將在表單的上下文中添加測試。
爲登陸表單編寫一個新的 describe
語句塊,而後複製粘貼咱們用於登陸表單的測試代碼。
describe('login form', () => {
// 在這裏插入登陸表單的測試代碼
})
複製代碼
而後將它重命名爲 fills out form and submits
。再建立一個新的名爲 sets firstName cookie
的測試塊。它會檢查 firstNameCookie
是否保存到了 cookie 中。
test('sets firstName cookie', async () => {
const cookies = await Page.cookies()
const firstNameCookie = cookies.find(c => c.name === 'firstName' && c.value === user.firstName)
expect(firstNameCookie).not.toBeUndefined()
})
複製代碼
Page.cookies
方法返回文檔的每一個 cookie 對象組成的數組。使用數組的 find
方法來檢查 cookie 是否存在。這能夠確保應用使用的是 Faker 生成的 firstName
。
若是你如今運行 test
腳本,你會發現測試失敗了,由於返回的是一個 undefined 的值。如今來解決這個問題。
在 App.js
文件中,給 state
對象添加一個 firstName
屬性。默認值爲空字符串。
state = {
complete: false,
firstName: '',
}
複製代碼
在 handleSubmit
方法內,添加以下代碼:
document.cookie = `firstName=${this.state.firstname}`
複製代碼
新建一個名爲 handleInput
的方法。每次輸入都會調用這個方法來更新 state。
handleInput = e => {
this.setState({firstName: e.currentTarget.value})
}
複製代碼
把這個方法做爲一個 prop 傳遞給 Login
組件。
<Login submit={this.handleSubmit} input={this.handleInput} />
複製代碼
在 Login.js
文件內,爲 firstName
元素添加 onChange={props.input}
方法。這樣,只要用戶在 firstName
輸入框中輸入內容,React 就會調用這個方法。
如今,當用戶點擊了 Login
按鈕,我須要應用把 firstName
信息保存到 cookie。運行 npm test
命令,看看應用可否經過全部測試。
若是應用在執行任何操做以前須要某個 cookie,這個 cookie 是否應該在以前受權的頁面設置呢?
在 App.js
文件中,像下面這樣重構 handleSubmit
方法:
handleSubmit = e => {
e.preventDefault()
if (document.cookie.includes('JWT')){
this.setState({ complete: true })
}
document.cookie = `firstName=${this.state.firstName}`
}
複製代碼
經過上面的代碼,SuccessMessage
組件只有在 cookie 中包含 JWT
時纔會加載。
在 App.test.js
文件中的 fills out form and submits
測試代碼塊中,添加以下代碼:
await page.setCookie({ name: 'JWT', value: 'kdkdkddf' })
複製代碼
這將把一個實際上經過一些隨機測試來設置頁面令牌的'JWT'
保存到 cookie。若是你如今運行 test
腳本,你的應用會執行並經過全部測試!
當測試失敗時,截圖能夠幫助咱們看到具體的內容。咱們來看看如何用 Puppeteer 來截圖並分析測試。
在 App.test.js
文件 nav loads correctly
測試語句塊內。添加一個條件語句來檢查列表項 listItems
的個數是否是不等於 3。若是這樣,Puppeteer 就應該對頁面進行截圖,更新測試的 expect 語句,指望 listItems
的個數是 3 不是 4。
if (listItems.length !== 3)
await page.screenshot({path: 'screenshot.png'});
expect(listItems.length).toBe(3);
複製代碼
顯然,咱們的測試會失敗,由於咱們的應用中有 4 個 listItems
。在終端中運行 test
腳本,測試失敗。同時你會在項目的根目錄中發現一個 screenshot.png
文件。
截圖
你也能夠配置截圖方法,以下:
fullPage
— 若是設爲 true
,Puppeteer 會對整個頁面截圖。quality
— 從 0 到 100 的值,用來指定圖片質量。clip
— 提供一個對象來指定頁面的某個區域進行屏幕截圖。你也能夠不使用 page.screenshot
方法,而是用 page.pdf
來建立頁面的 PDF 文件。這個方法有本身的配置。
scale
— 設置縮放倍數的數字,默認值爲 1。format
— 設置紙張格式。若是設置這個屬性,會優於傳給它的任何寬度或高度選項。默認值是 letter
。margin
— 用來設置紙張的邊距。讓咱們看看 Puppeteer 在測試中如何處理頁面請求。在 App.js
文件中,我會添加一個異步的 componentDidMount
方法。此方法會從Pokemon API 中獲取數據。這個請求的響應會使用 JSON 文件的形式。我也會將這些數據添加到組件的狀態中。
async componentDidMount() {
const data = await fetch('https://pokeapi.co/api/v2/pokedex/1/').then(res => res.json())
this.setState({pokemon: data})
}
複製代碼
確保在 state 對象中添加了 pokemon: {}
。在 app 組件內,添加一個 <h3>
標籤。
<h3 data-testid="pokemon">
{this.state.pokemon.next ? 'Received Pokemon data!' : 'Something went wrong'}
</h3>
複製代碼
運行應用,你會發現應用已經成功獲取數據。
使用 Puppeteer,我能夠編寫任務來檢查咱們的 <h3/>
元素是否包含成功請求到的內容,或攔截請求、強制失敗。這樣,我能夠查看應用在請求成功和失敗狀況下是如何工做的。
我首先讓 Puppeteer 發送一個請求來攔截獲取請求。而後,若是個人網址包含 pokeapi
,那麼 Puppeteer 應該停止攔截的請求。不然,一切都應該繼續下去。
打開 App.test.js
文件,在 beforeAll
方法中添加以下代碼:
await page.setRequestInterception(true);
page.on('request', interceptedRequest => {
if (interceptedRequest.url.includes('pokeapi')) {
interceptedRequest.abort();
} else {
interceptedRequest.continue();
}
});
複製代碼
setRequestInterception
是一個標誌,使我能訪問頁面發出的每一個請求。一旦請求被攔截,請求就會停止,並返回特定的錯誤碼。也能夠將請求設置爲失敗或檢查一些邏輯以後繼續攔截請求。
咱們來寫一個新的名爲 fails to fetch pokemon
的測試。這個測試會執行 h3
元素。而後抓取這個元素的內容,確保內容爲 Received Pokemon data!
。
await page.setRequestInterception(true);
page.on('request', interceptedRequest => {
if (interceptedRequest.url.include('pokeapi')) {
interceptedRequest.abort();
} else {
interceptedRequest.continue();
}
});
複製代碼
執行 debug
代碼,你會實際看到 <h3/>
元素。你會注意到元素的內容一直是 Something went wrong
。全部的測試都經過了,那意味着咱們成功的阻止了 Pokemon 請求。
注意在停止請求時,咱們能夠控制請求頭、返回的錯誤碼和自定義響應的實體。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。