Jest manual mock
Jest の続き、 mockについて。Jest の documentation は悪くはないけど React のそれに比すると、ちいと本質の説明が抜けていたりする。
今回テストの対象は、小さなアプリで、Web API / React の組み合わせを TypeScript で書いたもの。Azure AD への oAuth2 と、Axios を使った API (Rest) call が存在し、これらを mock してみよう、というお題。が、壁が厚すぎるので、まずは簡単な、エラー表示だけをつかさどる Error というモジュールを Mock してみる。
error.tsx (actual):
---
import * as React from "react";
interface Props {
errors: string
}
interface State {
}
export default class Error extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
}
public render() {
return <h3>{this.props.errors}</h3>;
}
}
actual 版は、props に渡した errors: string をレンダーするが、mock 版は "I am mock" をレンダーする。
次に、 Unit Test を書くが、これが動作するようになるまで3日かかってしまった。分かってしまえば簡単。
error.test.tsx
---
import * as React from "react";
import {shallow, mount} from "enzyme";
import * as renderer from "react-test-renderer";
jest.mock("../components/error");
import Errors from "../components/error";
const errorMessage = "hello world!";
test.only("test error component", () => {
const wrapper = shallow(<Errors errors={errorMessage} />);
const tobe = "<h3>" + errorMessage + "</h3>";
expect(wrapper.text()).toEqual(errorMessage);
// expect(wrapper).toHaveProp("errors");
});
何故3日もかかったか。私がバカで大きな勘違いをしていたからに尽きる。上のとおり、jest.mock() という関数を呼んで mock すべきモジュールを指定するが、この関数は、error.tsx (actual) の中身(prototype)を直接操作するものではなく、アプリからこれが import や require されるときに返す関数・モジュールをミミックするものである。なので、import や requireは、jest.mock() を実施した後に置かないと、いつまでたっても actual を掴まされることになる。悪いことに TypeScript では(というかTSLintだからかな)、import は関数の外に無いと叱られる。TypeScript でのJest サンプルが少ないので左様な記述が少なく、、、という言い訳。上を実施、mock が上手く動作していれば、Unit Test は Failed するはず。以下結果:
FAIL src\__tests__\error.test.tsx
× test error component (15ms)
● test error component
expect(received).toEqual(expected)
Expected value to equal:
"hello world!"
Received:
"I am mock"
at Object.<anonymous> (src/__tests__/error.test.tsx:11:28)
at new Promise (<anonymous>)
at <anonymous>
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 2.19s
Ran all test suites.
ばんざーい。
jest.mock() を Unit Test の中に置きたい場合は、
test.only("test error component", () => {
jest.mock("../components/error");
const Errors = require("../components/error").default;
const wrapper = shallow(<Errors errors={errorMessage} />);
const tobe = "<h3>" + errorMessage + "</h3>";
expect(wrapper.text()).toEqual(errorMessage);
});
にてイケる。以上、基礎編。
===
いよいよ、Adal oAuth と Ajax request を司るモジュールを mock してみる。これは AdalTs という名前で、以下4つの public methodを提供する。
Init()
isAuthenticated()
adalRequest()
logOut()
Web API からRestでデータを持ってくるのが adalRequest() であり、ここで Azure に認証されていないと oAuth2 を開始する。されていれば、Axion を使って API に GET しにいくが、ここは無論非同期処理であり Promise を返す。
これの mock 版を作る。
./__mocks__/adal-ts.tsx
---
export default class AdalTs {
public Init(): any {
return true;
}
public isAuthenticated = () => {
const promise = new Promise((resolve, reject) => {
resolve();
});
return promise;
}
public adalRequest(): any {
console.log("---mock adalRequest called");
const promise = new Promise((resolve, reject) => {
const d = this.taskdata;
resolve({ data: d});
});
return promise;
}
public logOut() {
return {};
}
private taskdata: any = [
{
Id: 1307327,
OIDString: "V089231",
Action: "Notification: Review and modify as required, and proceed the WorkFlow.",
Days: "56 days"
},
{
Id: 1319423,
OIDString: "V089539",
Action: "II: Invoice Submission",
Days: "29 days"
}
];
actial 版の各 public methodを書き換えているが、adalRequest() では、Web API GETに変わり、固定のダミーデータをpromise.resolve に返すというもの。
多少余談となるが、この AdalTsはもともと singleton で作っていた。即ち、export new AdalTs() し、constructor を細工して同一インスタンスを返すようになっていた。Unit Test では、これは使えない。たとえば、jest.mock("../adal-ts")とやって、その実態が class (type)ではなくて インスタンスだとエラーとなる。mock の対象となるモジュールは singleton はダメであり、単一インスタンスが必要な場合は factory を作って処理する。
さて、AdalTsのmock版ができたので、Unit Test を作成する。Tasks という AdalTs の関数を使っているモジュールがあるので、これを対象とする。
tasks.test.tsx
---
import * as React from "react";
import {shallow, mount} from "enzyme";
jest.mock("../components/adal/adal-ts");
import Tasks from "../components/test";
it("test adal", () => {
const wrapper: any = shallow(<Tasks search="" />);
const divs = wrapper.find(".taskitem");
expect(divs.length).toEqual(2);
});
---
jest.mock() の順番が重要であることは前述した。これのあとに、AdalTSを使用しているモジュール群を import することにより、mock 版を引っ張ってくる。
Tasks モジュールでは、adalRequest() でAPIから読み込んだ [{}] を、ループし、以下のような html をレンダーする.
<div class="taskitem">
ID: V089231, Action: II: Invoice Submission
</div>
mock 版では、2個の要素を持つ array を返しているので、wrapper.find(".taskitem")の length は、2とならねば assert error となる。
上を走らすと残念、 assert error となる。expect() でブレークしてみると、divs.length が 0 となっている。wrapper.instance().rows.length も 0 となっている。。。
よく考えればあたりまえである。adalRequest() は非同期処理である。enzyme の shallow ()は左様な事は知る由も無く、非同期処理が終了する前にスルッと抜けてくる。
調べた結果、以下が宜しいようだ。
import * as React from "react";
import {shallow, mount} from "enzyme";
jest.mock("../components/adal/adal-ts");
import DsFactory from "../factory";
import TasksTest from "../components/test";
it("test adal", () => {
const wrapper: any = shallow(<TasksTest search="" />);
return DsFactory.GetAdalTs().adalRequest({}).then((r: any) => {
wrapper.update();
const divs = wrapper.find(".taskitem");
expect(divs.length).toEqual(r.data.length);
});
});
DsFactoryモジュールは、前述した single instance を保つための factory 。shallow() の後に、Tasks が呼んでいるadalRequest()を走らせ、promise.resolve を待ってから shallow を updateする。その後各種 assert を実施するというものである。余談になるが、ここは少々疑問が残っている。このような単純例では、9割9分、promise のタイミングはOKであろうが、複雑な 非同期処理や render の場合、shallow() から invoke された非同期処理の完了と、その次の行でinvokeしている非同期処理の完了、常に後者が遅く完了するとは限らないのではないかなあと思う。とりあえず今後の課題とするが、enzyme にpromise を待つようなオプションがあればベスト。
今回テストの対象は、小さなアプリで、Web API / React の組み合わせを TypeScript で書いたもの。Azure AD への oAuth2 と、Axios を使った API (Rest) call が存在し、これらを mock してみよう、というお題。が、壁が厚すぎるので、まずは簡単な、エラー表示だけをつかさどる Error というモジュールを Mock してみる。
error.tsx (actual):
---
import * as React from "react";
interface Props {
errors: string
}
interface State {
}
export default class Error extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
}
public render() {
return <h3>{this.props.errors}</h3>;
}
}
./__mock__/error.tsx (mock 版)
mock版は、actual版 の同フォルダに __mock__ フォルダを作成しその直下に置かねばならない
+ error.tsx (actual)
+ __mock__
+ error.tsx (mock)
---
import * as React from "react";
interface Props {
errors: string
}
interface State {
}
export default class Error extends React.Component<Props, State> {
public render() {
return (<h3>{"I am mock"}</h3>);
}
}
次に、 Unit Test を書くが、これが動作するようになるまで3日かかってしまった。分かってしまえば簡単。
error.test.tsx
---
import * as React from "react";
import {shallow, mount} from "enzyme";
import * as renderer from "react-test-renderer";
jest.mock("../components/error");
import Errors from "../components/error";
const errorMessage = "hello world!";
test.only("test error component", () => {
const wrapper = shallow(<Errors errors={errorMessage} />);
const tobe = "<h3>" + errorMessage + "</h3>";
expect(wrapper.text()).toEqual(errorMessage);
// expect(wrapper).toHaveProp("errors");
});
FAIL src\__tests__\error.test.tsx
× test error component (15ms)
● test error component
expect(received).toEqual(expected)
Expected value to equal:
"hello world!"
Received:
"I am mock"
at Object.<anonymous> (src/__tests__/error.test.tsx:11:28)
at new Promise (<anonymous>)
at <anonymous>
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 2.19s
Ran all test suites.
ばんざーい。
jest.mock() を Unit Test の中に置きたい場合は、
test.only("test error component", () => {
jest.mock("../components/error");
const Errors = require("../components/error").default;
const wrapper = shallow(<Errors errors={errorMessage} />);
const tobe = "<h3>" + errorMessage + "</h3>";
expect(wrapper.text()).toEqual(errorMessage);
});
にてイケる。以上、基礎編。
===
いよいよ、Adal oAuth と Ajax request を司るモジュールを mock してみる。これは AdalTs という名前で、以下4つの public methodを提供する。
Init()
isAuthenticated()
adalRequest()
logOut()
Web API からRestでデータを持ってくるのが adalRequest() であり、ここで Azure に認証されていないと oAuth2 を開始する。されていれば、Axion を使って API に GET しにいくが、ここは無論非同期処理であり Promise を返す。
これの mock 版を作る。
./__mocks__/adal-ts.tsx
---
export default class AdalTs {
public Init(): any {
return true;
}
public isAuthenticated = () => {
const promise = new Promise((resolve, reject) => {
resolve();
});
return promise;
}
public adalRequest(): any {
console.log("---mock adalRequest called");
const promise = new Promise((resolve, reject) => {
const d = this.taskdata;
resolve({ data: d});
});
return promise;
}
public logOut() {
return {};
}
private taskdata: any = [
{
Id: 1307327,
OIDString: "V089231",
Action: "Notification: Review and modify as required, and proceed the WorkFlow.",
Days: "56 days"
},
{
Id: 1319423,
OIDString: "V089539",
Action: "II: Invoice Submission",
Days: "29 days"
}
];
actial 版の各 public methodを書き換えているが、adalRequest() では、Web API GETに変わり、固定のダミーデータをpromise.resolve に返すというもの。
多少余談となるが、この AdalTsはもともと singleton で作っていた。即ち、export new AdalTs() し、constructor を細工して同一インスタンスを返すようになっていた。Unit Test では、これは使えない。たとえば、jest.mock("../adal-ts")とやって、その実態が class (type)ではなくて インスタンスだとエラーとなる。mock の対象となるモジュールは singleton はダメであり、単一インスタンスが必要な場合は factory を作って処理する。
さて、AdalTsのmock版ができたので、Unit Test を作成する。Tasks という AdalTs の関数を使っているモジュールがあるので、これを対象とする。
tasks.test.tsx
---
import * as React from "react";
import {shallow, mount} from "enzyme";
jest.mock("../components/adal/adal-ts");
import Tasks from "../components/test";
it("test adal", () => {
const wrapper: any = shallow(<Tasks search="" />);
const divs = wrapper.find(".taskitem");
expect(divs.length).toEqual(2);
});
---
jest.mock() の順番が重要であることは前述した。これのあとに、AdalTSを使用しているモジュール群を import することにより、mock 版を引っ張ってくる。
Tasks モジュールでは、adalRequest() でAPIから読み込んだ [{}] を、ループし、以下のような html をレンダーする.
<div class="taskitem">
ID: V089231, Action: II: Invoice Submission
</div>
mock 版では、2個の要素を持つ array を返しているので、wrapper.find(".taskitem")の length は、2とならねば assert error となる。
上を走らすと残念、 assert error となる。expect() でブレークしてみると、divs.length が 0 となっている。wrapper.instance().rows.length も 0 となっている。。。
よく考えればあたりまえである。adalRequest() は非同期処理である。enzyme の shallow ()は左様な事は知る由も無く、非同期処理が終了する前にスルッと抜けてくる。
調べた結果、以下が宜しいようだ。
import * as React from "react";
import {shallow, mount} from "enzyme";
jest.mock("../components/adal/adal-ts");
import DsFactory from "../factory";
import TasksTest from "../components/test";
it("test adal", () => {
const wrapper: any = shallow(<TasksTest search="" />);
return DsFactory.GetAdalTs().adalRequest({}).then((r: any) => {
wrapper.update();
const divs = wrapper.find(".taskitem");
expect(divs.length).toEqual(r.data.length);
});
});
DsFactoryモジュールは、前述した single instance を保つための factory 。shallow() の後に、Tasks が呼んでいるadalRequest()を走らせ、promise.resolve を待ってから shallow を updateする。その後各種 assert を実施するというものである。余談になるが、ここは少々疑問が残っている。このような単純例では、9割9分、promise のタイミングはOKであろうが、複雑な 非同期処理や render の場合、shallow() から invoke された非同期処理の完了と、その次の行でinvokeしている非同期処理の完了、常に後者が遅く完了するとは限らないのではないかなあと思う。とりあえず今後の課題とするが、enzyme にpromise を待つようなオプションがあればベスト。
コメント
コメントを投稿