Page Object Pattern を React Component テストでも利用する

reactjssoftware test
tomoyukikashiro
tomoyukikashiro

E2E テストで利用する Page Object Pattern を Component テストでも利用することで可読性の高いテストがかけるのではないだろうかと思ってメモに残します。

Page Object Pattern とは

https://webdriver.io/docs/pageobjects/

E2E テストでは、以下2つのコードが混在することになり、テストがしたいことがぱっと見でわかりにくいことがあります。

  • テストコード(例:フォームにinvalidなemailが入力されている場合、エラーが表示すること
  • テストするためにページを操作するコード(例:フォームに文字を入力または、フォームのエラー文字を取得)

さらに、 テストするためにページを操作するコード が点在していると、フォームの id や name を修正しただけで、点在するコードをすべて修正することになりメンテナンス性も損なわれがちです。

Page Object Pattern では、テストするためにページを操作するコード を オブジェクトでひとまとめにして、使い回すことで、テストの可読性やメンテナンス性を上げる方法です。

Page Object Pattern の例

https://webdriver.io/docs/pageobjects/ の例をそのまま利用すると、、

// selenium など E2E テストライブのインスタンスを保持する親クラス
import Page from './page'

class LoginPage extends Page {

    // ログインページのテストで使う `テストするためにページを操作するコード`
    get username () { return $('#username') }
    get password () { return $('#password') }
    get submitBtn () { return $('form button[type="submit"]') }
    get flash () { return $('#flash') }
    get headerLinks () { return $$('#header a') }

    async open () {
        await super.open('login')
    }

    async submit () {
        await this.submitBtn.click()
    }

}

export default new LoginPage()

これを、テストコードで使いまわしますことで、 テストするためにページを操作するコード がシンプルになり、テストコードが読みやすく、またページに変更があっても Page Object を修正するだけで済み、テストコードへの影響を抑えることができます。

it('should deny access with wrong creds', async () => {
    await LoginPage.open()
    await LoginPage.username.setValue('foo')
    await LoginPage.password.setValue('bar')
    await LoginPage.submit()

    await expect(LoginPage.flash).toHaveText('Your username is invalid!')
})

React Component テストで Page Object Pattern を 利用する

import type { ComponentProps } from "react";
import { render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import LoginForm from "./LoginForm";

type Props = ComponentProps<typeof LoginForm>

const setup = (props): Props => {
  const { getByRole, getByLabelText, getByText } = render(<LoginForm {...props} />);
  return {
    typeEmail: (email: string) => userEvent.type(getByLabelText("email"), email),
    getByText,
    submit: () => userEvent.click(getByRole("button"))
  };
};


it("invalidなemailの場合、エラーを表示する。", () => {
  const utils = setup();
  utils.typeEmail("invalidemail");
  utils.submit();

  expect(utils.getByText("Emailが正しくありません。")). toBeInTheDocument();
});