Edited at

RSpecで何をテストしたらいいのか決めてない時、とりあえずわたしが書いてるもの。

More than 1 year has passed since last update.


はじめに


やりたいこと:カッコよくグリーンを出したい!

「テストは大事!」とは言うものの、どんなRSpecを書いたらいいのかわからない...。

そんな状況が、Railsのお仕事に本格的に関わるようになってから、続いておりました。

そもそも自動テストには憧れがありますし、specを実行して、ターミナルに緑の文字を出したいのはやまやまです。でも、何を書けばいいのかわからない。

そんな時の、とりあえずの取りかかりの例を載せてみます。

※ この記事の例について:

下記の例は、実際はテストとして無理に書く必要はないものだったりします。

自明なものを書きすぎると、たとえば全てのテストを通すのにも非常に時間がかかってしまいますので、このあたりは、rspecを動かす場合の確認用、1例として見ていただけると幸いです。

※ これからRspecを始めたい方 / 初心者の方へ (20180523 追記)

@jnchito さんが、「どんな時に、どういう点に着目してspecを書けばいいか」について、とても素敵な記事を書いてくださっているので、是非そちらを見ていただければと思います!


事前の注意 / 前提条件


  • Railsの簡単なアプリケーションの作り方がわかっていること

  • RSpecの動かし方が簡単にわかっていること



  • RSpecの言い回し、良く使う単語(let, subject, context...) をなんとなくでいいのでわかっていること

Rails5で標準で使えるようになっている、minitestについては触れません。


まず空っぽのspec / pendingで実行

usersというテーブルに対応する、Userというクラスを用意したとします。

合わせて、booksテーブルに対応するBook、teamsテーブルに対応するTeamというクラスを用意します。

class User < ApplicationRecord

# Bookをいっぱい持ってるリレーションがあると想定
# Teamに所属すると仮定
has_many: books
belongs_to: team
end

class Book < ApplicationRecord
end

class Team < ApplicationRecord
# Teamにはuserがたくさんいる
has_many: users
end

Userに対応するspecは、user_spec.rb になります。

自動生成だと、こんな感じの空っぽのテストが作られるかもしれません。

#

# bundle exec rspec spec/models/user_spec.rb という感じで実行
#
require 'rails_helper'

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

実行すると、こんな結果が返りますね。

All examples were filtered out; ignoring {:focus=>true}

*

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

1) User add some examples to (or delete) /xxxxx/spec/models/user_spec.rb
# Not yet implemented
# ./spec/models/user_spec.rb:4

Finished in 0.00118 seconds (files took 11.46 seconds to load)
1 example, 0 failures, 1 pending

pending "...." はまだ何も実装していないので、メッセージのみ出て終了です。

本来はこの状態ではなく、何かしらのテストを記載していくのが望ましいのですね。

でも、標準的にActiveRecordの機能を使ってデータが取れれば問題無いので、単純にUserクラスには、特に自分で実装したメソッドがまだ無い状態。

じゃあ、テストするものが無いじゃない...?


接続をテストしてみよう

ActiveRecordを使う場合は、データベースに接続することになります。

これは当たり前かもしれませんが、せっかくなので本当に接続しているのかテストをつけてみましょう。

テストの前に、rails consoleでconsoleを起動してみましょう。

接続情報はこんな具合に確認できます。

Loading test environment

Switch to inspect mode.
>> User.connection_config
{:adapter=>"mysql2", :encoding=>"utf8", :username=>"db_user", :port=>3306, :database=>"myapp_test", :password=>"xxxxx", :host=>"127.0.0.1"}

RAILS_ENV=testで起動の場合は、database.yml に設定した test用のDB接続設定が返ります。

ということで、”myapp_test” というDBへ接続することが期待され、その通りになっていればOKです。

実際のテストはこのような感じです。



  • RSpec.describe の後に定義されているクラスが、spec中のdescribed_classに相当します


    • この例だと、described_class = User となります




  • describe '' で、何をテストするか明示します


    • この例だと、connection_configというクラスメソッドがテスト対象です

    • クラスに対応するデータベース接続ですね



1, 2の2通りの書き方を添えてみました。

(私の職場では、クラスメソッドのテストの場合は、CSSのセレクタのように ".メソッド名"としています。

インスタンスメソッドの場合は、同じように "#メソッド名”としています)

RSpec.describe User, type: :model do

# 1. 基本の書き方
describe '.connection_config' do
it '指定のDBに接続していること' do

# expect(処理内容).to 期待する結果 という形で記述します
expect(described_class.connection_config[:database]).to match(/myapp_test/)
expect(described_class.connection_config[:database]).not_to match(/myapp_production/)
end
end

# 2. テスト対象を "subject" として明確にした書き方
describe '.connection_config' do
# subjectという変数に当てはめる場合は、expect(処理内容).to は is_expected.to に置き換え可能です
subject { described_class.connection_config[:database] }

it '指定のDBに接続していること' do
is_expected.to match(/myapp_test/)
is_expected.not_to match(/myapp_production/)
end
end
end

DBの接続情報も無理にチェックする必要は無いと思います。

わたしの場合ですが、メインのDBとは別のDBに接続するクラスを作ったことがあったので、そうしたクラスについては、はじめのうち、敢えてDBの接続情報をチェックするテストを付けていたりしました。


関連(Association) をテストしてみよう

Userクラスには、has_many: booksbelongs_to: teamという関連付けが定義されています。

当たり前だと思うかもしれませんが、この関連付けをテストするspecを追加してみます。



  • RSpec.describe の後に定義されているクラスが、spec中のdescribed_class に相当します


    • この例だと、described_class = User となります




  • describe '' で、何をテストするか明示します


    • この例だと、「Association」(関連付け)ですね




  • let(:変数名) { 定義内容 } は、変数名 = 定義内容 と同じです


    • letを使うと、実際にその変数が呼び出されるまでは割り当てが発生しません




RSpec.describe User, type: :model do

# 関連付けのテスト
describe 'Association' do
let(:association) do
# reflect_on_associationで対象のクラスと引数で指定するクラスの
# 関連を返します
described_class.reflect_on_association(target)
end

# booksとの関連付けをチェックしたい場合
context 'books' do
# targetは :books に設定
let(:target) { :books }

    # macro メソッドで関連づけを返します
it { expect(association.macro).to eq :has_many }

# class_name で関連づいたクラス名を返します
it { expect(association.class_name).to eq 'Book' }
end

# teamとの関連付けをチェックしたい場合
context 'team' do
let(:target) { :team }
it { expect(association.macro).to eq :belongs_to }
it { expect(association.class_name).to eq 'Team' }
end
end
end


(まだ実装してないけど)メソッドがあるかテストしてみよう

ApplicationRecordを継承しない(DBにひも付かない)クラスを作った場合。

仮実装中でまだちゃんとした値を返せないんだけど、メソッドを持っていることだけテストしたい...。

そういう場合は、rspecでは、respond_to というマッチャ(チェック用のメソッド)を使います。

# テスト対象のクラス

class Hoge
# インスタンスメソッド (hoge = Hoge.new してから hoge.say_hoge)
def say_hoge
# 何も返さない
# return "hoge!" # 実装した場合
end
end

# 対応するspec

RSpec.describe Hoge, type: :model do
# Hogeのインスタンスがテスト対象
describe 'Hoge instance'
subject { described_class.new }
expect(subject).to respond_to(:say_hoge)
end

# 実装後、実際のsay_hogeのテスト(返り値をチェック)
# describe '#say_hoge'
# subject { described_class.new.say_hoge }
# is_expected.to eq 'hoge!'
# end
end



  • respond_to(:say_hoge) のところでは、対象のインスタンスに対して、以下の処理を行います


    • subject.respond_to?(:foo)


    • respond_to?(:メソッド名)は、そのオブジェクトがメソッドを持っているかどうかをtrue/falseで返します



Ref. https://relishapp.com/rspec-pritesh/rspec-expectations/docs/built-in-matchers/respond-to-matcher


ささっとですが、まとめ

以上、少しですが、とりあえず自動テストの最初の一歩として私が書いていったりするのは、このようなテストです。

特に、メソッドの呼び出しのテストだけでも少しずつ書いていくと、


  • あれ?これ実装してない?

  • 引数の渡し方があってない?

  • プライベートで定義しちゃってる?

といったことがわかるようになります。

また、テストの書き方については、オープンソースで公開されているアプリケーションのspec/ 以下を眺めてみると、とても参考になります。

ぜひ、機会があれば覗いてみてくださいね。

また、「ここが違う」「こうした方がいいよ」といったご意見やご指摘いただけると幸いです。


追記:20180520



  • @jnchito さんから編集リクエストをいただきました。ありがとうございます!

  • コメントも踏まえて注意書き、補足を追記いたしました。