こんにちは、Studyplus for School事業部エンジニアの島田です。
もうすぐ2020年も終わりになりますね。
はじめに
皆さんはスモークテストをしていますか?
スモークテストとは元々「電子機器での発煙がないかをテストしていたこと」を起源とし、そこから転じて「ソースコードの開発・追加・修正を終えたソフトウェアが動作する状態にあるかを確認するテストのこと」となったようです。
Studyplus For School(以下FS)の開発チームでは、こちらの記事でも少しふれているStatic vs Unit vs Integration vs E2E Testing for Frontend Apps にあるEnd to End(E2E)をスモークテストとして位置付けています。
www.itmedia.co.jp
testingjavascript.com
tech.studyplus.co.jp
なぜ導入したか
これまでE2Eテストの導入を検討した事はあったのですが、
- どこまでをテストすべきか
- 運用負荷が高そう。UIの変更に追随することが大変ではないか
- UI変更に対応できなくなるとテストが落ちても気にしなくなり、テストが狼少年になってしまうのではないか
といった懸念がありましたが、上記の記事にあるIntegration testsの線引きを決め、E2Eテストではサービスの重要な機能に絞った最低限のテストにする事を決めました。
また、こちらの記事で少し触れているログイン関連の改修ではリリース後に一部機能の不具合が発覚しました。そのためログインなどのクリティカルな機能ではE2Eによる退行テストが必要だと強く感じるようになりました。
tech.studyplus.co.jp
ツール選定
E2Eテストを検討するにあたっては、まずサービス、テストフレームワークの選定をしました。
様々な候補が出たのですが、それぞれを調査・検討するには数が多過ぎるので、実績や知名度や特性から以下に絞りました。
SaaSとOSS(課金へのアップグレードもある)のそれぞれ2つをリストアップし、4つのサービス、フレームワークの調査・比較をする事にしました。
autify.com
www.lambdatest.com
playwright.dev
www.cypress.io
それぞれについて以下の内容を中心に調査をしました。
- 選定にあたって
- 初期コスト(導入、学習等)
- 運用コスト(課金有無、自前で構成する場合)
- テストの実装方法
各メンバーで分担して調査した結果を元にチームで検討した概略が以下になります。
- Autify
- メリット
- コードを書かなくて良い。エンジニア以外でも出来る
- テスト実行までの最初の設定が手軽そう
- 日本語ドキュメント、サポートがある
- デメリット
- 課金が比較的高い
- テストコードという資産が残らない(他への乗り換えが難しくなる)
- LambdaTest
- メリット
- ローカルで簡単に様々なブラウザでの実行が確認できる
- ダッシュボードで様々な結果(動画、リクエスト内容)が確認出来る(年間契約: $99/月)
- 既存の言語・テストフレームワークで書ける
- 自動テスト以外の機能も充実している
- Integrationが多い
- デメリット
- Seleniumの学習コスト
- 海外での導入実績はそれなりにあるが、サービスとしての信頼性は未知数
- Playwright
- メリット
- デメリット
- 学習コスト高そう。実装に慣れるのが大変そうな印象
- 実行環境の準備・運用
- 実行結果(キャプチャ)の保存をしたい場合に、自分たちで考える必要がある
- Cypress
- メリット
- ダッシュボードで様々な結果(動画、リクエスト内容)が確認出来る(年間契約: $99/月)
- デメリット
- 学習コスト。Playwrightと比べれば低そうではある
これらの内容から、
- 導入コスト:Playwright > Cypress > LambdaTest > Autify
- 運用コスト:Autify > LambdaTest > Cypress > Playwright
- 金額: Autify > LambdaTest , Cypress > Playwright
と判断し、LambdaTest か Cypress が争点となりました。スモークテストのみをするのであれば両者とも大きな差異はないと判断しました。そのため同じ金額を課金するのであれば+α(クロスブラウザチェックなど)が出来るLambdaTestを採用しました。
※ 上記はあくまでFSチーム内での簡易的に調査した内容なので、もしかしたら認識が違っている箇所もあるかと思いますので、もし修正点があればご指摘いただければと思います。
LambdaTestでE2Eテスト(自動テスト)
LambdaTestでE2Eテストをするには、SeleniumのリモートWebDriver経由でLambdaTestのSelenium Automation Gridを利用して様々なブラウザのテストを実行する事ができます。
そのため様々な言語・フレームワークで実現をすることができます。こちらに詳しく記載されています。
ドキュメント以外にもこちらに実装のサンプルがあります。
FSチームでは学習コスト等を考えて、RSpecでテストを実装することにしました。
www.lambdatest.com
github.com
構成とテストの実行タイミング
FSのインフラ構成とデプロイについてはこちらの記事に詳しい説明があります。アプリケーションはSPAでサーバー(API)とクライアント(Web)で構成され、デプロイにはJenkinsを利用しています。
サーバーの環境には、開発・ステージング・本番の3つがあります。
E2Eテストの実行タイミング、フローの概要は、
- 各リポジトリ(サーバー or クライアント)にてmasterへマージ
- Jenkinsで開発環境にデプロイ
- JenkinsからCircleCI経由(APIでpipelineを実行)でE2Eテストを実行(SeleniumのリモートWebDriverでLambdaTestに接続)
- 問題なければ、ステージング、本番へリリース
という感じになります。
E2Eテスト概要図
E2Eテストについてはアプリケーションとは別のリポジトリで管理し、通常のbuild(単体テスト、Lint)とは分けてデプロイ後に実行する事としました。そうした理由としては、
- アプリケーションがサーバー(API)とクライアント(Web)でGithubのリポジトリが分かれて管理しているため、それぞれのデプロイに対してE2Eテストを実行したい
- E2Eテストは、実行環境(サーバー)にデプロイしてからでないと確認が出来ない
- スモークテストの位置付けとしては、(本番にリリースされなければ)masterのbranchマージ後に問題が発覚すれば良い
といった事が上げられます。
circleci.com
tech.studyplus.co.jp
E2Eテストコード(RSpec)の実装例
RSpecで実装概略です。
リポジトリ構成
$ tree -L 2 -a
.
├── .circleci
│ └── config.yml
├── .envrc
├── Gemfile
├── Gemfile.lock
├── spec
│ ├── login_spec.rb
│ └── spec_helper.rb
└── vendor
└── bundle
Gemfile
source 'https://rubygems.org'
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
gem 'retriable'
gem 'rspec'
gem 'rubocop', require: false
gem 'selenium-webdriver'
spec/spec_helper.rb
@driver.execute_script "lambda-status=#{lambda_status}"
はLamdaTestからSlackで通知をするために、LamdaTestにRSpecの成功可否を知らせるために設定しています。
@driver = Retriable.retriable(on: Net::ReadTimeout) do
はLamdaTestにてタイムアウトする事が稀にあるので、その際にリトライをするようにしています。
Slack通知成功
Slackエラー通知
require 'selenium-webdriver'
require 'retriable'
require 'net/protocol'
require 'net/http'
RSpec.configure do |config|
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
config.shared_context_metadata_behavior = :apply_to_host_groups
config.after(:example) do |example|
lambda_status = example.exception ? 'failed' : 'passed'
@driver.execute_script "lambda-status=#{lambda_status}"
end
config.around(:example) do |example|
caps = {
browserName: ENV['LT_BROWSER'],
version: ENV.fetch('LT_BROWSER_VERSION') { 'latest' },
platform: ENV['LT_OPERATING_SYSTEM'],
name: example.metadata[:description] || example.metadata[:location] || 'RSpec Sample Test',
build: 'RSpec Selenium Boron',
network: true,
visual: true,
video: true,
console: true,
tags: [example.metadata[:file_path].split('/').last&.split('.')&.first]
}
@driver = Retriable.retriable(on: Net::ReadTimeout) do
client = Selenium::WebDriver::Remote::Http::Default.new
client.read_timeout = 120
Selenium::WebDriver.for(
:remote,
http_client: client,
url: "https://#{ENV['LT_USERNAME']}:#{ENV['LT_APPKEY']}@hub.lambdatest.com/wd/hub",
desired_capabilities: caps
)
end
begin
example.run
ensure
@driver.quit
end
end
end
spec/login_spec.rb
クリティカルなテストケースのみ(今回はログイン)として、なるべくUIの状態に依存しないシンプルな実装(最低限のxpath)を心がけました。
RSpec.describe 'login' do
describe 'ログイン・ログアウト' do
let(:email) { 'test@example.com' }
let(:password) { 'sample' }
context '未ログインの場合' do
before do
@driver.manage.window.maximize
@driver.get("#{ENV['WEB_URL']}/login")
@driver.find_element(:xpath, "//button[contains(text(), 'ログイン')]").click
email_element = @driver.find_element(:name, 'operator[email]')
email_element.send_keys(email)
password_element = @driver.find_element(:name, 'operator[password]')
password_element.send_keys(password)
end
subject { @driver.find_element(:xpath, "//button[contains(text(), 'ログイン')]").click }
context '正しいメールアドレス、パスワード' do
it 'ログイン出来る' do
subject
expect(@driver.current_url).to eq "#{ENV['WEB_URL']}/?login=success"
end
end
...
end
end
end
.circleci/config.yml
実行結果についてはSlackで通知されます。
CircleCIのSlack通知
version: 2.1
orbs:
slack: circleci/slack@4.1.1
executors:
default:
working_directory: ~/test-e2e
docker:
- image: cimg/ruby:2.6-browsers
environment:
LT_OPERATING_SYSTEM: win10
LT_BROWSER: chrome
WEB_URL: https://example.com
commands:
install_dependencies:
steps:
- run:
name: gem install bundler v1.17.2
command: |
gem install bundler:1.17.2
- run:
name: bundle install
command: |
bundle install -j4 --path vendor/bundle
notify_failed:
steps:
- slack/notify:
event: fail
mentions: '@engineer'
template: basic_fail_1
notify_success:
steps:
- slack/notify:
event: pass
custom: |
{
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "E2E Test Successful! :tada:",
"emoji": true
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Project*:$CIRCLE_PROJECT_REPONAME"
},
{
"type": "mrkdwn",
"text": "*When*:$(date +'%m/%d/%Y %T')"
},
{
"type": "mrkdwn",
"text": "*Tag*:$CIRCLE_TAG"
}
],
"accessory": {
"type": "image",
"image_url": "https://assets.brandfolder.com/otz5mn-bw4j2w-6jzqo8/original/circle-logo-badge-black.png",
"alt_text": "CircleCI logo"
}
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "View Job"
},
"url": "${CIRCLE_BUILD_URL}"
}
]
}
]
}
jobs:
build:
executor: default
steps:
- checkout
- restore_cache:
keys:
- v2-bundler-{{ arch }}-{{ checksum "Gemfile.lock" }}
- v2-bundler-{{ arch }}-
- install_dependencies
- save_cache:
key: v2-bundler-{{ arch }}-{{ checksum "Gemfile.lock" }}
paths:
- vendor/bundle
- persist_to_workspace:
root: ~/test-e2e
paths:
- ./*
- notify_failed
rspec:
executor: default
steps:
- attach_workspace:
at: ~/test-e2e
- restore_cache:
keys:
- v2-bundler-{{ arch }}-{{ checksum "Gemfile.lock" }}
- v2-bundler-{{ arch }}-
- install_dependencies
- run:
name: run test
command: |
bundle exec rspec
when: always
- notify_failed
notify_success:
executor: default
steps:
- notify_success
workflows:
build:
jobs:
- build
- rspec:
requires:
- build
- rubocop:
requires:
- build
- notify_success:
requires:
- rspec
JenkinsからCircleCIのジョブを実行する際のAPI呼び出し例
curl --request POST \
-u '${CIRCLECI_TOKEN}:' \
--header 'content-type: application/json' \
--data '{"branch": "main"}' \
--url 'https://circleci.com/api/v2/project/${vcs-slug}/${org-name}/${repo-name}/pipeline'
LambdaTestのダッシュボード
Seleniumによる自動テストの経過・結果はLambdaTestのダッシュボードで確認することができます。Seleniumのvideo
オプション等を有効にしておくと、テストの動画やリクエスト内容などブラウザでの実行を確認することが出来ます。
Lambdatest Automation
さいごに
LambdaTestを導入するまでにかかったコストは低く、実行結果が簡単に分かりやすく確認出来る事は大きなメリットだと感じています。
また自動テスト以外にも、様々なブラウザで検証できるクロスブラウザテスト等の機能が充実しており自動テスト以外でも導入の恩恵は大きいと感じています。
ただ、自動テストで時々タイムアウトになるなど不安定な時がありました。あまりに常態化するとテストが落ちても気にしなくなりテストをしている意味がなくなるので、そこが心配ではありますが現時点ではほぼ発生しないので気になってはいません。
今後はE2Eによるクリティカルな機能のテストケースの実装を追加し、より保守性を向上させると共に手動で実施するテストを減らし生産性を高めていきたいと考えています。