注: これが正解かはわかっていない
一度にいくつかのe2eテストを高速に実行する目的で並列にしたくて、かつブラウザのコンテキストつまりCookieなどをテスト間で共有したくない場合がある。
たとえば、並列実行したい2つのテストがそれぞれwebサービスのログイン情報が異なるみたいな時などが挙げられる。
残念ながらPlaywright Javaはスレッドセーフではないので、そういったことをするには少々の工夫が必要になってしまう。。。
今回は並列に実行するためのアプローチとしてこのような対応方法を考えた。
今回はgauge-javaと併せて使いたいという事情があり、gauge-javaがスレッドを複数利用することでテストを並列実行するという要件だったので、今回は2のパターンを頑張る必要があった。
実装
Playwrightの Browser
BrowserContext
Page
あたりの値をスレッドごとに管理するシングルトンを用意し、そこからPageを取得するようにしてみる。
BrowserContext, Pageに関してはテストケースごとに都度初期化して利用したい。ゆえにこのような関数が必要になる。
- initPage: スレッドローカルなPageインスタンスの生成, @BeforeSpecから呼ばれる
- getPage: Pageインスタンスの取得, テストケースから
- removePage: Pageインスタンスの破棄, @AfterSpecから
- removeBrowser: Browserインスタンスの破棄, @AfterAllから
object BrowserManager { // ブラウザ設定 private val launchOptions = BrowserType.LaunchOptions().apply { headless = false } // スレッドごとにBrowser, Context, Pageを保持する private val threadLocalBrowser: ThreadLocal<Browser> = ThreadLocal() private val threadLocalContext: ThreadLocal<BrowserContext> = ThreadLocal() private val threadLocalPage: ThreadLocal<Page> = ThreadLocal() fun initPage(): Unit { println("[${Thread.currentThread().name}] initPage") // すでにスレッドに対応するPageが存在する場合はそれを返す val page: Page? = threadLocalPage.get() page ?: run { // Pageが存在しない場合は作成する val browser = threadLocalBrowser.getOrSet { val playwright = Playwright.create() playwright.chromium().launch(launchOptions) } val context = browser.newContext() val newPage = context.newPage() threadLocalContext.set(context) threadLocalPage.set(newPage) } } fun getPage(): Page { println("[${Thread.currentThread().name}] getPage") // Pageが存在していることを前提として取得する threadLocalPage.get()?.let { return it } ?: run { throw IllegalStateException("Page is not initialized") } } fun removePage() { println("[${Thread.currentThread().name}] removePage") // BrowserContext, Pageが存在していたら破棄する threadLocalPage.get()?.let { it.close() threadLocalPage.remove() } threadLocalContext.get()?.let { it.close() threadLocalContext.remove() } } fun removeBrowser() { println("[${Thread.currentThread().name}] removeBrowser") threadLocalBrowser.get()?.let { it.close() threadLocalBrowser.remove() } } }
並行動作サンプル
並行動作している例はこのように実装した。
実際はgaugeで実行することになるが、紙面の都合で簡易的に自分で2スレッド生成してBrowserManagerを呼び出している。
fun main() { println("start") val thread1 = Thread { // BeforeSpec想定 BrowserManager.initPage() // Spec想定 val page = BrowserManager.getPage() page.navigate("https://www.google.com") println("[${Thread.currentThread().name}] Page: ${page.title()}") page.navigate("https://www.yahoo.co.jp") println("[${Thread.currentThread().name}] Page: ${page.title()}") // AfterSpec想定 BrowserManager.removePage() // AfterAll想定 BrowserManager.removeBrowser() } val thread2 = Thread { // BeforeSpec想定 BrowserManager.initPage() // Spec想定 val page = BrowserManager.getPage() page.navigate("https://www.google.com") println("[${Thread.currentThread().name}] Page: ${page.title()}") page.navigate("https://www.yahoo.co.jp") println("[${Thread.currentThread().name}] Page: ${page.title()}") // AfterSpec想定 BrowserManager.removePage() // AfterAll想定 BrowserManager.removeBrowser() } thread1.start() thread2.start() thread1.join() thread2.join() println("end") exitProcess(0) }
結果として、出力はこのようになる。
2スレッドで並行してブラウザインスタンスが生成され、スレッド数ぶんのChromiumのプロセスを立ち上げてwebアクセスしていることがわかる。
start [Thread-0] initPage [Thread-1] initPage [Thread-1] getPage [Thread-0] getPage [Thread-1] Page: Google [Thread-0] Page: Google [Thread-1] Page: Yahoo! JAPAN [Thread-1] removePage [Thread-1] removeBrowser [Thread-0] Page: Yahoo! JAPAN [Thread-0] removePage [Thread-0] removeBrowser end
雑
ThreadLocal使えば良いんじゃないの的な提案をChatGPTに教えてもらったのでマジで便利だなっていうのと、ThreadLocal
の使い方を知らなかったので非常に勉強になった。
AIつかってプログラムの改善をすることで自分の知識が深まって引き出しが増えてよいですね。