DeNA Testing Blog

Make Testing Fun, Smart, and Delighting End-Users

Golangでtestingことはじめ(1)

こんにちは。
Golangが一般的に使われるようになってきてもう久しいですね。
最近作られたSWET製のツールでも、Golangを採用したものがあります。
そこで、Golangの標準テストパッケージtestingやその他についてまとめたいと思います。

今回から3回にわたり、

  • testingパッケージを使ったユニットテスト(testing
  • テストにおける共通処理(testing
  • アプリケーションのテスト(gomock, httptest

を紹介します。

この記事を読んで一通りGolangでテストがかけるようになると嬉しいです。

この文章中に登場するサンプルは GitHub にありますので、実際に動作させることが可能です。

$ go get github.com/duck8823/sample-go-testing
$ cd $GOPATH/src/github.com/duck8823/sample-go-testing
$ git checkout refs/tags/blog
$ dep ensure # 依存パッケージのダウンロード

なお、文章中のコマンドは全てバージョン1.9.2での動作を確認しています。

testingパッケージ

testingパッケージはGolangが標準で提供している、テスト全般を支援するパッケージです。
ベンチマークやカバレッジ、標準出力のテストなどカバーしている範囲は広く、サードパーティ製のテストフレームワークに頼らずともテストが記述できます。 標準なのでGolangをインストールしたら使えます。パッケージマネージャなどで指定する必要はありません。
以下のtypeについてユニットテストを書いてみましょう。

animals/animal.go

package animals

import (
    "fmt"
    "github.com/duck8823/sample-go-testing/foods"
)

type Duck struct {
    name string
}

func NewDuck(name string) *Duck {
    return &Duck{name}
}

func (duck *Duck) Say() string {
    return fmt.Sprintf("%s says quack", duck.name)
}

func (duck *Duck) Eat(food foods.Food) string {
    return fmt.Sprintf("%s ate %s", duck.name, food.Name())
}

foods/food.go

package foods

type Food interface {
    Name() string
}

type Apple struct {
    cultivar string
}

func NewApple(cultivar string) *Apple {
    return &Apple{cultivar}
}

func (apple *Apple) Name() string {
    return apple.cultivar
}

testing では、 _test.go で終わる名前のファイルにテストコードを書きます。
また、TestXxxで始まる関数についてテストを実行します。

なお、テストコードは対応するソースコードと同一のパッケージにすることでpackage privateな変数や関数を呼び出すことができ、 APIを必要以上公開せずにテストを書くことができます。

animals/animals_test.go

package animals

import "testing"

func TestDuck_name(t *testing.T) {
    duck := &Duck{"tarou"}
    actual := duck.name
    expected := "tarou"
    if actual != expected {
        t.Errorf("got: %v\nwant: %v", actual, expected)
    }
}

パッケージ名_test をテストコードを記述するパッケージ名として利用することもできます。 パッケージ外に公開したAPIを通してのみテストしたい場合は、こちらを利用するとよさそうです。
この場合、テスト対象のパッケージもインポートする必要があります。

animals/animals_01_test.go

package animals_test

import "testing"
import "github.com/duck8823/sample-go-testing/animals"

func TestDuck_Say(t *testing.T) {
    duck := animals.NewDuck("tarou")
    actual := duck.Say()
    expected := "tarou says quack"
    if actual != expected {
        t.Errorf("got: %v\nwant: %v", actual, expected)
    }
}

テストを記述したら実行しましょう。 go testの引数にはテストが記述されたファイルを指定することができます。 テストがプロダクトコードと同一のパッケージの場合、引数にテスト対象のファイルを含める必要があります。

$ go test animals/animals_test.go animals/animal.go

プロダクトコードとテストコードのパッケージを分けてimportしている場合は、単一のファイルでも実行可能です。

$ go test animals/animals_01_test.go

他の言語ではビルドツールやテストランナーを介して実行するものが多い中、 goのサブコマンドとしてtestを実行できることからも、言語として積極的にサポートしているのがわかります。

go testの引数はファイルの他、github.com/duck8823/sample-go-testing/animalsのようにパッケージを指定することもできます。 上記の記述だと冗長的になってしまいますが、パッケージの指定はディレクトリからの相対パスとして記述することもできます。

$ go test ./animals

パッケージングされたアプリケーションを作成している場合は ./... を指定することで全ディレクトリのテストを再帰的に実行してくれます。

$ go test ./...

(1.9より以前のバージョンの場合は)

$ go test $(go list ./... | grep -v /vendor)

実行結果は以下のように表示されます。

ok      command-line-arguments  0.007s

詳細な結果を見たい場合は、-vオプションが有効です。

$ go test -v animals/animals_01_test.go
=== RUN   TestDuck_Say
--- PASS: TestDuck_Say (0.00s)
PASS
ok      command-line-arguments  0.007s

アサーション

標準のtestingパッケージでは、アサーション用の関数が用意されていません。
Errorf などの関数を利用し、自分でエラーメッセージを書く必要があります。

duck := animals.NewDuck("tarou")
actual := duck.Say()
expected := "tarou says quack"
if actual != expected {
    t.Errorf("got: %v\nwant: %v", actual, expected)
}

testify のようなサードパーティ製のアサーションライブラリも存在しています。
これらのライブラリを利用することで、記述が容易になります。

import "github.com/stretchr/testify/assert"
duck := animals.NewDuck("tarou")
actual := duck.Say()
expected := "tarou says quack"
assert.Equals(t, expected, actual)

サブテスト

テストを階層構造にすることができます。
サブテストはテストケース内で t.Run をコールすることで実現できます。
第一引数にはテストケース名、第二引数には無名関数を渡すことができます。

animals/animals_02_test.go

package animals_test

import (
    "github.com/duck8823/sample-go-testing/animals"
    "github.com/duck8823/sample-go-testing/foods"
    "testing"
)

func TestDuck(t *testing.T) {
    duck := animals.NewDuck("tarou")

    t.Run("it says quack", func(t *testing.T) {
        actual := duck.Say()
        expected := "tarou says quack"
        if actual != expected {
            t.Errorf("got: %v\nwant: %v", actual, expected)
        }
    })

    t.Run("it ate apple", func(t *testing.T) {
        apple := foods.NewApple("sunfuji")

        actual := duck.Eat(apple)
        expected := "tarou ate sunfuji"
        if actual != expected {
            t.Errorf("got: %v\nwant: %v", actual, expected)
        }
    })
}

テスト結果は以下のように表示されます。

$ go test -v ./animals/animals_02_test.go 
=== RUN   TestDuck
=== RUN   TestDuck/it_says_quack
=== RUN   TestDuck/it_ate_apple
--- PASS: TestDuck (0.00s)
    --- PASS: TestDuck/it_says_quack (0.00s)
    --- PASS: TestDuck/it_ate_apple (0.00s)
PASS
ok      command-line-arguments  0.006s

ベンチマーク

Golangのtestingパッケージでは、ベンチマークも標準でサポートしており、簡単に記述することができます。 ベンチマークは、関数を以下のように定義することで実現できます。

func BeanchmarkXxx(b *testing.B) {
    for i := 0; i < b.N; i++ {
        //  ここに処理を記述
    }
}

animals/animals_06_test.go

package animals_test

import (
    "github.com/duck8823/sample-go-testing/animals"
    "github.com/duck8823/sample-go-testing/foods"
    "testing"
)

func BenchmarkDuck_Say(b *testing.B) {
    duck := animals.NewDuck("tarou")

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        duck.Say()
    }
    b.StopTimer()
}

func BenchmarkDuck_Eat(b *testing.B) {
    duck := animals.NewDuck("tarou")
    food := foods.NewApple("sunfuji")

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        duck.Eat(food)
    }
    b.StopTimer()
}

ベンチマークの実行は -bench オプションをつけます。

$ go test -v -bench=. ./animals/animals_06_test.go
goos: darwin
goarch: amd64
BenchmarkDuck_Say-4     10000000               139 ns/op
BenchmarkDuck_Eat-4      5000000               407 ns/op
PASS
ok      command-line-arguments  3.965s

出力結果はループの回数とループ1回あたりの処理時間です。 -benchmem オプションをつけることで、メモリに関するベンチマークも取得することができます。

前処理や後処理がある場合は ResetTimerStopTimer 関数を呼び出して正しく測定できるようにしましょう。

func BeanchmarkXxx(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        //  ここに処理を記述
    }
    b.StopTimer()
}

カバレッジ

カバレッジも標準でサポートされています。
-cover オプションをつけることで、テスト実行結果にカバレッジ情報が付加されます。

$ go test -cover ./animals
ok      github.com/duck8823/sample-go-testing/animals   10.014s coverage: 100.0% of statements

カバレッジレポートの出力

カバレッジレポートを出力するためにには -coverprofile オプションを利用しますが、 バージョン1.9(2018/01/08現在)では複数のパッケージにまたがって利用することができません。

$ go test -cover -coverprofile cover.out ./animals ./foods
cannot use test profile flag with multiple packages

1.10からはサポートされるようなので、試してみましょう。

beta版のインストールは以下の手順で行います。

go get golang.org/x/build/version/go1.10beta1
go1.10beta1 download

go コマンドをインストールした go1.10beta1 に置き換えて実行してみましょう。

$ go1.10beta1 test -cover -coverprofile cover.out ./animals ./foods
ok      github.com/duck8823/sample-go-testing/animals   10.009s coverage: 100.0% of statements
ok      github.com/duck8823/sample-go-testing/foods 0.006s  coverage: 100.0% of statements

cover.outの出力結果は以下のようになりました。

mode: set
github.com/duck8823/sample-go-testing/foods/food.go:11.39,13.2 1 1
github.com/duck8823/sample-go-testing/foods/food.go:15.35,17.2 1 1
github.com/duck8823/sample-go-testing/animals/animal.go:12.33,14.2 1 1
github.com/duck8823/sample-go-testing/animals/animal.go:16.32,18.2 1 1
github.com/duck8823/sample-go-testing/animals/animal.go:20.47,22.2 1 1

-coverprofile で出力されるカバレッジレポートは見にくいので、 htmlとして出力しましょう。
カバレッジレポートからhtmlを生成するのも標準機能です。

go tool cover -html=cover.out -o cover.html

f:id:swet-blog:20180115132650p:plain

最後に

今回は紹介しませんでしたが他にもテストのスキップや、標準出力のテストが容易になるExampleといった魅力的な機能があります。 Golangは 公式ドキュメント も充実しているので、是非ご覧ください。

次回はtestingパッケージを利用して共通処理(Before/After)を実現する方法について紹介します。

iOS Test Night 1周年を振り返る

iOS Test Nightを立ち上げたSWETの@tarapoです。

2016/11/18にはじまった「iOS Test Night」は2017/12/5の「iOS Test Night #6」で1周年を迎えました。

f:id:swet-blog:20171220185651p:plain

今回は、その1年を振り返り次の1年の抱負を語りたいと思います。

iOS Test Nightとは

iOS Test Nightとは、SWETが主催するiOSのテストに関係することについて語り合うことを目的とした勉強会です。

本勉強会をはじめた理由は、日本語でのiOSのテストに関係する情報の少なさにあります。 しかし、皆さんテストに関することは興味があるでしょうし、何かしら行っていると思っています。そこで、情報を共有する場所があれば良いのではと思ったのが発端です。

そして、本勉強で知見を得た人が何かしらアクションを起こすことにより、良いスパイラルが生まれれば情報が自然と増えていくだろうと考えました。

どれぐらい本勉強会により情報が増えたかは分かりませんが、iOS Test Night #6では本勉強会きっかけでテストをはじめた方の発表枠をもうけたところ全て埋まりました。 何かしらの良いスパイラルは生まれつつあると思っています。

参加登録数・発表数

この1年間(合計6回)のiOS Test Nightの参加登録数と発表数は以下のとおりです。

登録数 発表数
712 47(31)
  • 登録数はキャンセルを除いたconnpassへの登録数になります。
  • 発表数の()内はユニークな人数になります。

発表数は40をこえており、ここ1年で40以上ものiOSのテストに関係する情報が共有されたことになります。 これらの発表資料についてはTest Nightグループの資料を見ていただくのが良いと思います。

この発表資料の中からSWETメンバーの発表について軽く振り返りたいと思います。

ふりかえり

主催であるSWETから毎回登壇させて頂いていますが、この1年間でSWETのメンバーが発表した回数は9回になります。

最初の発表

最初に発表したのは「iOSアプリの自動テストをはじめよう」という内容で以下がその時の発表資料です。

本発表は、自動テストをはじめる際の注意点などを紹介したものです。 今後、自動テストをはじめる際にこの注意点を思い出してもらえればと思い、私が経験したことをベースにしつつ作成したものです。

SWETの発表の傾向

発表した内容を大まかに分類すると以下のような2パターンに分けられるかと思います。

上記にあるように、自動テストに関する知見の紹介や、自動テスト(主にUIテスト)をやりはじめた後にある大変なことの知見についての情報共有を行なってきました。

UIテストについて並列化を行いやすくなってきたのはXcode8、Xcode9になってからです。 そのためこの1年ではUIテストの並列化に対する話が多くなっていると言えます。

また、上記以外にもSWETが開発しているサービスについての紹介を1回しています。 あまりiOS Test Nightでは発表していないですが、SWETで開発しているサービスは色々ありますので、そのうち本ブログで紹介できればと思います。

あれから1年

iOS Test Nightをはじめて1年間。 この1年間の間に、iOSにおけるテスト周りには色々な変化がありました。

Xcode8からBuilding for TestingとTest Without Buildingが出来るようになり、Xcode9からはシミュレーターの多重起動も出来るようになりました。

その結果、UIテストにおける実行時間の短縮が以前に比べてやりやすくなっています。

またXcode9での大きな変化は、是非ともWhat's New in Testingと題したWWDC2017の発表をまだ見ていない方は見てもらうのが良いかと思います。

このように1年間という期間は短いようで、非常に色々なことが起こります。 今まで出来なかったことが出来るようになったり、大変だったことが簡単に出来るようになったりもします。

まだまだiOS Test Nightのネタは尽きそうにありません。

これからのTest Night

今回、本記事を執筆するにあたり今までに発表された方の資料を読み直しました。 最初はテストを知らないと言っていた方も、今では普通にテストの本を読んでいる方もいます。

是非、発表したときから今までの間にどのような変化があったかをiOS Test Nightで発表して欲しいなと思います。

これからのiOS Test Nightは次の1年においても、テストをこれからはじめる人にとってもテストで苦労している人にとっても、知見を共有できる有益な場として本勉強会が存在できればと思います。

最後に

最初、iOS Test Nightをはじめる時は登壇してくれる方や参加してくれる方がどれぐらいいるか不安でしたが、この1年間で多くの方に登壇・参加していただきました。

そして、多くの知見を共有してもらい懇親会で色々な話が出来ました。

次の1年も知見を共有する場として存在していきたいと思います。 次回のiOS Test Night #7でお会いしましょう。

ZaleniumをKubernetes/GKEで動かす

この記事はSelenium/Appium Advent Calendar 2017の第18日目です。

SWETのGLやってます@okitanです。

少し前ですが、「日本Seleniumユーザーコミュニティ」のエキスパートが教えるSelenium最新事情という連載にDockerでSelenium Gridを構築して複数マシンのブラウザ自動テストを行うという記事を書きました。 記事中では、公式のSelenium GridのDockerイメージを使う方法に加えて、現在SWETで注目して運用しているZaleniumを利用する方法を紹介しました。

本記事では、Kubernetesを利用し、Zaleniumの以下の2つの構築方法を紹介します。

なお、本記事ではKubernetesに関する用語が出て来ます。 KubernetesのドキュメントWEB+DB PRESS Vol.99の「実践Kubernetes」がわかりやすいと思います。

脱線しますが、WEB+DB PRESS Vol.99には、SWETの@tarappoが寄稿した「UIテスト自動化」も特集2として載ってますのでおすすめです。

Selenium GridとZaleniumについてのおさらい

Zaleniumは複数のマシンで分散してブラウザ操作の自動化を実現するSelenium Gridを構築するためのソフトウェアです。 Selenium Gridを利用するとGoogle Chrome・Firefox・Microsoft Edge等多種多様なブラウザを管理でき、クロスブラウザテスティングが簡単にできるようになります。 Zaleniumは公式のSelenium Gridに追加で以下の機能をもっています。 (Selenium Gridに関する詳細やZaleniumの機能の詳細については、元記事を読んでください)

  • Selenium nodeのオートスケール機能
  • ブラウザの動作をSelenium Gridの管理コンソール上からのライブで確認できる機能(ライブプレビュー)
  • ブラウザ動作の録画機能
    • 録画された動画をドライバのログと合わせてWeb上で一覧できるダッシュボードという機能があります

ここで注目したいのは、Selenium nodeのオートスケール機能です。自動テストを開発プロセスに組み込むにはある程度短い時間で開発に対してフィードバックを行う必要があります。 その上、自動テスト数が増えていくにしたがって増加する実行時間を短くするため、並列実行数を増やしていかないといけません。 そのために、実行時間が何分以内に収まるような自動テストの並列実行計画の戦略と、それを受け入れるだけのキャパシティのあるテスト実行環境が必要になります。 Zaleniumのオートスケール機能はこの後者の問題を解決してくれます。

そしてオートスケールと相性の良いのがクラウドです。 今日ではKubernetesを用いてクラウドでコンテナ群を管理するのが一般的です。 もちろん、ZaleniumもKubernetesに対応しています。 また、最終的にクラウドで動かすにしてもローカルで動作確認できると便利なので、自分の開発環境で試してみる用にminikubeでの説明もあわせておこないます。

なお、以下の環境で動作確認してます。

minikube上にZaleniumを立てる

事前準備

minikubeのインストール方法を簡単に説明します。 公式のminikubeのチュートリアルは、 deprecatedとなっているdocker-machine-driver-xhyveを利用しています。 本記事ではdocker-machine-driver-xhyveの代替となるHyperkit driverのインストール方法もあわせて紹介しておきます。

Homebrew Caskを利用してminikubeをインストールします。

$ brew cask install minikube

Hyperkit driverをダウンロードして配置します。

$ curl -LO https://storage.googleapis.com/minikube/releases/latest/docker-machine-driver-hyperkit \
    && chmod +x docker-machine-driver-hyperkit \
    && sudo mv docker-machine-driver-hyperkit /usr/local/bin/ \
    && sudo chown root:wheel /usr/local/bin/docker-machine-driver-hyperkit \
    && sudo chmod u+s /usr/local/bin/docker-machine-driver-hyperkit

KubernetesのCLIであるkubectlをインストールします。

$ brew install kubectl

これでminikubeを起動する準備が整いました。

minikubeの起動

Hyperkit driverを利用するので、--vm-driver=hyperkitを指定して起動し、Kubernetesのcontextをminikubeに向けます。 minikubeが使えるリソースは--cpu=4--memory=4096と指定できますので、利用するホストマシーンのスペックにあわせて調整してください。

$ minikube start --vm-driver=hyperkit
$ kubectl config use-context minikube

なお、筆者の環境では何度かminikubeが起動しないこともありました。 そのような場合、rm -rf ~/.minikubeと一旦環境をリセットしてから起動するとうまくいきました。

Zaleniumの起動

Dockerだけで起動していたZaleniumでは、/var/run/docker.sockをマウントすることで、ブラウザが動くコンテナをオートスケールさせることができていました。 Kubernetesにおいては、実際にブラウザが動くPodをオートスケールさせるために、ServiceAccountが必要になります。

$ kubectl create serviceaccount zalenium

作成したServiceAccountを利用してZaleniumのDeploymentを作成します。 ZaleiniumはKubernetes利用時には、app=zaleniumというラベルを利用しているので、作成時に指定しています。

$ kubectl run zalenium \
    --image=dosel/zalenium:3.8.1c \
    --overrides='{"spec": {"template": {"spec": {"serviceAccount": "zalenium"}}}}' \
    --labels=app=zalenium,role=grid \
    --port=4444 \
    -- start --chromeContainers 1 \
             --firefoxContainers 0 \
             --seleniumImageName elgalu/selenium:3.8.1-p3 \
             --videoRecordingEnabled false \
             --sendAnonymousUsageInfo false

作成したDeploymentに対応するServiceを作成してアクセスできるようにします。

$ kubectl expose deployment zalenium --type=NodePort
service "zalenium" exposed

初回作成時にはDockerイメージを取得する時間のため少々待つ必要があります。 kubectl get podsによりPodの状況を確認します。

$ kubectl get pods
NAME                        READY     STATUS    RESTARTS   AGE
zalenium-40000-547b2        1/1       Running   0          53m
zalenium-57d6bb658f-s7vrb   1/1       Running   0          53m

上記、zalenium-40000-547b2となっているのが、ブラウザが動くSelenium Nodeです。この40000は連番で、オートスケールして台数が増えるたびに数が増えていきます。 今回は起動のオプションでGoogle Chromeのコンテナを1つだけ作ると指定しているので合計で1つ起動しています。 また、zalenium-57d6bb658f-s7vrbとなっているのがZalenium本体です。

両者ともRunningになったら、起動は完了しているので、minikubeのipアドレスおよびZalenium ServiceのnodePortからSelenium GridのURLを調べます。

$ echo http://$(minikube ip):$(kubectl get service zalenium \
         -o go-template='{{(index .spec.ports 0).nodePort}}')/wd/hub
http://192.168.64.10:31243/wd/hub

Zaleniumの動作確認

ZaleniumのURLが取得できたので動作を確認します。

require "selenium-webdriver"

caps = Selenium::WebDriver::Remote::Capabilities.chrome(
  "name" => "hello zalenium",
  "recordVideo" => true
)

driver = Selenium::WebDriver.for(:remote,
  url: "http://192.168.64.10:31243/wd/hub", # (1)
  desired_capabilities: caps
)

# あとはブラウザを好きに操作してください
driver.get("http://www.selenium.jp/")
driver.quit

上記のスクリプトをselenium_script.rbとして保存し、下記のように実行してください。 なお、スクリプト中(1)の部分は上記のSelenium GridのURLが使われてますので、実行環境に応じて読み替えてください。

$ gem install selenium-webdriver
$ ruby selenium_script.rb

スクリプトの実行に伴い、http://192.168.64.10:31243/grid/admin/liveでブラウザ動作のライブプレビューが、http://192.168.64.10:31243/dashboard/で録画されたダッシュボードが確認できます。 何度かスクリプトを実行すると、ブラウザを動かすコンテナが動的に作成されて動作することが確認できます。

GKEでZaleniumを動かす

このように、ローカルに立てたminikube上でZaleniumの動作が確認できました。 そこで、今度はGKEで試してみましょう。

まずは、Google Cloud PlatformのドキュメントをみてGCPの設定とgcloudツールの設定を行ってください。

Kubernetesクラスタの作成

まずはKubernetesを動かすためのクラスタを作成します。

$ gcloud container clusters create zalenium \
         --num-nodes 1 \
         --machine-type g1-small

こちらはGCPのコンソールから作成することも可能なので、心配な人はGUIで作成してください。 クラスタを作成したらクラスタを操作するための認証情報を取得します。

$ gcloud container clusters get-credentials zalenium

クレデンシャルを取得すると、kubectl config current-contextで取得できるcontextがGKEに自動的に変更されます。 これでZaleniumを起動する準備ができました。

Zaleniumの起動

基本はminikubeと同様に実行できるのですが、クラウド上に作成した場合は第三者にアクセスされる可能性があるので今回はZaleniumに対してBasic認証をかけておきます。 Basic認証は--gridUser--gridPasswordで指定できます。

$ kubectl create serviceaccount zalenium
$ kubectl run zalenium \
    --image=dosel/zalenium:3.8.1c \
    --overrides='{"spec": {"template": {"spec": {"serviceAccount": "zalenium"}}}}' \
    --labels=app=zalenium,role=grid \
    --port=4444 \
    -- start --chromeContainers 1 \
             --firefoxContainers 0 \
             --seleniumImageName elgalu/selenium:3.8.1-p3 \
             --videoRecordingEnabled false \
             --sendAnonymousUsageInfo false \
             --gridUser swet --gridPassword test

この作成したDeploymentは以下のようにGKEのServiceとして公開することができます。

$ kubectl expose deployment zalenium --type=LoadBalancer

LoadBalancer起動後、以下のコマンドでSelenium GridのURLが取得できます。

$ echo http://swet:test@$(kubectl get service zalenium \
         -o go-template='{{{(index .status.loadBalancer.ingress 0).ip}}:{{(index .spec.ports 0).port}}')/wd/hub
http://test:swet@35.190.235.227:4444/wd/hub

ここでは、Basic認証の情報がURLについているので、--gridUser--gridPasswordを変えていた場合は適宜読み替えてください。 このBasic認証情報付きのURLはそのまま、先程のスクリプトの(1)のURLを置き換えて利用することができます。

また、Zaleniumのライブプレビュー画面とダッシュボードもBasic認証で守られていることが確認できます。

ちなみに、下のスクリーンショットはGKEを使って10並列でブラウザを動かしている様子です。

f:id:swet-blog:20171215194322p:plain
GKE上で10並列で動かしている様子

クラスタの削除

以上、GKEでZaleniumを利用可能なことがわかったのでクラスタを削除します。

$ gcloud container clusters delete zalenium

まとめ

以上のようにKubernetesを利用してクラウド上でZaleniumを利用可能となりました。 今回は導入手順を紹介するだけで実践的な内容は最小限にとどめています。 運用上注意することや工夫すること等は今後も紹介していきたいと思いますので楽しみにしていてください。

それでは楽しいクラウドでZaleniumライフを。

参考文献

iOSシミュレータでのUIテストの様子を録画してみよう

モバイル 自動化/自動テスト Advent Calendar 2017 14日目の記事です。

はじめまして、SWETグループの加瀬です(@Kesin11
12/05に行われたiOS Test Night #6ではLT枠で発表させて頂きました。

今回は発表で紹介したrecordVideoについての補足と、UIテストを録画する方法を改めて紹介したいと思います。

以後の内容についての動作確認は以下の環境で行いました。

  • Xcode 9.1
  • Ruby 2.3.4
  • Appium 1.7.1

recordVideoとは

xcrun simctl ioに含まれるiOSシミュレータの画面を録画するツールです。 XcodeのリリースノートによるとXcode 8.2より追加された機能です。

使い方はシンプルで、iOSシミュレータを立ち上げてから以下のコマンドを実行するだけです。

xcrun simctl io booted recordVideo ./test.mov

実行するとプロセスが立ち上がり、録画が開始されます。録画の終了はctrl+cです。

上記のコマンド中のbootedは「起動しているシミュレータ」という意味になります。以下のようにシミュレータのUDIDを指定することも可能です。

xcrun simctl io "FDF18984-04A9-4B9A-AD2C-E323152DFD03" recordVideo ./test2.mov

指定しているUDIDは自分の環境でのiOS 11.1、iPhone 5sのシミュレータのものです。

もう少し詳しいオプションなどはxcrun simctl io --helpで確認することができます。

recordVideoでUIテストの様子を録画する

recordVideoを使ってiOSシミュレータの画面を録画することができたので、次はUIテストの開始と終了に合わせて自動的に録画の開始と終了を実行させてみましょう。

今回は以下のシェルスクリプトを作成して簡単に制御してみました。

recorder.sh

# recordVideoをバックグラウンドで実行
xcrun simctl io booted recordVideo screenshots/test.mov &

# プロセスIDを保存
PID=`echo $!`

# テスト実行
# この例ではAppium + RSpectでテストを実行しています
bundle exec rspec spec/scenario_test.rb

# バックグラウンドのrecordVideoにSIGINTシグナルを送信
kill -2 $PID

これだけでテストの起動と終了に合わせて録画ができるようになりました。

Appium + RSpecと組み合わせる

テストに合わせて録画することはできましたが、以下の点をまだ改良できそうです

  • 録画するiOSシミュレータの指定がbootedなので複数のシミュレータが起動している場合は選択できない。UDIDを指定する場合は調べるのが大変
  • UIテスト全体が録画されるため、失敗したテストケースが動画のどの部分か調べることが大変

これらの問題を解決するためAppium + RSpecでのテストに組み込むためのコードをRubyで書いてみました。

recorder.rb

require 'open3'
require 'json'

class Recorder
  attr_reader :udid, :pid, :file_path

  def initialize(driver)
    @udid = _get_udid(driver)
  end

  def _get_udid(driver)
    device_name = driver.caps[:deviceName]
    platform_version = driver.caps[:platformVersion]

    # appiumが実行しているシミュレータのUDIDを取得
    stdout, _stderr, _status = Open3.capture3('xcrun simctl list --json devices')
    json = JSON.parse(stdout)
    booted_device = json.dig('devices', "iOS #{platform_version}").find do |device|
      device['state'] == 'Booted' && device['name'].match(device_name)
    end

    booted_device['udid']
  end

  def start(file_path)
    raise 'UDID is null' unless @udid
    raise 'Already started recording' if @pid

    # バックグラウンドで録画を開始
    @pid = spawn("xcrun simctl io #{@udid} recordVideo #{file_path}", out: '/dev/null', err: '/dev/null')
    @file_path = file_path
    Process.detach(@pid)
  end

  def stop
    raise 'Any recording process started' unless @pid

    # 録画終了
    killed_process_num = Process.kill('SIGINT', @pid)
    raise "Kill pid: #{@pid} did not end correctly" unless killed_process_num.positive?

    # たまに終了に時間がかかる場合があるので待つ。既に終了している場合はエラーになるのでrescueで無視する
    begin
      Process.waitpid(@pid)
    rescue Errno::ECHILD
    end

    @pid = nil
  end

  def remove_video
    raise 'file_path is null' unless @file_path

    File.delete(@file_path)
    @file_path = nil
  end
end

RecorderクラスはrecordVideoを扱いやすくするためのクラスです。start()とstop()で録画をコントロールするのと、Appiumのdriverから録画するシミュレータを特定しています。

シミュレータの特定はAppiumのcapabilityとインストール済みのシミュレータ情報を表示するxcrun simctl listを組み合わせて実現しています。

まず、driverのcapabilityからdeviceName(端末名)とplatformVersion(iOSのバージョン)を取り出します。
xcrun simctl listの実行結果にはシミュレータのUDIDが含まれているので、先程のdeviceNameとplatformVersionと照合することによりUDIDを得ることができます。

次はRecorderクラスを実際に使用するコードです。RSpecを使う場合には共通処理をヘルパーとしてテスト本体とは分離することが多いと思いますので、そこに組み込みます。

spec_helper.rb

capability = {
  caps: {
    # iPhone 5s, iOS 11.1のシミュレータの場合は以下を設定
    platformVersion: '11.1',
    deviceName:      'iPhone 5s',

    # その他の設定は環境に応じて設定してください
    app:             APP_PATH,
    platformName:    'iOS',
    automationName:  'XCUITest',
  },
  appium_lib: {
    wait: 60
  }
}

RSpec.configure do |config|
  config.before(:each) do |example|
    @driver = Appium::Driver.new(capability)
    @driver.start_driver

    @recorder = Recorder.new(@driver)
    @recorder.start("#{ROOT}/screenshots/#{example.description}.mov")
  end

  config.after(:each) do |example|
    @recorder.stop

    # テストが通った場合は録画を消す(=失敗したものは残る)
    @recorder.remove_video unless example.exception

    @driver.driver_quit
  end
end

RSpecはRSpec.configureでbefore(:each)とafter(:each)に処理を仕込むことが可能なのでこれを利用しています。
before(:each)で録画を開始し、after(:each)で録画を終了することでテストケース毎に録画を分割しています。
さらにテストケースが無事に成功した場合には動画を削除することで、失敗したテストケースの動画だけがscreenshot/のディレクトリに最終的に残ります。

必要な処理はこれだけで、実際のテストケースを書くファイルには対応は不要です。

デモ

録画した動画のデモです。実際のUIテストはこの前後に別のテストケースが実行されており、失敗したこのテストケースの部分だけ動画が保存されます。

f:id:swet-blog:20171205133146g:plain:w300
録画したUIテストの様子

ちなみにこちらのデモはiPhone 5sのシミュレータで録画したものであり、25秒の動画で4.5MBのサイズでした。

まとめ

今回はiOS Test Night #6で発表したLTの補足という形でrecordVideoの紹介と、UIテストの様子を録画するTipsを紹介させて頂きました。

今回はAppium + RSpecでのUIテストと組み合わせてみましたが、recordVideo自体はRubyやAppiumに依存するツールではないため他にも活用方法が考えられると思います。
ぜひ色々試してみてください。

機械学習を使ってUI自動テストをサイト間で再利用する

SWETの薦田(@toshiya-komoda)です。 今回は第3回目の記事で言及させていただいた機械学習とUIテストに関して実験的に進めている技術開発について紹介させていただこうと思います。 この記事で紹介している内容の実装はGitHubにアップロードしていますので、もし興味がある方はこちらも覗いてみていただければと思います。

こちらはTensorFlow Advent Calender 2017第7日目の記事にもなっています。機械学習の実装の中でKerasを用いてます。

とりあえずデモ

最初に以下のデモ動画をご覧いただきたいです。会員登録フォームに対する自動テストのデモです。各入力欄に適切な情報を入力しつつ、パスワード欄にだけ'weak'という不正なパスワード文字列を入力して、バリデーションで弾かれることを確認するテストです。デモでは入力欄に値を埋める部分を、Chrome Extensionを用いて自動化しています。動画は2倍速にしています。

youtu.be

一見何の変哲もないテストの動画です。しかし、このデモの中では「ページ中の入力欄の意味を機械学習を用いて判別し、適切な値を入力する」ことを行っています。言い換えると、各サイト固有のDOM Elementを扱うロジックが存在せず、入力欄へ正しくデータを埋めるロジックがサイトに依存せず流用可能となっています。

以下、この技術の詳細についてUIテスト開発の課題のおさらいも含めて、順に説明させていただきます。

UIテストにおけるDOM操作とその課題

まずは、そもそものUIテスト開発における課題のおさらいです。

一般的なUIテストケースのテスト実装は、

  • あらかじめテスト対象サイトのHTMLにテストで使うためのIDを各UI Elementに埋め込んでおく
  • テストコード側で埋め込んだIDを用いて、テストロジックを記述する

というフローになります。この結果として、

  • テスト実装者がテスト対象ページのDOMの詳細を理解しなければならない
  • テスト対象ページのDOMの変更で容易にテストが壊れる
  • テストロジックがテスト対象ページのDOMに密結合するため、テストコードの再利用性が乏しい

などの問題が発生します。一般的なウェブアプリケーションにとって、UIの変更頻度が比較的高いことを考えるとこれらのUIテストコードの特性が、テストの開発コスト・運用コストを増大させてしまうことは想像できるかと思います。

UIテストコードとテスト対象のDOMを疎結合にする

では、どういったことができると嬉しいのかについて簡単な例で考えてみます。今、サイトAの会員登録ページと、サイトBの会員登録ページがあるとします。2つのサイトで同じテストケース「会員登録ページのフォームを埋めて、次の画面に進む」を自動化したいとします。サイトA、サイトBでもちろんフロントエンドのコードは異なるため、現状ではそれぞれのページにカスタマイズされたUIテストコードを個別に書くしかありません。

f:id:swet-blog:20171205165042p:plain

ここで、もし個々のサイトをUIテスト実装で必要な程度にうまく抽象化してくれるレイヤが存在したとしたらどうでしょう?

この状況を表現したのが以下の図になります。

f:id:swet-blog:20171205165100p:plain

同じテストケースを自動化する場合のみとはいえ、サイトA・サイトBに対して、抽象レイヤを挟むことでテストコードを再利用できます。また、この抽象レイヤがUIの細かい詳細を吸収してくれるのであれば、細かいUIの変更によってテストが壊れる問題も緩和できると期待できます。

ここで、具体的にどのような操作をどの程度の粒度で抽象化すればよいか、という問題はテストケースに依存します。このため、どのように抽象化を行うかを一般的に議論することは難しい問題といえますが、冒頭のデモの例では「該当ページ中の入力欄が入力を求めている値の意味」を抽象化した例といえます。

もちろん、このようなアプローチではサイト固有の仕様をテストすることは難しく、UI自動テストの全てのニーズを捉えることは難しいでしょう。しかし、限定的なテストケースに限られるとしても、UIテストの導入・メンテナンスコストを大きく削減できることからこのようなアプローチが有効であるケースはあると考えています。

機械学習を用いてDOM Elementの意味を推定する

それでは、UIテスト設計における抽象化の問題をどのように機械学習の問題に置き換えたのかについて説明していきます。 冒頭のデモは以下の論文で述べられている手法をベースにしつつ、細かいところを自分たちで試行錯誤しつつ作成したデモです。

J. W. Lin, F. Wang and P. Chu, "Using Semantic Similarity in Crawling-Based Web Application Testing," 2017 IEEE International Conference on Software Testing, Verification and Validation (ICST), Tokyo, 2017, pp. 138-148.
doi: 10.1109/ICST.2017.20

URL: http://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=7927970&isnumber=7927908

この論文の内容をかいつまんで述べると、

  • DOM Elementを構成するHTMLの文字列から当該Elementの意味あるいは役割を高い精度で推定できる
    • メールアドレスの入力欄、会員規約の同意チェックなどの意味や役割
  • 機械学習でいうところの多値分類問題として問題を定式化し、解くことで実現される
  • 具体的には、自然言語に対して開発されてきた機械学習の手法を、よしなにHTMLに対して適用してやるとうまくいく

というものです。HTMLの文字列を見てその意味やページ上での役割を推定するということは、まさに前項で説明したDOMの抽象化レイヤの役割そのものです。

DOM Element分類問題の定式化

さて、前述の先行研究をベースにしつつ、DOM Elementの分類問題を以下のように定式化しました。

登場人物は以下のようになります。

登場人物 意味
ウェブページ 同じ機能を提供するテストページの集合。 複数のサイトにまたがって存在している。
ElementのHTML テストケース内で操作対象となるDOM ElementのHTML文字列。分類問題の入力値となる。
トピックラベル テスト対象としてElementの意味を表すラベル。分類問題の出力値となる。
トピックラベル集合 ありうるトピックラベル全体の有限集合。 抽象化したいテストケースごとに定義されます。

ここでDOM Element HTMLを入力として、トピックラベルを出力とする多値分類器が作れれば良いことになります。 定式化の具体例を用いた説明を記事の最後に補足として付記していますので、詳細に興味のある方はそちらも御覧ください。

教師データの半自動的な収集

さて、前節で教師ありの分類問題としての定式化ができました。次は実際に意味のある教師データを集めることが必要です。要するに様々なウェブサイトから、テストで操作したいDOM Elementに対応するHTML文字列を抽出して、これらに適切なトピックラベルをつけていくという作業を行う必要があります。

この問題を解決するために、以下のようなアプローチを取りました。

  • 最初に小さなサンプル数で良いので手動で教師データを作る。
  • この教師データを用いてトピック分類器をつくる。
    • この時点では、推定精度は高くない。
  • このトピック分類器を用いて、半自動的に教師データを収集するスクリプトを作る。
  • 半自動的に作られた教師データの正誤判定のみ人間が行う。

このような収集の半自動化を行うことで、素直に手動で教師データを作成する場合に比べて約10倍の速度で学習データを集められるようになりました。

判定精度の評価実験

最後にトピックラベルの推定精度を評価してみます。冒頭のデモで用いている「会員登録ページにおいて入力フォームに値を入力する」というユースケースに絞って実験を行っています。

教師データ

コーパスとなるウェブサイトは、Google検索に「会員登録」と入力して出てきた200サイト分の会員登録ページを用いています。会員情報を実際に入力するページへの導線はサイトごとに異なるので、ここは手動で入力ページまでいってデータを集めました。

以降の評価では学習データの中で、同一のトピックラベルを持つDOM Elementが10個以上存在するもののみを用いて評価を行っています。同じトピックラベルを持つデータが少なすぎるDOM Elementに対して、意味のある学習を行うことはそもそも難しいからです。合計で2610個のDOM Elementを用いて学習を行い、652個のDOM Elementを用いてモデルのバリデーションを行っています。

用いた教師データはトレーニングスクリプトと合わせて、冒頭のGitHub上で公開しています。 なお、冒頭のデモ中ではテスト対象ページの教師データを取り除いた上で学習させたモデルを用いています。

分類器

解くべき問題は自然言語処理を用いた多値分類問題です。比較的扱いが簡単だった以下の2つの分類器を用いました。

LSIを用いた次元圧縮 + ロジスティック回帰(以下、LSI)

  • ライブラリにgensimsklearnを用いて実装しました。
  • 圧縮後の次元数は500です。

単純なニューラルネットワーク(以下、NN)

  • ライブラリにkerasを用いて実装しました。
  • 全結合層を1つ重ねた場合(NN1)と2つ重ねた場合(NN2)で評価しています。
  • 学習時のエポック数はNN1、NN2ともに決め打ちで800epochs分学習させています。
    • 目分量ですが800epochs程度でだいたい学習が収束したように見えていました

今回用いたニューラルネットワークはもっともシンプルといって良い単純なネットワークかと思いますが、kerasのmodel.summary()の出力を記載しておきます。

全結合1層 (NN1)
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_1 (Dense)              (None, 18)                54882     
=================================================================
Total params: 54,882
Trainable params: 54,882
Non-trainable params: 0

全結合2層 (NN2)
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense_1 (Dense)              (None, 100)               304900    
_________________________________________________________________
dense_2 (Dense)              (None, 18)                1818      
=================================================================
Total params: 306,718
Trainable params: 306,718
Non-trainable params: 0

推定精度

結果は以下のようになりました。 テストセットをランダムに5回ずつ変えて平均を計測しています。

分類器 Accuracy
LSI 90.8 %
NN1 92.5 %
NN2 92.7 %

今、トピックラベルの取りうる値は18通りあり、ランダムに予測した場合の正答率は5.6%程度となることを考えると悪くない精度が出ていると考えられます。

TensorBoardで表示したニューラルネットワークの学習曲線は以下のようになっていました。

NN1(全結合1層)の学習過程(TensorBoard)

f:id:swet-blog:20171206221559p:plain

NN2(全結合2層)の学習過程(TensorBoard)

f:id:swet-blog:20171206220823p:plain

全結合2層の学習では過学習が起きてしまっているように見えるなど、ニューラルネットワークに関してはもう少しチューニングの余地がありそうですが今回は深追いしていません。

最も精度の良かった全結合2層の場合に、具体的にどのような場合に推定が失敗しているかを見てみましょう。

失敗例: input tagのHTML文字列だけでは情報が不足しているケース

DOM HTML <input id="kiad2" name="kiad2" type="text" maxlength="80" value="">
単語列 ['kiad', '2', 'text', 'kiad', '2']
推定値 userName
正解 address

このケースではサイトの変数名の付け方に正解を予測するための情報がほとんどなく、人間が見たとしてもこのHTMLが住所の入力欄であることを判定するのは難しいといえます。例えば、入力欄のHTMLだけでなく関連するDOM要素(labelなど)を入力として与えるなど入力の定式化自体を考え直さないといけないと考えられます。

失敗例: 分類器の精度が向上すれば良くなるかもしれないケース

DOM HTML <input type="text" name="d" id="d" placeholder="オナマエ" class="sizeL" style="transition: outline 0.55s linear; outline: none; outline-offset: -2px;">
単語列 ['d', 'text', 'd', 'オナマエ']
推定値 mailSubscriptionCheck
正解 userName

今回は教師データ中に含まれる単語のみから辞書を作成し、単語列をベクトルに変換しています。このため、辞書内の単語数は3000強しかありません。従って、このケースで現れている「オナマエ」という日本語の文字列が「名前」と同じ意味であることを判定することができなかったと考えられます。例えばWikipediaなど一般的なコーパスから作成した単語の分散表現を組み合わせることで精度を改善できる可能性があると考えています。

その他、予測すべきトピックラベルが教師データ中にあまり存在せず、教師データが足りていないように見えるケースもありました。この点については、教師データをさらに増やすことで精度を向上させられる可能性があると考えています。

課題・将来展望

今回のデモおよび推定精度の評価では

  • 会員登録ページの入力フォームを埋めるというテストケース。
  • HTMLをレイアウト情報として利用するウェブページがテスト対象。

という2つの条件を仮定しています。一方で実用的にはより広い範囲のユースケースでの適用可能性を検討する必要があります。

まず、より複雑なテストケースに拡張できるかどうかを検討する必要があります。例えば、

  • 複数のページにまたがるテストケースを扱う
  • シングルページアプリケーションのようにより複雑な構造を持つページを扱う
  • テストの成功・失敗を判定するロジックを学習させる

といった課題が挙げられます。

また、この手法自体の適用範囲はHTMLに限定されるものではありません。このため、HTMLとは異なるフォーマット(XMLなど)を用いてレイアウト情報を管理しているモバイル・アプリケーションの世界への適用も試してみたい課題となります。

まとめ

今回は、機械学習を用いてUIテストコードをサイトをまたいで再利用する技術について紹介させていただきました。課題欄でも述べた通り、まだまだ検討すべき課題が多くある状態です。SWETではこうした技術的チャレンジを楽しめる方を随時募集しています。

参考文献

補足: 分類問題の具体例による説明

仮想的で簡単なフォームの例で考えると、以下のようになります。

ウェブページ(コーパス)

# サイトA
<form>
  メールアドレス: <br>
  <input type="email" name="user_email" placeholder="please input your email"> <br>
  パスワード: <br>
  <input type="password" name="user_pass" placeholder="please input your password"><br>
  <input type="submit" id="confirm_button" value="確認画面へ">
</form>
# サイトB
<form>
  メールアドレス: <br>
  <input type="email" name="email_address"><br>
  パスワード: <br>
  <input type="password" name="password"><br>
  パスワード(確認用): <br>
  <input type="password" name="password_confirmation"><br>
  規約に同意する <br>
  <input type="radio" name="service_term_confirmation"><br>
  <input type="submit" value="登録">
</form>

トピックラベル集合。このリストは抽象化したいテスト対象ページに合わせて、手動で作成する必要があります。

"email_input"
"password_input"
"password_confirm_input"
"service_term_input"
"form_submit_button"

ElementのHTML。

# サイトA
1. <input type="email" name="user_email" placeholder="please input your email">
2. <input type="password" name="user_pass" placeholder="please input your password">
3. <input type="submit" id="confirm_button" value="確認画面へ">
# サイトB
4. <input type="email" name="email_address">
5. <input type="password" name="password">
6. <input type="password" name="password_confirmation">
7. <input type="radio" name="service_term_confirmation">
8. <input type="submit" value="登録">

トピックラベルの列。この値がいわゆる正解データ、教師データと呼ばれるものになります。各値はトピックラベル集合の要素のどれか1つが入ります。また、番号が対応するElementのHTMLにひも付きます。

# サイトA
1. "email"
2. "password"
3. "form_submit"
# サイトB
4. "email"
5. "password"
6. "password_confirmation"
7. "service_term"
8. "form_submit"

HTML文字列の単語列への分解

さらにElementのHTMLは、HTMLパーサを用いて意味のある単語のみを抽出した単語列として扱います。この後、日本語が含まれている場合は形態素解析も行います。先の例のElementのHTMLの列は以下のように変換されます。

ElementのHTMLの単語列

1. ["email", "user", "email", "please", "input", "your", "email"]
2. ["password", "user", "pass", "please", "input", "your", "password"]
3. ["submit", "confirm", "button" "確認", "画面", "へ"]
4. ["email", "email", "address"]
5. ["password", "password"]
6. ["password", "password", "confirmation"]
7. ["radio", "service", "term", "confirmation"]
8. ["submit", "登録"]

この形までくれば、あとは完全に自然言語処理の世界の問題として扱うことができます。

ラベル付けについての補足

この手法ではラベル付けに任意性が存在します。ユースケースごとにモデルを学習することを想定しているので当然といえば当然です。これは最終的にどこまでテストをしたいのかに依存して変わるものです。

今回は会員登録フォームの入力欄について、

  1. 純粋に入力される値の意味だけで分類した場合 (粗い分類)
  2. 意味に加えて入力フォーマットの情報も含めて分類した場合 (細かい分類)

の2種類のラベル付けに対して評価を行っています。これは、例えば以下のように3つの欄に分かれた電話番号の入力欄が与えられたときに

1. <input type="text" id="tel1" name="tel1" value="" autocomplete="off"> # xxx-yyyy-zzzz形式のxxx
2. <input type="text" id="tel2" name="tel2" value="" autocomplete="off"> # xxx-yyyy-zzzz形式のyyyy
3. <input type="text" id="tel3" name="tel3" value="" autocomplete="off"> # xxx-yyyy-zzzz形式のzzzz

対応するトピックラベルを

1. 'telephone_number'
2. 'telephone_number'
3. 'telephone_number'

とするか、

1. 'telephone_number_first'
2. 'telephone_number_middle'
3. 'telephone_number_last'

とするかでトピックラベルの付け方を変えるということです。後者のラベル付けの方は分類がより詳細なため、分類問題としては難しくなります。

前者のような粗いラベル付けでも、テスト実装時の工夫でテストを実装することは可能です。例えば、'telephone_number'と推定されたElementの数を数えて値を分割して入力するなどが考えられます。

本文中の評価では、冒頭のデモを作成するのに粗いラベル付けで十分だったため粗い方のラベル付けを用いた場合の精度を評価として示しています。 参考までですが、この節で説明した後者の細かいラベル付の場合の推定精度をいかに示します。

分類器 Accuracy (テストセットをランダムに10回変えて平均を計測)
LSI 78.2 %
NN 82.7 %

精度は前者の粗いラベル付けの場合と比べて10%程度低くなり、今回用いたものよりもさらに洗練された分類器が必要になる事がわかっています。

Bluepillを導入してiOSのUIテスト実行を並列化する

はじめまして、SWETグループの細沼(@tobi462)です。

9月から10月にかけて iOSDC 2017 や、それに関連した勉強会(リジェクトコン)などが開催され、iOS開発者にとってはホットな時期だったかと思います。私自身もiOSDC 2017ではライトニングトーク、俺コン Vol.1 / Day. 2 で15分発表をさせていただき、発表者・参加者の両面からこれらのイベントを楽しめたと思っています。

さて今回は、iOSDCでのLT発表の中で触れさせていただいたBluepillについて、実際の使い方などを掘り下げて紹介したいと思います。

サンプルコード

実際に動作するサンプルコードも用意しておきましたので、必要に応じてご利用ください。
https://github.com/YusukeHosonuma/iOS-Bluepill-Sample

Homebrewがインストールされていれば、READMEに書かれた手順に従って、実行するところまで行えるかと思います。

なおBluepillの動作確認サンプルであるため、アプリの内容に特に意味はありません。

Bluepillとは

Bluepill は、iOSアプリのUIテスト実行について、複数シミュレータを同時起動して並列で実行するツールです。

f:id:swet-blog:20171113143332p:plain

並列でテストが実行されるため、UIテストの実行時間を短縮できるというメリットがあります。また、テストは自動的に分割(グルーピング)されるため、開発者が自分でテストの分割を意識する必要がないのも便利な点といえます。

LinkedInが開発したツールで、GitHubリポジトリのREADMEによると巨大なテストスイートを妥当な実行時間で終わらせるために開発した、と書かれています。

LinkedIn created Bluepill to run its large iOS test suite in a reasonable amount of time.

また、以下のツールにインスパイアされて開発したとも書かれています。

  • parallel iOS test(pxctest)
    • 複数のOSバージョン・端末でテストを並列実行するツールで、端末カバレッジの網羅を目的としています。
  • Facebook製ツール

本記事では触れませんが、興味のある方は上記のツールも調べてみても、面白いかもしれません。

動作確認環境

以下の環境で確認を行っています。

  • Xcode 9.0(※)
  • macOS Sierra (10.12.6)
  • Fastlane 2.62.1
  • bluepill 2.0.2

ちなみにXcode 9.0にて公式で複数シミュレータが起動できるようになりましたが、それ以前のXcode 8.xの時点からBluepillは複数シミュレータ起動を実現していました。そのあたりからLinkedInのUIテストの実行時間短縮に対する本気度が伺えます。

なおREADMEにも書かれていますが、Xcode 8.xで利用したい場合は、最新バージョン(2017/11/13時点)の 2.0.2 ではなく、1.1.2 を利用する必要があるので注意しましょう。

以降、アプリやスキームの名称は xxx などに置き換えて記載していきますので、必要に応じて自身の環境に読み替えてください。

※記事執筆時点の最新バージョンは9.1ですが、Bluepillは対応中だったので9.0を利用して確認を行っています

インストール

GitHubリポジトリからダウンロード

GitHubリポジトリのリリースページでバイナリが公開されているので、そこからダウンロードしてPATHの通った適当なディレクトリ(/usr/local/bin/ など)に配置します。

解凍すると bluepillbp という2つのバイナリが格納されていますが、両方とも必須なのでご留意ください。

$ bluepill --version
Bluepill v2.0.2

Homebrewでのインストール

READMEにも書かれていますが、Homebrewでもインストールが可能なので、こちらを利用するとより手軽にインストールできます。

$ brew install bluepill
$ bluepill --version
Bluepill

ただし、(2017/11/13時点では)上記のとおりバージョン番号の出力がされないという事象があります。これはIssueにも報告が上がっていますが、しばらく動きがないようなので、気になる方はGitHubのリリースページからダウンロードした方が確実かもしれません。

設定から実行まで

以下の2段階の手順で実行します。

  1. テスト用のビルド(test-for-building)
  2. 実行(bluepill)

テスト用のビルド(test-for-building)

まずは xcodebuild コマンドの build-for-testing を利用して、テスト実行用のアプリをビルドします。

$ xcodebuild build-for-testing \
    -scheme xxxUITests \
    -destination 'platform=iOS Simulator,name=iPhone 6,OS=latest' \
    -derivedDataPath ./build
...

** TEST BUILD SUCCEEDED **

build-for-testing は、Xcode 8から導入された機能で、ビルドとテスト実行を分離できる仕組みです。通常は build-for-testing でテスト用にビルドしたアプリを test-without-building を利用してテスト実行を行います。

実行(Bluepill)

bluepill コマンドを使って実行します。

READMEには多くのオプションが記載 されていますが、必須なのは以下の2つのみです。

  • --xctestrun-path
    • .xctestrun ファイルへのパスを指定
  • --output-dir
    • 実行結果の出力先パスを指定
$ bluepill \
    --xctestrun-path \
    build/Build/Products/xxxUITests_iphonesimulator11.0-x86_64.xctestrun \
    --output-dir \
    bluepill_output

上記コマンドを実行すると、以下のようなログが出力され、デフォルト値である 並列数=4 でUIテスト実行が行われます。

Bluepill runtime version and compile time version are matched: 9.0 (9A235)
{36562} 20171029.150441 [  INFO  ] Using xctestrun configuration
{36562} 20171029.150441 [  INFO  ] This is Bluepill
{36562} 20171029.150441 [  INFO  ] Running with 4 simulators.
{36562} 20171029.150441 [  INFO  ] Packed tests into 5 bundles
{36562} 20171029.150441 [  INFO  ] Started Simulator 1 (PID 36582).
{36582} 20171029.150441 [  INFO  ] (BP-1) Running Tests. Attempt Number 1.
{36582} 20171029.150441 [  INFO  ] (BP-1) [Attempt 1] Create Simulator
{36562} 20171029.150442 [  INFO  ] 1 Simulator still running. [1]
{36562} 20171029.150442 [  INFO  ] Using 357 of 1064 processes.
{36562} 20171029.150442 [  INFO  ] Started Simulator 2 (PID 36585).
{36582} 20171029.150442 [  INFO  ] (BP-1) Booting a simulator without launching Simulator app
{36585} 20171029.150442 [  INFO  ] (BP-2) Running Tests. Attempt Number 1.
...
{36562} 20171029.150813 [  INFO  ] PID 37244 exited 0.
{36562} 20171029.150814 [  INFO  ] All simulators have finished.

またコマンドラインでオプションを指定する他にも、JSONで設定ファイルを用意する方法もあります。なお、JSONで指定するキー名では-oなどの省略系は指定できませんので注意しましょう。

{
  "xctestrun-path": "build/Build/Products/xxxUITests_iphonesimulator11.0-x86_64.xctestrun",
  "output-dir": "bluepill_output"
}

--config オプションで作成したJSONファイルを指定して実行できます。

$ bluepill --config config.json

出力結果

先ほど出力先として指定した bluepill_output フォルダの中身を見ると以下のようになっています。

bluepill_output
├── 1
│   ├── 1-xxx-results.txt
│   ├── 1-xxx-timings.json
│   ├── 1-simulator.log
│   ├── TEST-xxx-results.xml
│   └── xxx-stats.txt
├── 2
│   ├── 1-xxx-results.txt
│   ├── 1-xxx-timings.json
│   ├── 1-simulator.log
│   ├── TEST-xxx-results.xml
│   └── xxx-stats.txt
├── 3
...
└── TEST-FinalReport.xml

TEST-FinalReport.xml がJUnit形式の結果XMLファイルとなっています。Jenkinsのプラグインなど、JUnit形式をサポートしているツールで読み込ませ、見やすい形で表示することも可能です。

1から始まる数字のフォルダには、Bluepillにより自動的に分割された、各グループでのテスト結果やログなどが格納されています。あまり確認するケースは多くないかもしれませんが、覚えておくと何かあった時に調査がしやすいでしょう。

どれだけ実行時間を短縮できるか?

冒頭のスライドからの抜粋になりますが、UIテストケース数が30あるiOSアプリに対して実行したところ、それぞれの実行時間は以下のようになりました(10回施行した平均値です)

実行環境:Mac Pro (Late 2013) 3.5 GHz 6コア / 16GB

方法 実行にかかった時間 通常のテスト実行に対する比率(%)
通常 14:53 100%
Bluepill(並列数3) 9:37 65%
Bluepill(並列数4) 8:27 57%

通常のテスト実行に比べて半分以下とまではいきませんが、かなり実行時間を短縮できることが分かるかと思います。

.xctestrunの中身

ところで build-for-testing により生成され、Bluepillの実行時に指定していた .xctestrun とは何者なのでしょうか?

ファイルの実体は plist 形式になっており、XML形式で出力されています。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>xxxx</key>
  <dict>
    <key>UITargetAppPath</key>
    <string>__TESTROOT__/Debug-iphonesimulator/xxx.app</string>
    <key>TestHostPath</key>
    <string>__TESTROOT__/Debug-iphonesimulator/xxxUITests-Runner.app</string>
    <key>TestBundlePath</key>
    <string>__TESTHOST__/PlugIns/xxxUITests.xctest</string>

上記は重要と思われる部分のみを抜粋したものですが、箇条書きに直すと以下のようになります。

  • UITargetAppPath
    • __TESTROOT__/Debug-iphonesimulator/xxx.app
  • TestHostPath
    • __TESTROOT__/Debug-iphonesimulator/xxxUITests-Runner.app
  • TestBundlePath
    • __TESTHOST__/PlugIns/xxxUITests.xctest

上から順に、それぞれ「テストターゲット」「テストランナー(ホストApp)」「テストバンドル」のパスとなっています。つまり .xctestrun には、テスト実行に必要な各ファイルへのパスが記録されているということになります。

この手のツールは実際に試してみると、思わぬところでエラーが発生して利用する気を失ってしまうということがありがちですが、こういった部分を理解しておくとそうした状況で早く解決できるかもしれません。

実行オプションについて

前述したとおり、Bluepillには 多数の実行オプション が用意されています。

ここでは比較的、重要と思われるオプションを紹介します。

必須オプション

  • xctestrun-path
    • .xctestrunのパス
  • output-dir
    • ログの出力先

任意オプション

  • config
    • JSONで記述された設定ファイルへのパス
  • device
    • 実行するデバイス(デフォルト:iPhone 6
  • headless
    • ヘッドレスモードを有効にするか(デフォルト:off
    • true(※)を指定することで、シミュレータが表示されないので省メモリに繋がります
    • CI環境などでは、他Jobとの競合を避ける意味でも有効にしておくと良いでしょう
  • num-sims
    • シミュレータの並列実行数(デフォルト:4
    • Mac Pro(Late 2013)やMacBook Pro(Mid 2015)で試した限りでは、デフォルト値の 4 が安定して良い結果が得られました
    • 必要であれば、マシンスペックに応じて変更すると良いでしょう
  • screenshots-directory
    • 失敗したテストのスクリーンショットを出力するディレクトリ(デフォルト:指定なし)
    • 原因の調査がしやすくなるので、指定しておいたほうが良いでしょう
    • 相対パスで指定する場合は ../ を使用すると正しく解釈されないので注意が必要です

※README上では、デフォルト値が off と記載されているオプションがありますが true/false で指定します。

Fastlaneへの組み込み

ここまで見てきたように、Bluepillはかなり手軽に使い始めることができます。一歩進んで、Fastlaneから利用できるようにすると、さらに便利になります。

Fastlaneについての説明やインストール方法についてはここでは触れませんので、必要に応じて 公式ドキュメント などをご参照ください。

テスト用アプリのビルド(build_for_testing)

scan アクションを利用し、build_for_testingtrue を指定することで行います。

desc 'テスト実行用にビルド'
lane :build_for_testing do
  scan(
    build_for_testing: true,
    scheme: 'xxxUITests',
    destination: 'platform=iOS Simulator,name=iPhone 6,OS=latest',
    derived_data_path: './build'      
  )
end

Bluepillを用いたテスト実行(test_with_bluepill)

sh アクションを利用して、shellコマンドを前述のコマンドをそのまま実行します。

desc 'Bluepill を用いたUIテスト実行'
lane :test_with_bluepill do
  sh('bluepill -c ../config.json')
end

設定ファイルの内容は基本的に同じなのですが、パスの頭に ../ を追加している点に注意しましょう。

{
  "xctestrun-path": "../build/Build/Products/xxxUITests_iphonesimulator11.0-x86_64.xctestrun",
  "output-dir": "../bluepill_output",
  "headless": true,
  "screenshots-directory": "bluepill_screenshots"
}

これはFastlaneの実行が fastlane ディレクトリで行われるため、1つ上のディレクトリを参照しなければならないためです。ただし、前述したように screenshots-directory オプションだけは ../ を使用すると正しく解釈されないためそのままにしています。

Fastlaneプラグイン

今回は、JSONの設定ファイルを用いるアプローチを取ったのでshell実行で済ませてしまいましたが、GitHub上にいくつかBluepill用のFastlaneプラグインも公開されています。

もし要件に合うものがあれば、そちらを利用するのも手かと思います。

ただし、READMEに書かれた全てのオプションがサポートされているプラグインは無さそうでした。プラグインを利用するときには、そうした制限があることを理解した上で利用しましょう(個人的には設定ファイルに集約したほうが、メンテナンス性を上げられるかと思います)

レーンの実行

それでは用意した2つのレーンを利用して、ビルド・テストを実行してみましょう。

$ fastlane build_for_testing
...
[xx:xx:xx]: fastlane.tools finished successfully 🎉

$ fastlane test_with_bluepill
...
[xx:xx:xx]: fastlane.tools finished successfully 🎉

両方のコマンドで、Fastlaneから successfully のメッセージが出力されれば成功です。

Jenkins上で実行する

ここまで整備できていれば、Jenkins上で実行することも簡単です。

ここでは実行とテスト結果の集計方法のジョブ設定だけ紹介します(Jenkinsの基本的な使い方や設定については触れませんので、必要に応じて書籍やWeb記事をご参照ください)

ビルド・テスト実行

ビルド > ビルド手順の追加から「シェルの実行」を追加し、fastlaneコマンドを利用して先ほど用意したlaneを呼び出すようにします。

fastlane build_for_testing
fastlane test_with_bluepill

f:id:swet-blog:20171101134312p:plain

テスト結果の集計

ビルド後の処理 > ビルド後の処理の追加から「成果物を保存」と「JUnitテスト結果の集計」を追加し、bluepill_output/*.xml といったように指定します。

f:id:swet-blog:20171101134353p:plain

テスト結果の確認

正常にビルドが成功すると、ビルドの結果画面などに「最新のテスト結果」がリンク表示され、その遷移先から詳細なテスト結果を確認することが出来ます。

f:id:swet-blog:20171101134405p:plain

それぞれのテストにかかった時間も確認できるので、設定しておくと便利かと思います。

まとめ

今回は「Bluepill」についての紹介と、実際に利用するにあたってのインストール手順からFastlaneへの組み込み、Jenkinsへの設定まで紹介しました。

この手のツールは実際に利用し始めるまでに予期せぬトラブルに遭遇して、面倒になってしまって結局使わないというケースもあると思い、分かりやすい記事を心がけてみたのですがいかがだったでしょうか。

また、次回のブログ記事もお楽しみください。

UIテストの最前線: SeleniumConf Berlin 2017参加レポート

SWETグループの薦田です(@toshiya_komoda)。10月9、10日にドイツ・ベルリンで開催されたSelenium Conferenceに参加してきました。私もLightning Talksで、機械学習とUIテストに関する発表をさせていただきました(スライド)。

こちらの内容については、別の記事で書かせていただくこととし、この記事ではカンファレンスで聴講し、特に気になったトークについてレポートさせていただこうと思います。

ウェブ・アプリケーションにおけるUIテストの最前線の状況が少しでも伝われば幸いです。

f:id:swet-blog:20171018220430j:plain

Seleniumについておさらい

Selenium WebDriverは、ウェブ・ブラウザをプログラムから操作するためのソフトウェアツールです。OSSとして開発されており、主な用途としてウェブ・アプリケーションのテスト自動化に用いられています。現バージョンは3.6.0(2017年10月24日現在)、プロジェクト開始から10年以上の歴史を持ちます。

Selenium内部ではブラウザを操作するためにWebDriverと呼ばれるAPIが用いられています。主要なブラウザベンダが、自社ブラウザ向けのWebDriverの実装を提供しています(Official Selenium Blog: Selenium 3.0: Out Now)。 Seleniumはブラウザベンダが公式に認めているブラウザ・テストツールと言って言い過ぎではないでしょう。また、W3Cにて仕様標準化の作業が進められています(WebDriverのCR)。

Selenium Conference

Selenium Conferenceはプロジェクト公式のミートアップイベントで、プロジェクトのリーダーであるステアリング・コミッティも参加するイベントです。7年の歴史があり、2011年から2015年は年に1度、2016年からは年に2度ずつ開催されています。 前回のカンファレンスは、2017年4月にアメリカ オースティンで開催されており、SWETでも参加レポートを公開しています。 また、過去のトークがYouTube上で視聴できるようになっているので、興味のある方は眺めてみると雰囲気が伝わるかと思います。 今回のトークもすでにアップロードされています。

それでは、特に気になったトークを3つ紹介させていただきます。

Selenium State of the Union, To Infinity and Beyond Selenium

ステアリング・コミッティの一人であるSimon Stewart氏による基調講演です。 基調講演の中で、Seleniumが普及しプロダクトが成熟していく中で現在のSeleniumに求められているものは、 より分かりやすいドキュメンテーションと分かりやすいAPIだ、と語りました。 これらを実現するために、現役でテストを書いているエンジニアにこそ、OSSであるSeleniumにコントリビュートしてほしいとのことでした。

また気になる話題として、Firefoxのバージョンアップに伴い2017年8月にサポートが切れてしまった Selenium IDEの動向についても言及がありました。 現在、applitoolsの開発者が中心となって、Selenium IDEを公式に作り直しているとのことで期待して待っていて欲しいとのことでした(開発中のSelenium IDEのレポジトリ)。

Keep Your Test Lean

GithubのQAエンジニアによるUIテストにおけるベストプラクティスの話です。 UIテスト自動化が効果的に機能するための要件として、

  • Valuable: 重要で実行頻度の高いテストケースが自動化されている
  • Reliable: UIテストの実行結果を安定させるための仕組みが備わっている
  • Fast: テスト実行時間が短い時間に収まっている

という3点を挙げ、UIテストスイートがこれらを満たすために必要な戦略と工夫について説明されています。

UIテストをやみくもに作成してしまった結果、メンテナンスコストばかり大きくなりメリットが感じられなくなった、という話は日本でもよく聞く話です。 SWETでもこれまでにUIテストを運用していく中で、全く同じような運用上の課題を経験しています(資料)。

特にUIテストをテスト担当者に属人化させず、システム開発者から目に見えるものとするための工夫が興味深いものでした。具体的には、

  • UIテストはシステムの開発言語と同じ言語で記述する
  • テスト対象システムと同じコードベースで管理する
  • テストコードベースの運用ルールをテスト対象システムと同じにする
  • テスト対象UIパーツにカスタムデータ属性を埋め込んでおく

といった工夫が紹介されていました。最後のテスト対象のUIパーツにカスタムデータ属性を埋め込むとは、例えば以下のHTMLにおけるdata-qaがこれに相当します。 このタグがついたUIパーツは、テストで利用されているということが開発者にひと目で分かります。これにより、意図せずテストを壊す変更が入ってしまうことを防止できるというわけです。

# コードは動画中のスライドより引用
<l-popup as="popup" class="has-menu tall-option has-icon cap-option">
  <ul class="popup-menu narrow-menu" data-qa='add-candidate-menu'>
    <li class="option">
      <a on-click="addManually()">
        <view is="icon-edit"></view>
        Add manually
      </a>
    </li>

また、非同期処理が原因でテストが落ちた場合の再実行の仕組みを整える、並列化やヘッドレスブラウザを利用して高速化を行う、といったテスト実行環境の足回りの改善の重要性にも触れています。

UIテスト導入を検討している方に、ぜひ一度視聴してほしい内容でした。

Readable. Stable. Maintainable. E2E Testing @ Facebook

Facebookでテストフレームワーク・テストインフラを担当しているエンジニアの発表です。

Seleniumをそのまま用いたUIテストコードの可読性が低いことは、よく指摘されるSeleniumの問題点ですが、Facebookの中で開発しているテストフレームワークがこの問題をいかに解決しているかについて、具体例を用いて説明しています。

f:id:swet-blog:20171020201722p:plain (写真は動画中より抜粋)

例えば、上記のようなフィード欄のUIテストを考えてみましょう。以下の例では、 Seleniumをそのまま使って書かれたコードが、可読性の低いテストコードになる様子を分かりやすく説明しています。

# コードは動画中のスライドより引用

var feed
// feedがロードされるのが非同期なので、これに対応するためのwhileループ
while (feed == nil) {
  feed = try? driver.findElement(by: id("feed"))
}

// id を直接指定したWebElementの取得
let story = element.findElementBy(by: id("permalink"))[2]

// たまに一回目のクリックがうまくいかないのでretryしている
story.click()
story.click()

// 理由は分からないが、sleepを追加するとテストが動作するので入れられたsleep
sleep(5000)

// id を直接したUIパーツのセレクト
let postText = driver.findElement(by: id("post_text"))

コメントに記載している通り、UIテストにまつわるバッドノウハウがそこかしこに登場しており、本質的にテストしたい内容が、表現されている実装とは言えないものとなってしまっています。 FacebookにおけるUIテストフレームワークの開発の動機の一つは、この問題を解決し、可読性が高くメンテナンスが容易なUIテストコードを実現することだったそうです。

FacebookのUIテストフレームワークにおける問題解決のポイントは、テスト対象画面上のUIパーツの構造とそれに紐づくアクションをクラスとして定義する仕組みを提供していることです。

例えば、前述のフィードのUIは以下のようなクラスとして定義されると説明されています。

# コードは動画中のスライドより引用

// フィードに対するUI定義クラス。
// 子要素と、それに対するアクションを定義していく
class FeedStory: Component {
  let definition = 
           root(storyLocator, "story")
           .withDescendant(
             profilePhotoLocator,
             "profile photo"
           ) // 画像アイコン
           .withDescendant(
             permalinkLocator,
             "permalink"
           ) // その下にフィード詳細へのパーマリンク
           .withDescendant(
             contentLocator,
             "post content"
           ) // その下に投稿内容
           .withDescendant(
              StoryFooter
           ) // その下にフッタ
             // 下部で定義されたStoryFooterクラスを使いまわすことができる

  // UIパーツ内の要素を取得する関数
  getPostText() -> String {
    return getTextOf("post content")
  }

  // UIパーツに対するアクションを定義した関数。
  // 返り値はPageObject Patternの考え方に従い、UIオブジェクトが返される
  openProfile() -> UserProfile {
    return click("profile photo", andGoTo: UserProfile)
  }
  ...
}

// Like, Commentを含むフッターの定義。StoryFeedクラスと同様にUIを定義していく
class StoryFooter {
  // Like, Commentを含むFooterコンポーネントのUI定義クラス
}

このようなUIパーツのクラスを利用することで、 例えば「フィードを投稿して、この投稿したフィードの詳細ページを開き、これにコメントを付ける」といった複雑なシナリオテストも、以下のように簡略に書けるようになるとのことです。

waitFor(LoginScreen)
  .loginAs(user)
  .selectComposer()
  .open()
  .enterText("Test Post")
  .post()
  .selectStoryWithText("Test Post")
  .openPermalink()
  .postComment("Test Comment")

冒頭のバッドノウハウだらけのテストコードと比較して、テストコードの可読性が大きく向上していることが伺えます。

はじめにテスト対象のUIの仕様をクラスとして定義してしまうことで、テストロジックのメンテナンスコストを下げるというアイディアは、UIテストのデザインパターンとしてよく知られたPageObjectパターンの考え方をフレームワークとして一歩推し進めたかたちとなっており、興味深いアプローチだと感じました。

また、トークの中ではテストコードの可読性に関する話題だけでなく、One Worldと呼ばれるモバイルテスト基盤の話など、UIテストに関する多くの技術トピックに触れています。

UIテストの技術的な側面に興味のある方には是非視聴していただきたい内容でした。

終わりに

講演以外で印象に残っていることは、ZaleniumというOSSソフトウェアの開発者と直接会って話ができたことです。 Zaleniumは、Seleniumを用いた複数のUIテストを並列実行することができるテスト基盤を提供するソフトウェアです。 SWETでもZaleniumを利用しており、機能拡張のPRを送っていました。 普段は会えないOSS開発者と直接会って話ができることは海外カンファレンス参加の一つの魅力だと感じました。

Selenium Conferenceは年2回、来年はインドとアメリカでの開催です。 来年4月には第1回目のAppium Conferenceも開催されます。 DeNA Testing Blogでは今後もテスト自動化の最前線の動向について、継続して情報発信していく予定です。

参考資料