目次
はじめに
AWSを使ってサーバレスアプリケーションを開発したのですが、テストもモダンな感じでやりたいので、UIテストを自動化することにしました。
テストコードは、実装した経験があるという理由でJavaとSeleniumを使うことに。せっかくAWSを使っているので、Code兄弟(CodeCommit、CodeBuild、CodePipeline)を使って自動化してみることにしました。SeleniumもCode兄弟もそれぞれ使用経験があるので、組み合わせて使うのは初めてだけどそこまで苦戦しないだろう!と思っていたら、思いのほか躓いたのでした…。
今回は、テスト設定を簡単に紹介しつつ、ぶち当たった課題&解決策をまとめたいと思います。
UIテスト自動化の構成
今回のテスト自動化部分の構成図を簡単にまとめると以下の図のようなイメージになります。
アプリケーションのフロントエンド部分はS3バケットで静的Webサイトホスティングされていて、そこで公開されているコンテンツに対してテストコードを実行します。
テストコードが実行される流れは、ざっくり以下のようなイメージです。
- ユーザがテストコードを実装し、CodeCommitのリポジトリにpush
- pushをトリガーとし、CodePipelineが実行される
- CodeCommitのリポジトリをclone
- CodeBuildでテストコードをコンパイルし、テストを実行
テストコードの実装
Seleniumといえば、これまではEclipseを使って結構なボリューム感のあるテストプロジェクトを実装していたのですが、今回はまずライトなものでとりあえず動くことを確認することから始めたい!と思い、AWS公式ドキュメントに記載されている手順を参考にMavenプロジェクトを作成することにしました。
Mavenプロジェクトの作成
まず、任意の作業ディレクトリで以下のコマンドを実行し、Mavenプロジェクトを作成します。Mavenは事前に要インストールです。私がインストールしたMavenはバージョン3.6.3です。
1 |
mvn archetype:generate -DgroupId=[任意のグループID] -DartifactId=[任意のアーティファクトID] -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false |
指定が必要な2つのパラメータは下記で、好きな名前を指定して問題ありません。
- 任意のグループID: パッケージ名。ドットつなぎで階層になる。(例:com.mycompany.app)
- 任意のアーティファクトID: プロジェクト名
buildspec.ymlの作成
ビルドの仕様を定義するbuildspec.ymlを作成します。要するにCodeBuildに実行してほしい処理をここで定義します。今回の場合だと、テストコードをコンパイルして、コンパイルしたテストコードを実行する、という処理です。
作成したMavenプロジェクトのルートディレクトリ直下に、以下の内容でbuildspec.ymlを作成します。
1 2 3 4 5 6 7 8 9 10 |
version: 0.2 phases: install: runtime-versions: java: corretto11 build: commands: - echo Build started on `date` - mvn test |
今回はJava11を使いたかったので「corretto11」と指定していますが、Java8の場合は「corretto8」と指定してください。Mavenだと「mvn test」でテストコードのコンパイルと実行をしてくれます。
テストコードの実装
Mavenプロジェクトのtestパッケージの配下に格納されているAppTest.javaというファイルに、以下のテスト用メソッドを追加します。
今回は、まずはとりあえず動くことを確認したいので、ブラウザでGoogleを開いて「ChromeDriver」と検索するテストコードを記述しています。
1 2 3 4 5 6 7 8 9 10 11 12 |
public void testGoogleSearch() throws InterruptedException { System.setProperty("webdriver.chrome.driver", "[ChromeDriverのパス]"); final WebDriver driver = new ChromeDriver(); driver.get("http://www.google.com"); Thread.sleep(5000); final WebElement searchBox = driver.findElement(By.name("q")); searchBox.sendKeys("ChromeDriver"); searchBox.submit(); Thread.sleep(5000); driver.quit(); } |
CodePipelineの設定
今回は詳細は省きますが、構成図で紹介したとおり、CodePipelineを設定します。ざっくり手順は以下のとおりです。
- CodeCommitでリポジトリを作成する
- CodePipelineを作成する
- ソースプロバイダーで、作成したCodeCommitリポジトリを選択する
- ビルドステージの追加で、CodeBuildのビルドプロジェクトを新規作成する
(Buildspecの設定で、作成したbuildspec.ymlを指定する)
CodePipelineを作成する前にCodeBuildのビルドプロジェクトを作成しても全く問題なしです。私はいつもCodePipelineをつくる流れでビルドプロジェクトを作成しています。
リポジトリにコードをコミットしたら、それをトリガーにしてbuildspec.ymlが実行される、という流れが作れたらOKです。
CodeBuild+Seleniumで発生した課題&解決策
前置きが相当長くなってしまいましたが、ここからが今回の本題!CodePipelineが正常に動く前提として、CodeBuild実行時に発生した課題と、実際に私が行った解決策をご紹介します。
その1:Chrome/ChromeDriverのインストールは必要?
昔、Jenkins+Antでテスト自動化をやってみたことがあり、そのときはJenkinsサーバにChromeとChromeDriverをインストールした記憶があったんですね。なので、今回も自分でインストールしないといけないんだろうな、と思っていました。
しかし!なんとCodeBuildで選択可能な環境(Dockerイメージ、UbuntuまたはAmazon Linux 2から選択)には、デフォルトでChromeとChromeDriverが入っているのですね。これに気付くまで一生懸命自力でインストールしようとしてたよ…めっちゃ楽じゃんー!
ちなみにCodeBuildで選択可能なDockerイメージはこちら(GitHub)で公開されています。Dockerfileの中身を見ると、ChromeとChromeDriverだけじゃなく、デフォルトで何がインストールされるかわかりますね。
基本的には最新バージョンに近いChromeとChromeDriverがインストールされる&パスも通されるので、特に過去のバージョンを使う必要がなければ自力でインストールする必要はありません。パスはDockerfileにも記載されていますが、よく選択されそうなDockerイメージ2つについてパスをまとめると以下の表になります。
Chromeパス | ChromeDriverパス | |
Ubuntu standard:4.0 | /opt/google/chrome/google-chrome | /usr/bin/chromedriver |
amazon Linux 2 x86_64 standard:3.0 | /usr/bin/chromium-browser | /usr/bin/chromedriver |
テストコード内でChromeDriverを指定する際には、以下の例のように上記のパスを指定します。
1 |
System.setProperty("webdriver.chrome.driver", "/usr/bin/chromedriver"); |
その2:CodeBuild環境からテスト対象URLへアクセスできない
テスト対象のURLへアクセスするためには、CodeBuild環境からインターネットへ接続できるようにする必要があります。CodeBuildからS3へのアクセスは、インターネットを経由するということですね。
ビルドプロジェクトの環境設定でVPC設定があるので、そこでNATゲートウェイをルートテーブルに持つサブネットを指定することで解決できます。(細かい設定は割愛)
以下の例ではサブネットを2つ指定していますが、冗長化しなければ1つでOKです。
「VPC設定の確認」ボタンをクリックすると、インターネットに接続できているかどうかが確認できます。以下のように緑のチェックマークが表示されたら接続成功です。
その3:ChromeDriverが起動できない
ChromeもChromeDriverもインストールされていて、かつインターネットにも接続できる状態なのにテストが動かないので、もしやChromeDriverが起動できていないのでは…?という疑惑が浮上。buildspec.ymlにchromedriverコマンドを追加して無理やりにでも起動してみたところ、以下のメッセージが表示されました。
1 2 3 4 5 6 |
> chromedriver --verbose Starting ChromeDriver 80.0.3987.106 (xxxxxxxxxxxxxxxxxxxxxxxxx-refs/branch-heads/xxxxxxxxx) on port 9515 Only local connections are allowed. Please protect ports used by ChromeDriver and related test frameworks to prevent access by malicious code. [xxxxxxxxxx.xxx][SEVERE]: bind() failed: Cannot assign requested address (99) [xxxxxxxxxx.xxx][INFO]: listen on IPv6 failed with error ERR_ADDRESS_INVALID |
CodeBuild環境ではIPv6をサポートしていないのに、ChromeDriverはIPv6アドレスでリッスンしようとして失敗している、とのことです。
解決策としては、CodeBuild環境でIPv6をサポートするか、ChromeDriverをIPv4アドレスでリッスンするかの2択になるのですが、前者はすぐ出来なさそうなので後者で解決したいと思います。色々調べてみると、どうやらChromeDriverの起動時オプションに「–whitelisted-ips」(頭はハイフン2つ)を指定するといいよ、という情報を見つけました。これを指定するとIPv4アドレスでリッスンしてくれるみたいですね。
以下のように、ChromeOptionsオブジェクトでオプションを指定してみます。
1 2 3 |
ChromeOptions options = new ChromeOptions(); options.addArguments("--whitelisted-ips="); driver = new ChromeDriver(options); |
アドレスを絞りたい場合は、=の後ろにカンマ区切りでIPv4アドレスを指定すればいいようです。
これで解決~!…と思ったら?
–whitelisted-ipsオプションが効かない
はい、–whitelisted-ipsを指定しても、なぜかエラーが変わりませんでした。どうやらオプションがちゃんと適用されていない模様です。なぜ。
ChromeOptions以外でオプションを指定する方法があるのか?と調査したところ、システムプロパティで指定する方法があるようです。以下のように、ChromeOptionsではなくシステムプロパティとしてオプションを指定すると、無事にオプションが適用されました!めでたし。
1 |
System.setProperty("webdriver.chrome.whitelistedIps", ""); |
その4:テストを実行するたびにChromeDriverのポートが変わる
mvn testコマンドを実行するとテストコードが実行されて、そのテストコード内でChromeDriverが起動されるのですが、ログを見ていると、起動のたびにChromeDriverが使うポートが変わっていることに気付きました。
1 2 3 4 5 6 7 8 |
[INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running uitest.AppTest Starting ChromeDriver 80.0.3987.106 (xxxxxxxxxxxxxxxxxxxxxxxxx-refs/branch-heads/xxxxxxxxx) on port 1024 Only local connections are allowed. Please protect ports used by ChromeDriver and related test frameworks to prevent access by malicious code. [xxxxxxxxxx.xxx][SEVERE]: bind() failed: Cannot assign requested address (99) |
上の例だと1024番ポートですが、これが毎回変わるのです。
「その3:ChromeDriverが起動できない」のログと比較していただくとわかるのですが、ChromeDriverをコマンドで起動した場合は9515番ポートを使用しています。通常ChromeDriverが使用するポートはデフォルトで9515番のようです。
じゃあ、テストコードからも9515番ポートでChromeDriver起動したらいいのでは!ということで、ChromeDriver周りの実装を以下のように変更します。
1 2 3 4 5 6 |
final ChromeOptions option = new ChromeOptions(); option.addArguments(CHROME_OPTIONS); final ChromeDriverService service = new ChromeDriverService.Builder().usingPort(9515).build(); driver = new ChromeDriver(service, option); |
CHROME_OPTIONSは、ChromeDriver起動時オプションをまとめて定義した配列です。これで常に9515番ポートを使ってくれるようになりました。
おわりに
今回書いた内容を振り返って思ったのですが、SeleniumそのものというよりもほとんどChromeDriverの問題でした。Seleniumを使ったテストの実装は初めてではないし、同じような自動化をJenkinsで構築したこともあったのに、動かす環境が違うとこんなにも苦戦するものなのですね…。
私は、今回紹介した「その1」~「その4」の対応で無事にテストが実行できるようになりました。同じことでお悩みの方がいましたら、少しでも参考になれば幸いです。
執筆者プロフィール
- 社内の開発プロジェクトの技術支援や、新技術の検証に従事しています。主にアプリケーション開発系支援担当で、Java&サーバサイドが得意です。最近は、サーバーレスonAWSを推進しています。