[譯] 使用 Puppeteer 和 Jest 測試你的 React 應用

jonjia發表於2018-05-14

如何使用 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 元素是否包含正確的文字。

在我們的測試描述中,需要定義 browserpage 變數。整個測試中都需要它們。

launch 方法可以傳遞配置選項給瀏覽器,讓我們使用不同的瀏覽器設定來測試應用。甚至可以設定模擬選項來更改頁面的設定。

我們先來設定瀏覽器。在檔案頂部建立了一個名為 isDebugging 的函式。我們會在 launch 方法中呼叫這個函式。這個函式內定義了一個名為 debugging_mode 的物件,這個物件包括下面三個屬性:

  • headless: false — 使用無頭模式執行測試(true)或者使用 Chromium 執行測試(false
  • slowMo: 250 — 延遲 250 毫秒執行設定 Puppeteer 選項。
  • devtools: true — 開啟應用時,瀏覽器是否開啟開發者工具。

這個 isDebugging 函式會返回一個基於環境變數的三元表示式。三元語句決定是返回 debugging_mode,還是返回一個空物件。

回到我們的 package.json 檔案,我們建立了一個 debug 指令碼,它會把 Node 設定為除錯環境。和上面的測試(使用瀏覽器預設選項)不同,如果我們的環境變數為 debugisDebugging 函式就會返回我們自定義的瀏覽器選項。

接下來,對我們的頁面進行配置。在 page.emulate 方法內完成。我們設定 viewport 屬性中的 widthheight,並將 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
複製程式碼

如果你的應用通過了測試,你會在終端中看到類似下面的內容:

[譯] 使用 Puppeteer 和 Jest 測試你的 React 應用

接下來,在 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 函式宣告下面,定義兩個全域性變數:browserpage。然後,呼叫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 方法。現在,我可以擺脫localhostbrowser.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 元素。然後使用三元運算子返回這個元素是否存在(truefalse)。

接下來,需要選取列表項。和之前一樣,給 $eval 方法傳入 .nav-li 引數選取列表元素。我們用 expect 方法斷言 navbar 元素存在(true),並且列表項的個數為 4。

你可能注意到了我在選取列表項上使用了 $$ 方法。這是在頁面內執行 document.querySelector 方法的快捷方式。當 eval 和 $ 符號沒有一起使用時,就不能傳遞迴調函式。

執行除錯指令碼,看看你的程式碼能否通過兩個測試。

[譯] 使用 Puppeteer 和 Jest 測試你的 React 應用


模擬使用者活動

讓我們看看如何通過模擬鍵盤輸入、滑鼠點選和觸控事件來測試表單提交活動。我們會使用 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 安裝它,或者在你自己的專案中匯入開發。

[譯] 使用 Puppeteer 和 Jest 測試你的 React 應用

如果使用者點選了 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 和 Jest 測試你的 React 應用

現在使用 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 是如何來執行測試的!

[譯] 使用 Puppeteer 和 Jest 測試你的 React 應用


在測試中設定 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 檔案。

[譯] 使用 Puppeteer 和 Jest 測試你的 React 應用

截圖

你也可以配置截圖方法,如下:

  • 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 請求。

[譯] 使用 Puppeteer 和 Jest 測試你的 React 應用

注意在中止請求時,我們可以控制請求頭、返回的錯誤碼和自定義響應的實體。


瞭解更多:


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章