Python
Selenium
unittest

E2EテストをPageObjectパターンで実装する

E2Eテスト実装のベストプラクティスとされているPageObjectパターンを試してみました。

テストケース

Djangoの管理画面からユーザ作成を行うテストを実装しました。
流れは次のとおりです。

  1. ログイン画面でユーザ名、パスワードを入力して[ログイン]をクリック
  2. 管理画面でユーザーの[+追加]をクリック
  3. ユーザ管理画面でユーザ名、パスワード、パスワードの確認を入力して[保存]をクリック
  4. ユーザ管理画面にユーザ作成完了のメッセージが表示されていることをテスト

PageObjectの実装

PageObjectはページ単位で作成するので、今回のケースだと次のPageObjectが必要になります。

  • LoginPage
    • ログイン画面
  • AdminPage
    • ログイン後に表示される管理画面
  • AddUserPage
    • 管理画面でユーザーの[+追加]をクリックした後に表示されるユーザ追加画面
  • UserPage
    • ユーザ追加完了後に表示される画面

ページ内で実行する操作をメソッドとして実装しますが、フォームへの入力、クリックなどの細かい単位ではなく、ある程度まとまったタスクをメソッドにします。
具体的には次のようにしました。

  • LoginPage
    • loginメソッド
      • ユーザ名、パスワードを入力して[ログイン]をクリック
  • AdminPage
    • go_to_add_user_pageメソッド
      • ユーザーの[+追加]をクリック
  • AddUserPage
    • add_userメソッド
      • ユーザ名、パスワード、パスワードの確認を入力して[保存]をクリック
  • UserPage
    • 今回は特に操作しないのでメソッドはなし

コード

実装したコードは次のとおりです。以下のリポジトリにも同じコードがあります。
https://github.com/shiimaxx/page-object-pattern-example

ログインページ

login.py
from pages.admin import AdminPage


class LoginPage(object):

    def __init__(self, driver):
        self.driver = driver

    def login(self, username, password):
        login_form = self.driver.find_element_by_id('login-form')
        username_ = login_form.find_element_by_name('username')
        password_ = login_form.find_element_by_name('password')
        button = login_form.find_element_by_css_selector('input[value="ログイン"]')
        username_.send_keys(username)
        password_.send_keys(password)
        button.click()
        return AdminPage(self.driver)

管理ページ

admin.py
from pages.user import AddUserPage


class AdminPage():

    def __init__(self, driver):
        self.driver = driver

    def go_to_add_user_page(self):
        self.driver.find_element_by_css_selector('a[href="/admin/auth/user/add/"]').click()
        return AddUserPage(self.driver)

ユーザ管理ページ

user.py
class AddUserPage(object):

    def __init__(self, driver):
        self.driver = driver

    def add_user(self, username, password):
        user_form = self.driver.find_element_by_id('user_form')
        username_ = user_form.find_element_by_name('username')
        password1 = user_form.find_element_by_name('password1')
        password2 = user_form.find_element_by_name('password2')
        button = user_form.find_element_by_name('_save')
        username_.send_keys(username)
        password1.send_keys(password)
        password2.send_keys(password)
        button.click()
        return UserPage(self.driver)


class UserPage(object):

    def __init__(self, driver):
        self.driver = driver

テスト

test_django_admin.py
import unittest

from selenium import webdriver
from selenium.webdriver.chrome.options import Options

from pages.login import LoginPage


class TestDjangoAdmin(unittest.TestCase):

    def setUp(self):
        options = Options()
        options.binary_location = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
        options.add_argument('--headless')
        options.add_argument('--disable-gpu')
        self.driver = webdriver.Chrome(chrome_options=options)
        self.driver = webdriver.Chrome()
        self.driver.implicitly_wait(5)
        self.driver.set_page_load_timeout(30)
        self.driver.set_window_size(1920, 1080)

    def test_add_user(self):
        self.driver.get('http://127.0.0.1:8000/admin')
        login_page = LoginPage(self.driver)
        admin_page = login_page.login('admin', 'p@ssword')
        add_user_page = admin_page.go_to_add_user_page()
        user_page = add_user_page.add_user('testuser', 'dummy_p@ssword')
        self.assertIn('testuser</a>" を追加しました。続けて編集できます。</li>', user_page.driver.page_source)

    def tearDown(self):
        self.driver.close()

まとめ

今回は1つのテストケースしか実装しなかったのでPageObjectパターンの恩恵はそこまでないかもしれませんが、テストを運用していくことを想定すると、テストケースが読みやすかったり、PageObjectやメソッドの再利用によってテストケースが追加しやすくなっていたりなど、保守性は上がっている状態だと思います。

参考文献