- 原文地址:Testing your React App with Puppeteer and Jest
- 原文作者:Rajat S
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:jonjia
- 校對者:sunhaokk 老教授
如何使用 Puppeteer 和 Jest 對你的 React 應用進行端到端測試
端到端測試可以幫助我們確保 React 應用中所有的元件都能像我們預期的那樣工作,而單元測試和整合測試做不到這樣。
Puppeteer 是 Google 官方提供的一個端到端測試的 Node 庫,它向我們提供了基於 Dev Tools 協議封裝的上層 API 介面來控制 Chromium。有了 Puppeteer,我們可以開啟應用、執行測試。
在這篇文章中,我將展示如何使用 Puppeteer 和 Jest 在一個簡單的 React 應用上執行不用型別的測試。
專案初始化
我們先來建立一個 React 專案。然後安裝其它依賴項,比如 Puppeteer 和 Faker。
使用 create-react-app
命令來建立 React 應用並命名為 testing-app
。
create-react-app testing-app
複製程式碼
然後,來安裝開發依賴。
yarn add faker puppeteer --dev
複製程式碼
我們並不需要安裝 Jest,因為它已經內建在 React 包中了。如果你再次安裝的話,那接下來的測試不能順利進行了,因為這兩個不同版本的 Jest 會相互衝突。
接下來,我們需要更新下 package.json
中的 test
指令碼去呼叫 Jest。還需要新增一個新的 debug
指令碼。這個指令碼用來把我們的 Node 環境設定為除錯模式並呼叫 npm test
。
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "jest",
"debug": "NODE_ENV=debug npm test",
"eject": "react-scripts eject",
}
複製程式碼
使用 Puppeteer,我們可以選擇使用無頭模式執行測試,也可以選擇在 Chromium 中開啟。這是一個很棒的功能,因為我們可以看到測試中具體的頁面、使用開發者工具、檢視網路請求。唯一的缺點就是它會使持續整合(CI)測試變的非常慢。
我們可以配置環境變數來決定是否使用無頭模式來執行測試。當我需要看到測試執行的具體情況,我就會通過執行 debug
指令碼來關閉無頭模式。當我不需要時,就會執行 test
指令碼。
現在開啟 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
方法非常有用,因為通過它我們可以在各種瀏覽器設定下執行測試,也可以複製不同頁面的屬性。
使用 Puppeteer 測試 HTML 內容
我們已經準備好來為 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
我現在希望應用在提交表單時能將資訊儲存到 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 截圖
當測試失敗時,截圖可以幫助我們看到具體的內容。我們來看看如何用 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、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。