Reactお辞儀(14)——テストフレームワーク
20186 ワード
テストは、問題の発見と予防だけでなく、リスクを低減し、企業の損失を減らすことができます.Reactでは、さまざまなテストフレームワークが用意されています.このセクションでは、JestとEnzymeについて詳しく説明します.
一、Jest
一、Jest
JestはFacebookからオープンソースのテストフレームワークで、Reactプロジェクトとシームレスに互換性があり、簡単に集中し、ゼロ構成を推奨し、オープンボックスで使用する目的で、論理とコンポーネントのユニットテストに使用されます.文法や断言はJasmineと似ており、スナップショットテスト、Mock、オーバーライド率レポートなどの機能も統合されており、マルチプロセス並列実行テストをサポートし、内部でJSDOMを使用してDOMを操作します.JSDOMは、通常のブラウザと同様の動作をし、ユーザーと対話したり、ノード上でイベントを配布したりすることができます.
1)運転
Jestの実行を容易にするために、次のコマンドを使用してCreate React Appを使用してプロジェクトを作成します.npx create-react-app my-app
テストファイルを__に置くだけでtests__ディレクトリに、またはその名前を追加します.test.jsまたは.spec.js接尾辞は、プロジェクトのsrcディレクトリに保存されている任意の深さでJestによって検出されます.次のコマンドを実行すると、関連するテスト結果が得られます.npm test
デフォルトでは、Jestは、今回変更したファイルに関連するテスト例のみを実行します.
2)テストの作成
テストケース(Test Case)を作成する場合は、test()またはit()関数を使用します.最初のパラメータはテスト名で、2番目のパラメータはテストコードを含むコールバック関数です.以下に示します.test("two plus two is four", () => {
expect(2 + 2).toBe(4);
});
expect()関数は、実際の値を受信し、結果としてマッチング器の所望の値と比較できると断言するために使用されます.一致に失敗すると、対応するエラー・プロンプトがコンソールに出力されます.
describe()関数は、以下に示すように、テスト例を論理的にグループ化することができ、その最初のパラメータは、グループの名前を定義することができる.describe("my test case", () => {
test("one plus one is two", () => {
expect(1 + 1).toBe(2);
});
test("two plus two is four", () => {
expect(2 + 2).toBe(4);
});
});
3)整合器
マッチング器(Matcher)によってコードを様々な方法でテストすることができる、例えば、前の例のtoBe()はObjectを用いるマッチング器である.is()は、オブジェクトが等しいかどうかを確認するために、次のようにtoEqual()に変更することができます.test("object assignment", () => {
const data = { name: "strick" };
data["age"] = 28;
expect(data).toEqual({ name: "strick", age: 28 });
});
他の一般的なマッチングには、undefined、null、ブール値の区別、数値の比較、文字列のマッチング、配列または反復可能なオブジェクトに特定の項目が含まれているかどうかを確認し、放出されたエラーをテストする機能もあります.
すべてのマッチング器を通過することができる.notは、例えばtoBeUndefined()がnullに一致しないことを検証し、以下に示す.test("null is not undefined", () => {
expect(null).not.toBeUndefined();
});
4)非同期テスト
Jestは、コールバック関数、Promise、Async/Awaitなど、非同期コードをテストするためのさまざまな方法を提供しています.次に、使い方を説明します.
(1)デフォルトでは、Jestテストは最後まで実行されると完了します.たとえば、check()関数(以下に示す)があり、check()の実行が終了すると、このテストはコールバック関数が実行されない前に終了します.function check(func) {
const success = true;
func(success);
}
test("the data is truth", () => {
function callback(data) {
expect(data).toBeTruthy();
}
check(callback);
});
この問題を解決するには、test()のコールバック関数にdoneという関数パラメータを渡し、Jestはdone()のコールバック関数が実行された後、次のようにテストを終了します.test("the data is truth", done => {
function callback(data) {
expect(data).toBeTruthy();
done();
}
check(callback);
});
(2)非同期コードがPromiseオブジェクトに戻ると,Jestはその状態の変化を待つ.ステータスが完了した場合はthen()メソッドを使用します.ステータスが拒否された場合は、catch()メソッドを使用します.以下に示します.//
function checkResolve() {
return new Promise((resolve, reject) => {
resolve(true);
});
}
test("the data is truth", () => {
return checkResolve().then(data => {
expect(data).toBeTruthy();
});
});
//
function checkReject() {
return new Promise((resolve, reject) => {
reject(false);
});
}
test("the data is falsity", () => {
return checkReject().catch(data => {
expect(data).toBeFalsy();
});
});
Promiseオブジェクトをtest()のコールバック関数の戻り値として使用すると、テストが早期に完了しないようにし、メソッドチェーンでの断言が行われません.
expect文でも使用できます....rejectsの2つのマッチング器はPromiseの2つの状態を処理し、以下に示すように文法がより簡潔である.test("the data is truth", () => {
expect(checkResolve()).resolves.toBeTruthy();
});
test("the data is falsity", () => {
expect(checkReject()).rejects.toBeFalsy();
});
(3)asyncとawaitの2つのキーワードをテストで使用して、checkResolve()を断言するなどのPromiseオブジェクトの処理結果を以下に示すように一致させることもできます.test("the data is truth", async () => {
const data = await checkResolve();
expect(data).toBeTruthy();
});
拒否されたステータスのPromiseもテストできます.以下に示すように、assertions()は、テストで指定された数の断言が実行されたかどうかを検証するために使用されます.function checkError() {
return new Promise((resolve, reject) => {
reject();
}).catch(() => {
throw "error";
});
}
test("the check fails with an error", async () => {
expect.assertions(1);
try {
await checkError();
} catch (e) {
expect(e).toMatch("error");
}
});
aysncとawiatはまた....rejectsを組み合わせて使用します.以下に示します.test("the data is truth", async () => {
await expect(checkResolve()).resolves.toBeTruthy();
});
test("the check fails with an error", async () => {
await expect(checkError()).rejects.toMatch("error");
});
5)補助関数
場合によっては、テストを実行する前に準備作業を行う必要があり、テストを実行した後に整理作業を行う必要がある場合があります.Jestは、次の2つの作業を処理するために4つの関連する補助関数を提供しています.
(1)beforeAll()およびafterAll()は、すべてのテスト・インスタンスの前後で1回実行されます.
(2)beforeEach()およびafterEach()は、各試験例の前後で実行され、非同期試験のように非同期コードを処理することができる.
4つの補助関数にそれぞれの関数名が出力され、次のコードに示す2つのテスト例があると仮定します.beforeAll(() => {
console.log("beforeAll");
});
afterAll(() => {
console.log("afterAll");
});
beforeEach(() => {
console.log("beforeEach");
});
afterEach(() => {
console.log("afterEach");
});
test("first", () => {
expect(2).toBeGreaterThan(1);
});
test("second", () => {
expect(2).toBeLessThan(3);
});
テストを実行するたびに、コンソールに「beforeAll」、2対の「beforeEach」と「afterEach」、「afterAll」の順に印刷されます.
テスト例をdescribe()でグループ化すると(以下に示す)、外部のbeforeEach()とafterEach()が優先的に実行されます.describe("scoped", () => {
beforeEach(() => console.log("inner beforeEach"));
afterEach(() => console.log("inner afterEach"));
test("third", () => {
expect([1, 2]).toContain(1);
});
});
6)Mock
JestにはMock関数が内蔵されており、消去関数の実装に使用してコード間の接続をテストしたり、関数の呼び出しやパラメータをキャプチャしたり、戻り値を構成したりすることができます.
カスタムforEach()関数の内部実装をテストする場合はjestを使用します.fn()は、以下に示すように、Mock関数を作成し、そのmock属性を確認することによって、コールバック関数が予想通りに呼び出されているかどうかを確認します.function forEach(items, callback) {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
}
test("forEach", () => {
const mockFunc = jest.fn(x => 42 + x);
forEach([0, 1], mockFunc);
expect(mockFunc.mock.calls.length).toBe(2); // Mock
expect(mockFunc.mock.calls[0][0]).toBe(0); // 0
expect(mockFunc.mock.calls[1][0]).toBe(1); // 1
expect(mockFunc.mock.results[0].value).toBe(42); // 42
});
各Mock関数には、関数がどのように呼び出されたか、呼び出されたときの戻り値などの情報が記録された特殊なmock属性が含まれており、この属性により、呼び出されるたびにthisの値を追跡することもできます.戻る値をMock関数で注入する場合は、次のようにチェーンで追加し、最初の呼び出しは10を返し、2回目の呼び出しは「x」を返し、次の呼び出しはtrueを返します.ここでmockName()メソッドは、出力されたログに表示されるMock関数に名前を付けることができ、デフォルトのjest.fn()を置き換えることができます.const myMock = jest.fn().mockName("returnValue");
myMock
.mockReturnValueOnce(10)
.mockReturnValueOnce("x")
.mockReturnValue(true);
console.log(myMock(), myMock(), myMock(), myMock()); //10, 'x', true, true
Mock関数は、例えばaxios要求のデータをブロックする、以下のコードに示すように、モジュールをシミュレートすることもできる.getは、テスト用の偽データを返すmockResolvedValue()メソッドを提供します.import axios from "axios";
jest.mock("axios");
class Users {
static all() {
return axios.get("./users.json").then(resp => resp.data);
}
}
test("should fetch users", () => {
const users = [{ name: "strick" }];
const resp = { data: users };
axios.get.mockResolvedValue(resp);
return Users.all().then(data => expect(data).toEqual(users));
});
原生のタイマー関数のテストは不便でjestに合格した.useFakeTimers()は、以下に示すようにタイマ関数をシミュレートすることができる.function timerGame() {
setTimeout(() => {
console.log("start");
}, 1000);
}
jest.useFakeTimers();
test("setTimeout", () => {
timerGame();
expect(setTimeout).toHaveBeenCalledTimes(1); // 1
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000); //1
});
Jestがシミュレートしたタイマー関数には、正しい時点に早送りしたり、現在待機しているタイマーを実行したりする機能もあります.
7)スナップショットテスト
Jestが提供するスナップショットテスト(Spapshot Testing)は、Reactコンポーネントを純粋なテキスト(すなわちスナップショット)にシーケンス化してハードディスクに保存する効率的なUIテストであり、テストのたびに現在生成されているスナップショットと保存されているスナップショットを比較し、次にスナップショットテストの使用例を説明します.
まずLinkコンポーネントを作成します.onMouseEnterイベントを含むリンクがレンダリングされ、マウスをこのリンクに移動するとclassプロパティが変更されます.import React from "react";
const STATUS = {
HOVERED: "hovered",
NORMAL: "normal"
};
export default class Link extends React.Component {
constructor(props) {
super(props);
this._onMouseEnter = this._onMouseEnter.bind(this);
this.state = {
class: STATUS.NORMAL
};
}
_onMouseEnter() {
this.setState({ class: STATUS.HOVERED });
}
render() {
return (
<a
href="#"
className={this.state.class}
onMouseEnter={this._onMouseEnter}
>
{this.props.children}
);
}
}
テストファイルspapshotを作成します.test.jsは、その内部にLinkコンポーネントを導入するほか、ブラウザやJSDOMに依存せず、ReactコンポーネントをJavaScriptオブジェクト(スナップショット)にレンダリングできるreact-test-rendererを導入する必要があります.import React from "react";
import Link from "./Link";
import renderer from "react-test-renderer";
test("Link changes the class when hovered", () => {
const component = renderer.create(Strick);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
tree.props.onMouseEnter(); //
tree = component.toJSON(); //
expect(tree).toMatchSnapshot();
});
最初にテストを実行すると、自動的に__が作成されます.snapshots__ディレクトリ、対応するスナップショットファイルspapshotを配置する.test.js.snapは、以下に示すように、onMouseEnterイベントをトリガして生成された2つのスナップショットを含む.exports[`Link changes the class when hovered 1`] = `
<a
className="normal"
href="#"
onMouseEnter={[Function]}
>
Strick
`;
exports[`Link changes the class when hovered 2`] = `
<a
className="hovered"
href="#"
onMouseEnter={[Function]}
>
Strick
`;
保存したスナップショットをリフレッシュする場合は、手動で削除するほかjest-uコマンドで実現できます.
二、Enzyme
EnzymeはReactコンポーネント用のテストフレームワークで、レンダリングされたDOM構造を処理することができ、オープンAPIはjQueryの構文に類似しており、3つの異なる方法でコンポーネントをテストすることができます:浅いレンダリング(Shallow Rendering)、完全レンダリング(Full Rendering)、静的レンダリング(Static Rendering).Enzyme 3から、Enzymeをインストールするとともに、Reactバージョンに対応するアダプタをインストールする必要があります.コマンドは以下の通りです.npm install --save enzyme enzyme-adapter-react-16
1)ライトレンダリング
DOMとは独立した浅いレンダーでは、Reactコンポーネントの最初のレイヤのみがレンダーされます.サブコンポーネントの動作は無視されます.また、サブコンポーネントをレンダーする必要はありません.これにより、より隔離性が向上します.ただし、浅いレンダリングには、Refsがサポートされていないという限界もあります.
上記のセクションのLinkコンポーネントを例にとると、以下に示すように、Enzymeを行う前にconfigure()関数でアダプタを構成してからshallow()関数でLinkコンポーネントを浅くレンダリングする必要があります.import React from "react";
import { shallow, configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import Link from "../component/Form/Link";
configure({ adapter: new Adapter() });
test("Link changes the class after mouseenter", () => {
const wrapper = shallow(Strick),
a = wrapper.find("a");
expect(wrapper.text()).toEqual("Strick");
a.simulate("mouseenter"); //
expect(a.prop("className")).toEqual("normal"); //
});
wrapperは、find()のような複数の操作DOMの方法を含む仮想DOMオブジェクトであり、セレクタに基づいて指定されたノードを見つけることができ、simulate()は現在のノードのイベントをトリガーすることができる.
2)完全レンダリング
mount()関数は、受信したコンポーネント、すなわちそのサブコンポーネントもレンダリングされます.完全レンダリングはJSDOMに依存し、複数のテストが同じDOMを処理する場合、相互に影響する可能性があるため、テスト終了後にunmount()メソッドを使用してコンポーネントをアンインストールする必要があります.
3)スタティックレンダリング
render()関数は、コンポーネントを静的にレンダリングします.つまり、それをHTML文字列にレンダリングし、CheerioライブラリでHTML構造を解析します.CheerioはJSDOMに似ていますが、より軽量でjQueryのように文字列を操作できます.
npx create-react-app my-app
npm test
test("two plus two is four", () => {
expect(2 + 2).toBe(4);
});
describe("my test case", () => {
test("one plus one is two", () => {
expect(1 + 1).toBe(2);
});
test("two plus two is four", () => {
expect(2 + 2).toBe(4);
});
});
test("object assignment", () => {
const data = { name: "strick" };
data["age"] = 28;
expect(data).toEqual({ name: "strick", age: 28 });
});
test("null is not undefined", () => {
expect(null).not.toBeUndefined();
});
function check(func) {
const success = true;
func(success);
}
test("the data is truth", () => {
function callback(data) {
expect(data).toBeTruthy();
}
check(callback);
});
test("the data is truth", done => {
function callback(data) {
expect(data).toBeTruthy();
done();
}
check(callback);
});
//
function checkResolve() {
return new Promise((resolve, reject) => {
resolve(true);
});
}
test("the data is truth", () => {
return checkResolve().then(data => {
expect(data).toBeTruthy();
});
});
//
function checkReject() {
return new Promise((resolve, reject) => {
reject(false);
});
}
test("the data is falsity", () => {
return checkReject().catch(data => {
expect(data).toBeFalsy();
});
});
test("the data is truth", () => {
expect(checkResolve()).resolves.toBeTruthy();
});
test("the data is falsity", () => {
expect(checkReject()).rejects.toBeFalsy();
});
test("the data is truth", async () => {
const data = await checkResolve();
expect(data).toBeTruthy();
});
function checkError() {
return new Promise((resolve, reject) => {
reject();
}).catch(() => {
throw "error";
});
}
test("the check fails with an error", async () => {
expect.assertions(1);
try {
await checkError();
} catch (e) {
expect(e).toMatch("error");
}
});
test("the data is truth", async () => {
await expect(checkResolve()).resolves.toBeTruthy();
});
test("the check fails with an error", async () => {
await expect(checkError()).rejects.toMatch("error");
});
beforeAll(() => {
console.log("beforeAll");
});
afterAll(() => {
console.log("afterAll");
});
beforeEach(() => {
console.log("beforeEach");
});
afterEach(() => {
console.log("afterEach");
});
test("first", () => {
expect(2).toBeGreaterThan(1);
});
test("second", () => {
expect(2).toBeLessThan(3);
});
describe("scoped", () => {
beforeEach(() => console.log("inner beforeEach"));
afterEach(() => console.log("inner afterEach"));
test("third", () => {
expect([1, 2]).toContain(1);
});
});
function forEach(items, callback) {
for (let index = 0; index < items.length; index++) {
callback(items[index]);
}
}
test("forEach", () => {
const mockFunc = jest.fn(x => 42 + x);
forEach([0, 1], mockFunc);
expect(mockFunc.mock.calls.length).toBe(2); // Mock
expect(mockFunc.mock.calls[0][0]).toBe(0); // 0
expect(mockFunc.mock.calls[1][0]).toBe(1); // 1
expect(mockFunc.mock.results[0].value).toBe(42); // 42
});
const myMock = jest.fn().mockName("returnValue");
myMock
.mockReturnValueOnce(10)
.mockReturnValueOnce("x")
.mockReturnValue(true);
console.log(myMock(), myMock(), myMock(), myMock()); //10, 'x', true, true
import axios from "axios";
jest.mock("axios");
class Users {
static all() {
return axios.get("./users.json").then(resp => resp.data);
}
}
test("should fetch users", () => {
const users = [{ name: "strick" }];
const resp = { data: users };
axios.get.mockResolvedValue(resp);
return Users.all().then(data => expect(data).toEqual(users));
});
function timerGame() {
setTimeout(() => {
console.log("start");
}, 1000);
}
jest.useFakeTimers();
test("setTimeout", () => {
timerGame();
expect(setTimeout).toHaveBeenCalledTimes(1); // 1
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000); //1
});
import React from "react";
const STATUS = {
HOVERED: "hovered",
NORMAL: "normal"
};
export default class Link extends React.Component {
constructor(props) {
super(props);
this._onMouseEnter = this._onMouseEnter.bind(this);
this.state = {
class: STATUS.NORMAL
};
}
_onMouseEnter() {
this.setState({ class: STATUS.HOVERED });
}
render() {
return (
<a
href="#"
className={this.state.class}
onMouseEnter={this._onMouseEnter}
>
{this.props.children}
);
}
}
import React from "react";
import Link from "./Link";
import renderer from "react-test-renderer";
test("Link changes the class when hovered", () => {
const component = renderer.create(Strick);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
tree.props.onMouseEnter(); //
tree = component.toJSON(); //
expect(tree).toMatchSnapshot();
});
exports[`Link changes the class when hovered 1`] = `
<a
className="normal"
href="#"
onMouseEnter={[Function]}
>
Strick
`;
exports[`Link changes the class when hovered 2`] = `
<a
className="hovered"
href="#"
onMouseEnter={[Function]}
>
Strick
`;
EnzymeはReactコンポーネント用のテストフレームワークで、レンダリングされたDOM構造を処理することができ、オープンAPIはjQueryの構文に類似しており、3つの異なる方法でコンポーネントをテストすることができます:浅いレンダリング(Shallow Rendering)、完全レンダリング(Full Rendering)、静的レンダリング(Static Rendering).Enzyme 3から、Enzymeをインストールするとともに、Reactバージョンに対応するアダプタをインストールする必要があります.コマンドは以下の通りです.
npm install --save enzyme enzyme-adapter-react-16
1)ライトレンダリング
DOMとは独立した浅いレンダーでは、Reactコンポーネントの最初のレイヤのみがレンダーされます.サブコンポーネントの動作は無視されます.また、サブコンポーネントをレンダーする必要はありません.これにより、より隔離性が向上します.ただし、浅いレンダリングには、Refsがサポートされていないという限界もあります.
上記のセクションのLinkコンポーネントを例にとると、以下に示すように、Enzymeを行う前にconfigure()関数でアダプタを構成してからshallow()関数でLinkコンポーネントを浅くレンダリングする必要があります.
import React from "react";
import { shallow, configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import Link from "../component/Form/Link";
configure({ adapter: new Adapter() });
test("Link changes the class after mouseenter", () => {
const wrapper = shallow(Strick),
a = wrapper.find("a");
expect(wrapper.text()).toEqual("Strick");
a.simulate("mouseenter"); //
expect(a.prop("className")).toEqual("normal"); //
});
wrapperは、find()のような複数の操作DOMの方法を含む仮想DOMオブジェクトであり、セレクタに基づいて指定されたノードを見つけることができ、simulate()は現在のノードのイベントをトリガーすることができる.
2)完全レンダリング
mount()関数は、受信したコンポーネント、すなわちそのサブコンポーネントもレンダリングされます.完全レンダリングはJSDOMに依存し、複数のテストが同じDOMを処理する場合、相互に影響する可能性があるため、テスト終了後にunmount()メソッドを使用してコンポーネントをアンインストールする必要があります.
3)スタティックレンダリング
render()関数は、コンポーネントを静的にレンダリングします.つまり、それをHTML文字列にレンダリングし、CheerioライブラリでHTML構造を解析します.CheerioはJSDOMに似ていますが、より軽量でjQueryのように文字列を操作できます.