はじめに
記念すべき第10回目(ドドーン)。
前回まででサインアップ、サインインの機能を作っていきました。
ただどんどんアプリケーションを作っていくうちに、テスト、面倒になってきましたね。デグレも気になるし...
ということで今回は少しアプリ開発を離れまして、TDD(Test Driven Development)、そしてテスト自動化を体得していきましょう!
前回のソースコード
前回のソースコードはこちらに格納してます。今回のハンズオンからやりたい場合はこちらからダウンロードしてください。
TDD
まずはTDDをご紹介。
特にアジャイル開発をやったりすると耳にする単語ですよね。
Test Driven Development、日本語だとテスト駆動開発です。
なんちゃら駆動開発って色々あるんですが、まぁ『なんちゃら』部分を第1に考えた開発って感じで、TDDの場合はテストファーストで開発をしていくって意味ですね。
TDDでは『レッド』『グリーン』『リファクタリング』の3つのフェーズを辿ります。
レッド
『レッド』はテストが通らないフェーズです。
TDDではまず機能をコーディングする前に期待動作のテストコードを書きます。
当然、機能をコーディングしていないのですからテストは通りません。この状態が『レッド』です。
TDDでは、この『レッド』のフェーズから機能をコーディングしていって『グリーン』のフェーズをめざします。
グリーン
『グリーン』はテストがとりあえず通ったフェーズです。
『レッド』フェーズからアプリをコーディングしていってテストコードが全て通った状態が『グリーン』フェーズです。
リファクタリング
『リファクタリング』はテストが通っていてコードとしても良い状態になったフェーズです。
『グリーン』フェーズは、『とりあえず動く』という状態です。
この『グリーン』な状態を保ちつつ、コードをより可読性高く、効率的に変更していくフェーズが『リファクタリング』です。
TDDではテスト自動化を行いますが、これがあるからこそ『動く』状態をキープしながらよりよいコードをめざせます。
TDDでは、機能やユーザーストーリー単位にこれらの3つのフェーズで開発を進めていくことで、デグレなくよいコードを作り上げることができます。
BDD
TDDの派生として、BDD(Behavior Driven Development)があります。
日本語では『振る舞い駆動開発』と言われています。
これは、TDDよりもより要望に近いテストを記述するフォーマットのようなものと覚えてもらえればよいかと思います。
開発の進め方はTDDと同じで、テストコードを記述して『レッド』『グリーン』『リファクタリング』をしていきます。
BDDでは、Given
、When
、Then
の3つをテストケースとして定義します。
-
Given
: 前提条件です。「サインイン済みのユーザーが」って感じです。 -
When
: 操作や入力です。「プロフィールページでサインアウトリンクをクリックしたとき」って感じです。 -
Then
: 期待結果です。「未サインイン状態になりトップページに遷移すること」って感じです。
はい、例を出してますが、BDDにのっとると例えば
サインイン済みのユーザーが、プロフィールページでサインアウトリンクをクリックしたとき、未サインイン状態になりトップページに遷移すること
というようなテスト項目を作れます。このようにBDDはよりユーザー目線の振る舞いをテスト項目として定義する考え方です。
このハンズオンでは、このBDDを使ってテストを定義し、テストコードを書いていきます。
テスト自動化
さて、アジャイル開発やTDD/BDDを採用する場合、テスト自動化が必須です。
というよりも一回作ったらもう絶対に追加で開発をしないシステム(どんなシステム?)以外は必須だと思っています。
リファクタリングをしていったり新しい機能を作っていくときに、全てのテストケースをいちいち人間の手でやっていくのは、時間的にも人員的にも不可能だとわかりますよね。
そこで、このハンズオンでもテスト自動化を導入していきます。
Rails(Ruby)には使いやすいテストフレームワークRSpecがあります。
またプログラムでWeb画面を操作するSeleniumとこれらをよりコーディングしやすくラッピングしてくれるCapybaraというフレームワークがあります。
このハンズオンでは、この3つのツールを使って実際にユーザーが操作しているのと同じ状態をテストするE2E(End to End)テストを自動化してみます。
必要なライブラリ類をインストールする
まずはテスト自動化を実行するために必要なライブラリ類をDockerイメージやRailsアプリにインストールしていきます。
最初に、Dockerコンテナ内でブラウザを立ち上げられないとE2Eテストが行えないので、Dockerfile
を編集してDockerイメージにブラウザをインストールします。
今回はChromeを立ち上げてテストできるようにします。
FROM ruby:2.6.5-alpine3.11
ENV HOME="/app"
ENV LANG=C.UTF-8
ENV TZ=Asia/Tokyo
WORKDIR $HOME
RUN apk update && \
apk upgrade && \
apk add --no-cache \
gcc \
g++ \
less \
libc-dev \
libxml2-dev \
linux-headers \
make \
nodejs \
postgresql \
postgresql-dev \
tzdata \
yarn && \
+ apk add --no-cache \
+ chromium \
+ chromium-chromedriver \
+ dbus \
+ mesa-dri-swrast \
+ ttf-freefont \
+ udev \
+ wait4ports \
+ xorg-server \
+ xvfb \
+ zlib-dev && \
apk add --virtual build-packs --no-cache \
build-base \
curl-dev
COPY Gemfile $HOME
COPY Gemfile.lock $HOME
RUN bundle install && \
apk del build-packs
COPY . $HOME
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]
これらのパッケージを追加することでDockerコンテナ内でChromeブラウザを起動させてテストすることができるようになります。
次にRailsアプリに必要なライブラリを追加していきます。
...
group :development, :test do
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
gem 'pry-rails'
+
+ # For test automation
+ gem 'rspec-rails', '~>3.9'
+ gem 'capybara'
+ gem 'selenium-webdriver'
end
...
ここまでで一度Dockerイメージを再ビルドしましょう。
$ docker-compose build
必要なライブラリ類がDockerイメージにインストールされた状態になります。
RSpecの初期設定をする
次にRailsアプリにRSpecの初期インストールをしていきます。
まず、ビルドしたDockerイメージからDockerコンテナを立ち上げて、rails g rspec:install
コマンドを実行します。
$ docker-compose up -d
$ docker-compose exec web ash
# rails g rspec:install
Running via Spring preloader in process 131
create .rspec
create spec
create spec/spec_helper.rb
create spec/rails_helper.rb
色々とファイルが出来上がりました。それぞれの役割は以下の通りです。
-
.rspec
: RSpecを実行するにあたりimportする設定ファイルを定義している -
spec/spec_helper.rb
: RSpecの設定ファイル -
spec/rails_helper.rb
: RSpecのRails要素をプラスした設定ファイル
rails_helper.rb
はspec_helper.rb
をオーバーライドしている間柄ですね。
ここで.rspec
を少し編集します。
- --require spec_helper
+ --require rails_helper
これでデフォルトでrails_helper.rb
が設定ファイルとして読み込まれるようになりました。
rails_helper.rb
にRSpecで利用するWebドライバーの設定をしていきます。
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../config/environment', __dir__)
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'
# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
begin
ActiveRecord::Migration.maintain_test_schema!
rescue ActiveRecord::PendingMigrationError => e
puts e.to_s.strip
exit 1
end
+
+ Capybara.register_driver :selenium_chrome_headless do |app|
+ options = ::Selenium::WebDriver::Chrome::Options.new
+ options.add_argument('--no-sandbox')
+ options.add_argument('--headless')
+ options.add_argument('--disable-gpu')
+ options.add_argument('--disable-dev-shm-usage')
+ options.add_argument('--window-size=1680,1050')
+ Capybara::Selenium::Driver.new(app, browser: :chrome, options: options)
+ end
RSpec.configure do |config|
+ # Driver setting for system tests.
+ config.before(:each, type: :system) do
+ driven_by :selenium_chrome_headless
+ end
+
+ config.before(:each, type: :system, js: false) do
+ driven_by :rack_test
+ end
config.fixture_path = "#{::Rails.root}/spec/fixtures"
config.use_transactional_fixtures = true
config.infer_spec_type_from_file_location!
config.filter_rails_from_backtrace!
end
Capybara.register_driver
でWebドライバーの設定を新しく作ります。
ちょっと書き方が独特な気がすると思いますが、こういうものなのだと思ってください。笑
途中に--headless
という設定がありますが、headless
は目に見える形でブラウザを立ち上げることなくブラウザテストを行うことができるものです。(ヘッドレス Chrome ことはじめ | Web | Google Developers)
これによってテストの実行速度が速くなったりするので是非使いましょう。
RSpec.configure
で2つのパターンのWebドライバー設定をしています。
config.before(:each, type: :system)
はシステムテストが実行される都度設定されるものことを意味しています。(システムテストはRSpecの用語でE2Eテストと同義と思っていただいていいかと)
driven_by
でドライバーを指定するのですが、デフォルトでは先ほど上で定義したselenium_chrome_headless
が、js: false
の場合にはrack_test
が設定されることがわかりますね。
rack_test
は高速にテストができるのですがjavascriptの機能などを見ることはできないドライバーです。
javascriptの確認をする必要がなく高速にテストを終えたいケースや、javascriptを動かしたくないテストケースに使えます。
また、このままではrails g
コマンドを実行したときに意図しないテストファイルが自動生成されてしまいます。
面倒なのでテスト関連のファイルが生成できないようにします。
...
module App
class Application < Rails::Application
config.load_defaults 6.0
# Don't generate system test files.
config.generators.system_tests = nil
# Timezone
config.time_zone = 'Tokyo'
config.active_record.default_timezone = :local
# Language
config.i18n.default_locale = :ja
+
+ # Don't create test files atomatically.
+ config.generators do |g|
+ g.test_framework :rspec,
+ fixtures: false,
+ view_specs: false,
+ helper_specs: false,
+ routing_specs: false,
+ controller_specs: false,
+ request_specs: false
+ end
end
end
ここまででRSpecでE2Eテストを実行する初期設定ができました!
テストが実行できるか試してみる
まずは試しにここまでの設定でちゃんとテスト自動化ができるのか試してみましょう。
まず、RSpecのシステムテストを実行するファイルを格納するspec/system
ディレクトリを作成し、その中にsample_spec.rb
の名前のサンプルファイルを用意してみましょう。
# mkdir spec/system
# touch spec/system/sample_spec.rb
RSpecは_spec.rb
をテストコードと判断して実行するようになっているのでお忘れなきよう。
ではsample_spec.rb
に「トップページにアクセスできること」を確認するテストコードを記述していきます。
feature "サンプルテスト", type: :system do
scenario "トップページにアクセスできること" do
visit root_path
expect(current_path).to eq root_path
end
end
テストコード自体はscenario
ブロックに囲まれた部分です。feature
は複数のscenario
を束ねるものでtype: :system
オプションをつけることでシステムテストとして実行することを宣言しています。
「トップページにアクセスできること」シナリオのテストコードの中身もちょっと紹介します。
visit root_path
でroot_path
、つまり/
にアクセスしています。
expect([検査対象]).to [期待結果]
で[検査対象]
が[期待結果]
であるかどうかを検査します。
今回の検査対象はcurrent_path
です。これは今表示されているページのパスのことです。期待結果がroot_path
なので、現在表示されているページが/
であればOK、そうでなければNGになります。
実はここで使っているvisit
やcurrent_path
はCapybaraのおかげでわかりやすい言葉で使えるようになっています。Capybaraの使い方は「使えるRSpec入門・その4「どんなブラウザ操作も自由自在!逆引きCapybara大辞典」 - Qiita」などがわかりやすいと思います。あとは全力で公式ドキュメントよむ。
このように操作と検査が一つのシナリオの中に記述され、検査が通るかどうかでOK/NGが判断されます。
もちろん、操作も検査も1つのシナリオの中で複数記述することができます。
では、このテストを実行してみましょう。
# rspec spec/system/sample_spec.rb
Capybara starting Puma...
* Version 4.3.1 , codename: Mysterious Traveller
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:40933
.
Finished in 16.97 seconds (files took 16.66 seconds to load)
1 example, 0 failures
RSpecのテストを実行するときはrspec
コマンドを使います。今回の例のようにファイル名を指定することで、そのテストファイルのみが実行されます。
ディレクトリを指定した場合は、そのディレクトリの全てのテストコードが、rspec
だけの場合はspec
ディレクトリの全てのテストファイルが実行されるます。
最後に1 example, 0 failures
と表示されています。example
はシナリオ数、failures
はそのうちNGだった数を表すので、今回は1つのテストシナリオが実行され全てOKであったことがわかります。
試しに、エラーになったときにどうなるか試してみましょう。
feature "サンプルテスト", type: :system do
scenario "トップページにアクセスできること" do
visit root_path
- expect(current_path).to eq root_path
+ expect(current_path).to eq sign_up_path
end
end
Capybara starting Puma...
* Version 4.3.1 , codename: Mysterious Traveller
* Min threads: 0, max threads: 4
* Listening on tcp://127.0.0.1:37237
[Screenshot]: /app/tmp/screenshots/failures_r_spec_example_groups_nested_トップページにアクセスできること_549.png
F
Failures:
1) サンプルテスト トップページにアクセスできること
Failure/Error: expect(current_path).to eq sign_up_path
expected: "/sign_up"
got: "/"
(compared using ==)
# ./spec/system/sample_spec.rb:4:in `block (2 levels) in <main>'
Finished in 4.23 seconds (files took 6.85 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/system/sample_spec.rb:2 # サンプルテスト トップページにアクセスできること
1 example, 1 failure
なのでNGになっていることがわかりますね。
途中の内容をみてみると、expected: "/sign_up"
に対してgot: "/"
であるためにNGになっていることが分かります。想定通りテストがNGになりました。
RSpecのシステムテストではテストがNGになった場合、自動的にスクリーンショットを保存して置いてくれます。そのファイルはtmp/screenshots/
に保存されます。
ただ、ファイルをみると真っ白。本当はroot_path
にアクセスしているのでトップページが表示されていてほしいですよね。
実はこれRSpecのバグっぽいんですよね...(Rails アプリケーションの不安定なテストを撲滅したい 〜system spec のデバッグ方法とテストを不安定にさせる要因〜 - あらびき日記)
ということでスクリーンショットがちゃんと表示されるようにヘルパーを作ってみましょう!
スクリーンショットを正しく表示させる
まずはテストを実行するときにヘルパーファイルを読み取るようにします。
これはrails_helper.rb
でコメントアウトを外すだけでOKです。
...
- # Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
+ Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }
...
これで、spec/support/**/*.rb
のファイルがテスト実行時に読み込まれるようになります。
ヘルパーファイルを格納しておくディレクトリを作って、スクリーンショットを表示させるためのヘルパーとしてvisible_screenshot_helper.rb
を作りましょう!
# mkdir -p spec/support/helpers
# touch spec/support/helpers/visible_screenshot_helper.rb
module VisibleScreenshotHelper extend ActiveSupport::Concern
included do |example_group|
example_group.after do
take_failed_screenshot
end
end
def take_failed_screenshot
return if @is_failed_screenshot_taken
super
@is_failed_screenshot_taken = true
end
end
RSpec.configure do |config|
config.include VisibleScreenshotHelper, type: :system
end
「Rails アプリケーションの不安定なテストを撲滅したい 〜system spec のデバッグ方法とテストを不安定にさせる要因〜 - あらびき日記」の記事を参考にしました。
これでもう一度テストを実行してみましょう。スクリーンショットがちゃんと表示されるようになるはずです。
テスト開始前にスクリーンショットを削除する
こうやってテストしているとスクリーンショットが溜まっていっちゃいますよね...
ということでテスト実行の直前にスクリーンショットを一度全て削除するようにrails_helper.rb
に設定を追加します。
...
RSpec.configure do |config|
# Driver setting for system tests.
config.before(:each, type: :system) do
driven_by :selenium_chrome_headless
end
config.before(:each, type: :system, js: false) do
driven_by :rack_test
end
+
+ # Delete screenshots before starting new tests
+ config.before(:all) do
+ FileUtils.rm_rf(Rails.root.join('tmp', 'screenshots'), secure: true)
+ end
config.fixture_path = "#{::Rails.root}/spec/fixtures"
config.use_transactional_fixtures = true
config.infer_spec_type_from_file_location!
config.filter_rails_from_backtrace!
end
これを追加するだけです。config.before(:all)
はテスト実行前に1度だけ実行されることを意味しています。
FileUtils.rm_rf
はRubyプログラムとしてrm -rf
をやるためのモジュールです。
「【Rails】RSpecのSystem Test実行前に前回テスト時のScreenshotを削除しておく - Qiita」
これでスクリーンショットが溢れかえる心配がなくなりました♪
(オプション)iTerm2でスクリーンショットを表示する
ちょっと裏技みたいな感じですが、iTerm2の場合、webコンテナにとある環境変数を与えるとテストNGのタイミングでiTerm2上にスクリーンショットを表示してくれるようになります。
version: '3'
services:
db:
image: postgres:12.1-alpine
environment:
- TZ=Asia/Tokyo
volumes:
- ./tmp/db:/var/lib/postgresql/data
web:
build: .
volumes:
- .:/app
ports:
- 3000:3000
depends_on:
- db
+ environment:
+ - RAILS_SYSTEM_TESTING_SCREENSHOT=inline
たったこれだけ。この環境変数を適用するために一度コンテナを再起動してRSpecを実行してみましょう。
# exit
$ docker-compose down
$ docker-compose up -d
$ docker-compose exec web ash
# rspec spec/system/sample_spec.rb
ちゃんとスクリーンショットが表示されていますね。
RSpecのスクリーンショットはどのファイルなのか探すのが面倒だったりするので、テストシナリオと紐づいてiTerm2で表示してくれるのは非常に助かります。
本日はここまでにしましょう!
後片付け
では後片付けしていきますー。
今回は特にDBにデータも保存していないのでDBを初期化する必要もありませんね。
そういえば、RSpecはテスト実行時に保存されたデータはテストシナリオごとに削除されるようになっているので、テストコード内でデータを保存したとしてもDBの初期化は必要ないんです。(しかもRSpecでテストをする場合、勝手にRAILS_ENV
がtest
で実行されるので、development
のDBを汚染することもないんです。)
と、いうことで今回はコンテナだけ落として終了です!
# exit
$ docker-compose down
あ、あと、sample_spec.rb
は今回のサンプル用だったので消しておきましょー。
あ、ついでにスクリーンショットも。
$ rm spec/system/sample_spec.rb
$ rm tmp/screenshots/*
まとめ
今回はTDD/BDDを説明させていただきました。
Red → Green → リファクタリング の流れと、
Given、When、Then で受入条件を考えていく方法を紹介しましたね。
さらにテスト自動化を実現するために、
RSpecを導入しました。(SeleniumとCapybaraも)
そして初めてのテストコードを書いて実行することができましたね!
次回は、実際にここまでつくってきたアプリのテストコードをコーディングします。
その中でテストコードの書き方を覚えていきましょう!
では、次回も乞うご期待!ここまでお読みいただきありがとうございました!
Next: コーディング未経験のPO/PdMのためのRails on Dockerハンズオン vol.11 - Test coding - - Qiita
本日のソースコード
Reference
- TDDがうまくいかないときは、BDD形式でバックログを書いてみる | Raksul ENGINEERING
- 【Rails】こわくない!TDD/BDD・テスト自動化はじめの一歩ハンズオン! - Qiita
- 使えるRSpec入門・その4「どんなブラウザ操作も自由自在!逆引きCapybara大辞典」 - Qiita
- Rails アプリケーションの不安定なテストを撲滅したい 〜system spec のデバッグ方法とテストを不安定にさせる要因〜 - あらびき日記