Rails+RSpecで気軽に始めるテスト

この記事は、 2021/9/30 に行われた WESEEK Tech Conference の内容です。

目次

テストについて普段思うこと

みなさん普段テストについて思っていることはありますか?

  • テストをどこから書いていけばいいかわからない
  • テストの無いプロジェクトにどうやって導入すればいいのか
  • ましてや、TDDは敷居が高そう
  • テストを書く時間がない
  • テストを書いたが、いちいち手元で実行するのが面倒
  • 仕様と実装の整合性を合わせるのが大変

テストを書く際にこのあたりのことについて思ったりしたことがあるのではないでしょうか。そこで、今回は下記についてお話をしていこうと思います。

  • テストができるようになるまで ツールの導入
  • はじめの一歩 テストの書き方
  • Gitにプッシュしたらテストを自動で実行する方法 CI
  • 挫折しないテストの心得 開発ルール
  • 仕様と実装の整合性を楽にする スキーマファースト

今回の内容は、まだプロジェクトにテストツールを導入したことがない方、テストを書いたことがない方向けの、入門的な内容となります。

テストができるようになるまで ツールの導入

まず、テストを実行できるようになるためのツールの導入を行います。

テストができるようになるまでにやることは下記です。

  • RSpec の導入
  • テストデータベースの設定
  • RSpec の起動を速くする設定
  • RSpec の並列実行の設定
  • RSpec の実行

RSpec の導入

Ruby で使えるテストフレームワーク RSpec をインストールします。
これは、 Gemfile に下記を記載して bundle install を行います。

group :development, :test do
  gem 'rspec-rails', '~> 5.0.0'
end
$ bunlde install

テストデータベースの設定

テストを実行するために、一時的にデータを保存するためのデータベースが必要です。これは、特に設定をしなくとも Rails アプリケーションを作成した際に、自動的に設定されている場合が多いです。

config/database.yml にデータベースの設定が記載されています。 PostgreSQL/MariaDB をデータベースに使用する場合の記載例は下記です。

test:
    <<: *default
    database: projects_test

RSpec の起動を速くする設定

次に、 Rspec の起動を速くするための設定を行います。 binstub をインストールします。 preloader である Spring を使用して、 Rails アプリケーションをバックグラウンドで走らせておき、RSpec の起動を高速化します。

Gemfile に下記を記載して、 bundle install します。

group :development do
    gem 'spring-commands-rspec'
end
$ bundle install

RSpec の並列実行の設定

テストケースが多くなってくると、実行時間も増えます。開発をする上で、テストの実行時間はできる限り短くしたいものです。そこで、テストを並列実行して高速化を図る rspec-queue を導入します。

これも、 Gemfile に下記を記載し、 bundle install を行います。

group :development do
    gem 'rspec-queue'
end
$ bundle install

RSpec の実行

以上で、 Rspec を導入するための準備は完了です。
下記のコマンドで RSpec を実行してみます。

通常実行:

$ bin/rspec spec/

並列実行:

$ bundle exec rspec-queue spec/

はじめの一歩 テストの書き方

テストファイルの作成

テストの準備ができたら、さっそくテストを書いてみます。
今回は、モデルのテストを書いてみます。

下記コマンドを実行し、ジェネレータで User モデルのテストを作成します。

$ bin/rails g rspec:model user

すると、下記のモデルスペックファイルが作成されます。

require 'rails_helper'

Rspec.describe User, type: :model do
    pending "add some examples to (or delete) #{__FILE__}"
end

テスト項目の書き出し

pending "add some examples to (or delete) #{__FILE__}" の箇所を書き換えて、テストを書いていきます。

テストを書き始める前に、テストしたい項目を書き出してみましょう。
it に続けて自然な英文になるよう、動詞から書き始めます。日本語で書いても大丈夫です。

require 'rails_helper'

Rspec.describe User, type: :model do
    it "is valid with a name, email and password"
    it "is invalid without a name"
    it "is invalid without an email address"
    it "is invalid without a duplicate email address"
end

4つのテスト項目を書き出しました。

  • name, email, password が有効であること
  • name が存在せず無効であること
  • email address が存在せず無効であること
  • email address が重複し無効であること

ここで、一度テストを実行してみます。

$ bundle exec rspec-queue spec/

すると、下記のように結果が表示されました。

****

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) User is valid with a name, email and password
     # Not yet implemented
     # ./spec/models/user_spec.rb:6

  2) User is invalid without a name
     # Not yet implemented
     # ./spec/models/user_spec.rb:7

  3) User is invalid without an email address
     # Not yet implemented
     # ./spec/models/user_spec.rb:8

  4) User is invalid without a duplicate email address
     # Not yet implemented
     # ./spec/models/user_spec.rb:9

Finished in 0.00086 seconds (files took 1.58 seconds to load)
4 examples, 0 failures, 4 pending

先頭にある * は pending であるテストがあることを表します。この場合は 4 つ pending のテストが存在します。

テストを書く

今回は、 it "is valid with a name, email and password" のテストの内容を書いてみます。

require 'rails_helper'

Rspec.describe User, type: :model do
    it "is valid with a name, email and password" do
        user = User.new(
            name: "hoge1",
            email: "hoge1@example.com",
            password: "password"
        )
        expect(user).to be_valid
    end
    it "is invalid without a name"
    ...

User.new でテストに必要なデータを用意し、これがモデルのバリデーションをクリアしているか expect(user) で評価しています。

再度、テストを実行してみます。結果はこちらです。

.***

Pending: (Failures listed here are expected and do not affect your suite's status)

  1) User is invalid without a name
     # Not yet implemented
     # ./spec/models/user_spec.rb:14

  2) User is invalid without an email address
     # Not yet implemented
     # ./spec/models/user_spec.rb:15

  3) User is invalid without a duplicate email address
     # Not yet implemented
     # ./spec/models/user_spec.rb:16

Finished in 0.01858 seconds (files took 5.57 seconds to load)
4 examples, 0 failures, 3 pending

. はテストがパスしたことを表します。 pending は 3 つになりました。このようにして、テストの内容を記述していき、全てテストが . になるのを目指します。

テストデータの作成

先程の例では、テストケース内で一時データを作成していました。テストシナリオが複雑化してくると、テストデータの作成も複雑化してきます。できるだけ、テストを書くことに専念したいため、データは手軽に準備したいところです。そこで、 FactoryBot gem を導入します。

Gemfile に下記のように記載し、 bundle install します。

group :development, :test do
    gem 'factory_bot_rails'
end
$ bundle install

次に、ジェネレータを使って作られるモデルに対して、自動的にファクトリも作られるように設定します。すでにあるモデルについては自動でファクトリは作成されないので、手動でファクトリを追加します。

config/application.rb に下記の記述を追加します。

config.generators do |g|
    g.test_framework :rspec,
        view_specs: false,
        helper_specs: false,
        routing_specs: false,
        request_specs: false,
end

User モデルはすでに作成されているため、手動でファクトリを追加しましょう。

$ bin/rails g factory_bot:model user

すると、次のようなファイルが spec/factories/users.rb に作成されます。

FactoryBot.define do
    factory :user do

    end
end

ファクトリの内容を書いていきます。

FactoryBot.define do
    factory :user do
       sequence(:name) { |n| "hoge#{n}" }
       sequence(:email) { |n| "hoge#{n}@example.com" }
       password { 'password' }
    end
end

sequence を使って 呼び出される毎に hoge1, hoge2, hoge3 ... のようなインクリメントされる値を生成するようにします。

ファクトリの準備ができたので、 User スペックを書き換えます。

書き換え前:

require 'rails_helper'

Rspec.describe User, type: :model do
    it 'is valid with a name, email and password' do
        user = User.new(
            name: 'hoge1',
            email: 'hoge1@example.com',
            password: 'password'
        )
        expect(user).to be_valid
    end
    it 'is invalid without a name'
    ...

書き換え後:

require 'rails_helper'

Rspec.describe User, type: :model do
    it 'is valid with a name, email and password' do
        user = build(:user)
        expect(user).to be_valid
    end
    it 'is invalid without a name'
    …

user = build(:user) の1行でファクトリからデータを作成できるようになりました。ここでテストを実行しても、パスするはずです。

さて、 build(:user) には何が入っているのでしょうか。
binding.pry を使って見てみると、下記のようなデータになっていることがわかります。

#<User
    id: nil,
    name: "example_1",
    created_at: nil,
    updated_at: nil,
    lock_version: 0,
    email: "hoge1@example.com",
    password: nil
>

ファクトリからユーザーモデルのデータが作成されています。

テストデータのトランザクション

Rspec でのテストデータの取り扱いについて少し説明します。

RSpec はテストケースごとにトランザクションが張られています。1つのテストが終わる度にロールバックするので、テストの独立性が保たれています。

より複雑なトランザクションを制御したい場合は、 Database Cleaner Gem を使いますが、基本的には標準のままで事足ります。

より詳しい RSpec の構文について

今回は時間が限られているため、 RSpec の詳しい書き方については触れませんでした。 RSpec でのテストの書き方についてはより詳しい書籍があるため、そちらを紹介します。

Everyday Rails - RSpecによるRailsテスト入門
Aaron Sumner, Junichi Ito (伊藤淳一), AKIMOTO Toshiharu, 魚振江
https://leanpub.com/everydayrailsrspec-jp

Gitにプッシュしたらテストを自動で実行する方法 CI

CI の導入

自動でテストが実行されていない場合、毎回、ローカル環境でテストを実行するのは面倒ですし、レビュー時にテストがパスしている品質のものが来ているのか不安に思います。

そこで、 Continuous Integration(継続的インテグレーション)を導入しましょう。

CI とは

Continuous Integration(継続的インテグレーション) は、Git 等リポジトリへのコードの変更を契機に、自動化されたビルドやテストが実行される環境とその開発手法のことを言います。

CI を実現するものには、 GitHub Actions, Jenkins, CircleCI などがあります。
これから CI を導入するなら GitHub Actions のほうが手軽です。

GitHub Actions での CI 構築

それでは、今回は GitHub Actions で CI を組んでみます。

まず、 CI で何を実施したいかを考えます。

  1. テストに必要なツールの準備
    • ruby のインストール
    • DB の準備
  2. lint (静的解析ツール) の実行
  3. テストの実行

それでは、構築していきます。

まず、 CI を構築したい GitHub リポジトリの Actions タブを選択します。
次に New workflow ボタンを押します。

すると、テンプレートの一覧が表示されます。今回は、 Ruby の Set up this workflow ボタンを選択しましょう。

workflow の編集画面に遷移します。 workflow の名前を変更しておきます。それでは、実際に編集していきます。

今回は、下記のような YAML を作成しました。

name: Ruby on Rails CI

on:
  push:
    branches-ignore:
      - stable

jobs:
  rails-test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        ruby-version:
          - 2.7.4

    env:
      RAILS_ENV: test
      PROJECT_DATABASE_HOST: 127.0.0.1
      PROJECT_DATABASE_PORT: 3306
      PROJECT_DATABASE_DBNAME: db_test
      PROJECT_DATABASE_USERNAME: user
      PROJECT_DATABASE_PASSWORD: password
      TZ: Asia/Tokyo

    services:
      mariadb:
        image: mariadb:10.6.0
        env:
          MYSQL_DATABASE: ${{ env.PROJECT_DATABASE_DBNAME }}
          MYSQL_USER: ${{ env.PROJECT_DATABASE_USERNAME }}
          MYSQL_PASSWORD: ${{ env.PROJECT_DATABASE_PASSWORD }}
          MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
          TZ: Asia/Tokyo
        options: >-
          --health-cmd "mysqladmin ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 3306:3306
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      # Resolve gems dependencies
      - name: Set up Ruby ${{ matrix.ruby-version }}
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: ${{ matrix.ruby-version }}
          bundler-cache: true

      # Pre-test setting
      - name: Setup database
        run: bundle exec rake db:create db:schema:load

      # Executing lint
      - name: Run linters
        run: bundle exec rubocop

      # Executing test
      - name: Run tests
        run: bundle exec rspec

  slack-notify:
    runs-on: ubuntu-latest
    needs:
      - rails-test
    if: always()
    steps:
      # run this action to get workflow conclusion
      # You can get conclusion via env (env.WORKFLOW_CONCLUSION)
      - name: Receive job statuses
        uses: technote-space/workflow-conclusion-action@v2
      - name: Slack Notification
        uses: weseek/ghaction-slack-notification@master
        with:
          type: ${{ env.WORKFLOW_CONCLUSION }}
          job_name: '*Test*'
          channel: '#channel_name'
          isCompactMode: true
          url: ${{ secrets.SLACK_WEBHOOK_URL }}

各ブロックを説明していきます。

name には、任意の workflow の名前を設定します。 on は、その workflow をどのような契機で実行するのか指定するものです。 push を指定し、リポジトリにコードがプッシュされる度に workflow が実行するようにします。 branches-ignore: stable とすることで、 stable ブランチへのプッシュの場合は workflow が実行されないようにしています。

job に rails-test という名前を付け、その中でイメージの指定や、環境変数、実行するステップを定義していきます。 env はこの後に説明する service で使用するための環境変数を定義しておきます。

services にはテストの実行に必要なコンテナを定義します。 今回はテスト用のデータベースである mariadb を定義します。

steps には、前述の「CI で何を実施したいか」で考えた内容を設定していきます。

uses で、定義済みの action を利用できます。 uses: actions/checkout@v2 はソースコードをチェックアウトするために使用しています。 uses: ruby/setup-ruby@v1 で ruby のセットアップをしています。

準備ができたら、 Setup database, Run linters, Run tests の3つの name のステップを定義し、それぞれ「データベースの準備」「静的解析の実行」「テストの実行」を行います。 run では、任意のコマンドを実行できます。 run: bundle exec rake db:create db:schema:load と書くことで、テスト用の DB を初期化しています。

slack-notify は、 CI 完了時に slack へ通知するための設定を行っています。

workflow の編集が終わったら、 Start commit でコミットを作成し、保存します。今回は GitHub の workflow 編集ツールで編集を行いましたが、 任意の Git クライアントで /.github/workflows/*.yml をコミット&プッシュして wofkflow を作成しても大丈夫です。

リポジトリにプッシュを行うと自動的に workflow が実行されます。 Actions の workflow 一覧には、実行結果が表示されます。

一覧の何れかの結果を選択すると、このようになっています。 rails-test が実行され、最後に slack-notify を実行していることがわかります。

GitHub Actions は PullRequest とも連携しており、便利です。該当する PullRequest に All checks have passed のように workflow の実行結果が反映されます。

挫折しないテストの心得 開発ルール

テストツールの導入、CIの構築で、テストを実行できる環境は準備できました。ここからは気楽にテストを書いていく開発手法を紹介していきたいと思います。

何から書けばいいのか

実際にテストを書いていこうとした時、「何から書いていけばいいのか」悩みます。ざっとテストの種類を挙げると、下記のものがあります。

  • Model specs
  • Controller specs
  • Request specs
  • Feature specs
  • View specs
  • Helper specs
  • Mailer specs
  • Routing specs
  • Job specs
  • System specs

これらのテストを最初からすべて整備しようとすると、モチベーションを維持するのが難しそうです。そこで、最初は下記に絞って、小さく始めましょう。

  • Model specs
  • Controller specs
  • Request specs

Feature/System specs などは最初は書かず、手動でのテストを併用しましょう。

よく聞く TDDで開発したほうがいいのか

そもそも TDD とは何でしょうか。 TDD は、下記のサイクルを回して行う開発手法です。

  1. はじめにテストコードを書く
  2. テストがパスするコードを書く
    • 罪を犯した書き方をしてもよい
  3. リファクタリング

まず最初にテストコードを書きます(テストファースト)。 2番目の「テストがパスするコードを書く」では「罪を犯した書き方をしてもよい」とあるので、動けばいいコードを書いても問題ありません。その後のリファクタリングできれいにしていきます。

さて、 TDD (テストファースト) で書いたほうがいいかですが、テストツールの導入期は気楽さを維持するために書かなくてもよいと考えています。先に機能を実装し、同じ PR もしくは少し遅れてテストを書くようにしましょう。ただし、テストを後回しにする代わりに罪を犯した書き方は極力しないようにしましょう。

どの粒度まで書けばいいのか

すでにプロジェクトが走っているものに後からテストを導入した際、どこまでテストを書けばいいのか悩みます。『テスト駆動開発』の書籍から引用すると、「不安が退屈に変わるまで書く」とあります。

後からテストを導入した場合は、不安のある箇所からテストを書き始めるか、新しい実装部分からテストを書き始めましょう。それ以外の部分は対応の必要がないならばそのままにしておきます。

テスト駆動開発
Kent Beck 著/和田 卓人 訳
https://shop.ohmsha.co.jp/shopdetail/000000004967/

仕様と実装の整合性を楽にする スキーマファースト

さいごに、簡単にスキーマファーストについて紹介したいと思います。
Rails アプリケーションで、 API を組むことがあると思いますが、これのテストを書こうとした時に便利なものです。

OpenAPI

先に、 OpenAPI について説明します。

OpenAPI は Rails など API を実装する際の仕様をまとめるドキュメントです。

committee + OpenAPI

OpenAPI で仕様を定義しておくと、これを使って実装が仕様どおりに行われているかテストしたくなってきます。
テストを行うためには、 committee を利用します。 committee は、 assert_response_schema_confirm メソッドを提供し、これを利用して、 OpenAPI ドキュメントに従って、レスポンスの JSON スキーマと一致するかチェックできるようになります。

Gemfile に下記を追加し、 bundle install します。

group :development do
    gem 'committee-rails'
end
$ bundle install

下記は committee を利用したテストコードの例です。 assert_response_schema_confirm(200) で、OpenAPI と実装が合致しているかテストしています。

describe 'request spec' do
  include Committee::Rails::Test::Methods

  def committee_options
    @committee_options ||= {
      schema_path: Rails.root.join('schema', 'schema.json').to_s,
      query_hash_key: 'rack.request.query_hash',
      parse_response_by_content_type: false,
    }
  end

  describe 'GET /' do
    it 'conform json schema' do
      get '/'
      assert_response_schema_confirm(200)
    end
  end
end

まとめ

冒頭で気楽に書こうと言いましたが、テスト導入時は準備することが多く大変です。
挫折しないために、最初から頑張りすぎないようにします。

  • テストファーストで書かないでやってみる
  • 書ける分のテストを書いて、小さく始める
  • 手動テストとうまく併用する

上記ができたら、

  • 手動テストも自動化へ
  • TDD
  • スキーマファースト

等、段階的にテストツールを導入していきましょう。

テストが本当に楽になっていくのと、その恩恵を受けられるようになるのはここからです。

著者プロフィール

田村 貴幸

株式会社WESEEK / システムエンジニア

Webシステム開発会社を2社経験後、2019年8月にWESEEKに入社。
現在は、大手IXの業務自動化システムの機能開発やインフラの設計・構築に携わる。
趣味は、電子工作と釣り。

株式会社WESEEKについて

株式会社WESEEKは、システム開発のプロフェッショナル集団です。

【現在の主な事業】

  1. 通信大手企業の業務フロー自動化プロジェクト
  2. ソーシャルゲームの受託開発
  3. 自社発オープンソースプロダクト「GROWI」「GROWI.cloud」の開発

GROWI

GROWIは、Markdown記法でページを記述できるオープンソースのWikiシステムです。

GROWI.cloud

GROWI.cloudはOSSのGROWIを専門的知識がなくても簡単に運用・管理できる、法人・個人向けの商用サービスです。

大手SIer・ISPや中小企業、大学の研究室など様々な場所でご利用いただいております。

【主な特徴】

  • テキストも図表もどんどん書ける、強力な編集機能
  • チーム拡大に迅速に対応できる管理者向け機能を提供
  • 充実した機能・サポートでエンタープライズにも対応

【導入事例記事】
インターネットマルチフィード株式会社様
[https://growi.cloud/interviews/mfeed/?utm_source=connpass-top&utm_medium=web-site&utm_campaign=mf:embed:cite]

株式会社HIKKY(VR法人HIKKY)様
[https://growi.cloud/interviews/hikky:embed:cite]

WESEEK Tech Conference

WESEEK Tech Conferenceは、株式会社WESEEKが主催するエンジニア向けの勉強会です。

WESEEKに所属するエンジニアが様々なテーマで定期的に発表を行う予定です。

次回は、10/28(木) 19:00~20:00に開催予定です。
実際に WESEEK で使っている勤怠入力システム「tickrec」の構成を元に、Slackで勤怠入力ができるアプリケーションを1から構築するための方法をレクチャーします。

現在、connpassやTECH PLAYで参加受付中です。皆様のご参加をお待ちしております!
https://weseek.connpass.com/event/226591/
TECH PLAYはこちらから

一緒に働く仲間を募集しています

東京の高田馬場オフィス、大分にある別府サテライトオフィスにてエンジニアを募集しております。

中途採用だけではなく、インターンシップも積極的に受け入れています!

詳しい募集要項は、弊社HPの採用ページからご確認ください。