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>;
    }
}

./__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>);
    }
}

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 を待つようなオプションがあればベスト。

コメント

このブログの人気の投稿

HiddenFor 要注意

SPA を IIS から流す際の ASP 側のルーティング

Jest の テスト・スクリプトをデバッグする術