3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

コーディング未経験のPO/PdMのためのRails on Dockerハンズオン vol.10 - TDD & Test Automation -

Last updated at Posted at 2020-03-09

はじめに

記念すべき第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では、GivenWhenThenの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を立ち上げてテストできるようにします。

Dockerfile
  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アプリに必要なライブラリを追加していきます。

Gemfile
  ...
  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.rbspec_helper.rbをオーバーライドしている間柄ですね。

ここで.rspecを少し編集します。

.rspec
- --require spec_helper
+ --require rails_helper

これでデフォルトでrails_helper.rbが設定ファイルとして読み込まれるようになりました。
rails_helper.rbにRSpecで利用するWebドライバーの設定をしていきます。

spec/rails_helper.rb
  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コマンドを実行したときに意図しないテストファイルが自動生成されてしまいます。
面倒なのでテスト関連のファイルが生成できないようにします。

config/application.rb
  ...
  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に「トップページにアクセスできること」を確認するテストコードを記述していきます。

spec/system/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_pathroot_path、つまり/にアクセスしています。
expect([検査対象]).to [期待結果][検査対象][期待結果]であるかどうかを検査します。
今回の検査対象はcurrent_pathです。これは今表示されているページのパスのことです。期待結果がroot_pathなので、現在表示されているページが/であればOK、そうでなければNGになります。

実はここで使っているvisitcurrent_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であったことがわかります。

試しに、エラーになったときにどうなるか試してみましょう。

spec/system/sample_spec.rb
  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です。

spec/rails_helper.rb
  ...
- # 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
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に設定を追加します。

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上にスクリーンショットを表示してくれるようになります。

docker-compose.yml
  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

image.png
ちゃんとスクリーンショットが表示されていますね。
RSpecのスクリーンショットはどのファイルなのか探すのが面倒だったりするので、テストシナリオと紐づいてiTerm2で表示してくれるのは非常に助かります。

本日はここまでにしましょう!

後片付け

では後片付けしていきますー。
今回は特にDBにデータも保存していないのでDBを初期化する必要もありませんね。
そういえば、RSpecはテスト実行時に保存されたデータはテストシナリオごとに削除されるようになっているので、テストコード内でデータを保存したとしてもDBの初期化は必要ないんです。(しかもRSpecでテストをする場合、勝手にRAILS_ENVtestで実行されるので、developmentのDBを汚染することもないんです。)

と、いうことで今回はコンテナだけ落として終了です!

# exit 
$ docker-compose down

あ、あと、sample_spec.rbは今回のサンプル用だったので消しておきましょー。
あ、ついでにスクリーンショットも。

$ rm spec/system/sample_spec.rb
$ rm tmp/screenshots/*

まとめ

今回はTDD/BDDを説明させていただきました。
RedGreenリファクタリング の流れと、
GivenWhenThen で受入条件を考えていく方法を紹介しましたね。

さらにテスト自動化を実現するために、
RSpecを導入しました。(SeleniumCapybaraも)
そして初めてのテストコードを書いて実行することができましたね!

次回は、実際にここまでつくってきたアプリのテストコードをコーディングします。
その中でテストコードの書き方を覚えていきましょう!

では、次回も乞うご期待!ここまでお読みいただきありがとうございました!

Next: コーディング未経験のPO/PdMのためのRails on Dockerハンズオン vol.11 - Test coding - - Qiita

本日のソースコード

Reference

Other Hands-on Links

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?