既存プロダクトに

RSpec を

導入していく

  • rry と申します
    • 平成.rb Organizer
    • Vue.js 日本ユーザーグループ
    • Vue Vixens
  • フロントエンドも好きな Rails エンジニアです 🐣

〜 宣伝 〜

Vue.js は良いものだ

  • Vue Vixens という
    女性向けイベントやっています
    • Rails Girls みたいな感じ 😘
  • 12/19(木)Vue Vixens #2 🎄🎅
    •  
  • Nuxt.js アドベントカレンダー

9月〜

 

とあるプロダクトの

保守運用を任された

※ この物語はお仕事でのお話です

モノリシックな Rails

API がいくつかある

今年フロントエンドを Nuxt.js にリニューアル

管理機能の画面はいまだモノリシックな Rails

※ この物語はお仕事でのお話です

RSpec のカバレッジ

5%

引き継ぎ当時...

RSpec をほぼ実装したことがない

よちよちメンバー 4人で保守運用

コードの中を確認...

  • RuboCop ... 入っていない

    • 警告が出すぎて入れられない

  • バックオフィス向け管理機能

    • 仕様を誰も把握していない

 

  • 長すぎるメソッド...

  • 改修しづらいコード...

  • 使われていない機能...

🍝

逃げちゃダメだ!

既存プロダクトに

RSpec を

導入していく戦い

タイトル・改

これからどうする?

我々がとったアクション

コーチを召喚

ねらいは、メンバーの
スキルアップとテストの充足、
技術負債 🍝 の解消

 

どことは言いませんが、界隈で有名な
ESM さんにお願いしました。

我々がとったアクション

計画づくり

「テストがないとデグレーションしてしまう!」

 

「今は機能追加よりもテストやリファクタリングを優先させる」

 

CEO や他部署に合意してもらい、
技術負債解消ターンに入った

我々がとったアクション

実行

←今ココ

第 1 期:Unit test 系の Spec を充足させる

第 2 期: Integration test 系の Spec を充足させる

    ↑今ココ

 

第 3 期:リファクタリング 💪

テストの考え方について

テストの考え方について

基本的な自動テストの考え方や RSpec の書き方は
​これらを読めば分かる

テストの考え方について

また、「使えるRSpec入門」シリーズ

なども大変お世話になっている。

これらを読んで基本が分かった
ところで、次は実践編 🚀

1. テストピラミッド、
    上から降るか下から登るか

                                     の詳しい説明は、

「初めての自動テスト」でも

    紹介されています。

  • Unit テストは速く実行できる
    • シンプルな Unit テストでパターンを網羅
      • model, decorator, uploader などの Spec
  • Servise(結合)テストは Request Spec でまとめる
    • Controller Spec などは省略して書かない
  • UI(E2E)テストは System Spec で書いていく
    • 実行速度が遅いため、網羅的に書くには不向き

1. テストピラミッド、
    上から降るか下から登るか

我々は model や decorator などの

Unit テストから書いていくことを
「計画づくり」のときに決めた。

  • メンバーが RSpec に慣れている場合
    • バグが起こったときに検知しやすいよう、
      E2E などの外側のテストから書いていく
  • メンバーが RSpec に慣れていない場合
    • 難易度が低くテストに慣れることができる
      Model などの Unit テストから書いていく

2. テストを実装する前に、

     describe、context などを使って、

     まずはペンディングテストを書いていく

「何をテストしたいのか」をペンディングで書いていく

ことにより、頭が整理されるため

describe Article, type: :model do
  # テスト対象(メソッド名など)
  describe '#published' do
    # 状態(〜の場合)
    context '非公開記事がある場合' do
      # 期待する出力(〜のこと)
      it '公開記事のみ取得できること'
    end
  end
end
describe Article, type: :model do
  describe '#published' do
    context '非公開記事がある場合' do
      before do
        create(:article, name: '公開記事', published: true)
        create(:article, name: '非公開記事', published: false)
      end

        subject(:article_published) { Article.published }

        # 期待する出力(〜のこと)
        it '公開記事のみ取得できること' do
          expect(article_published).map(&:published)).to match_array([true])
        end
    end
  end
end
  • 「設計をテストで表現できる」のが RSpec の良さ
  • 「ふるまい」を先にペンディングで書いていく(BDD)
    • 機能実装するときも同じようにペンディングテスト
      から書いていくと🙆

3. あくまで「ふるまい」をテストすること

  • マジックナンバーや、中身が分かりにくい
    (または変動する恐れのある)値で検証するのは ❌ 👮
describe Article, type: :model do
  describe '#some_data' do
  	context '何らかのデータがある場合' do
      let(:bad_article) { create(:article, title: Foo.title, description: Foo.description, tag_type: 1) }
      let(:good_article) { create(:article, title: 'タイトル', description: 'ディスクリプション', tag_type: 'Ruby') }

      subject(:bad_some_data) { bad_article.some_data }
      subject(:good_some_data) { good_article.some_data }

      it 'データの配列が返ってくること' do
        # 分かりにくい例 ×
        expect(bad_some_data).to eq [article.title, article.description, 1]

        # 分かりやすい例 ○
        expect(good_some_data).to eq %w(タイトル ディスクリプション Ruby)
      end
    end
  end
end

我々は全知全能だからコードの中身を知っているだけ。

メソッドの中身がどうなっていようと、期待する値を受け取れればそれで良い。

4. 「テストが書けない!」

  • ひとつのメソッドに仕事をさせすぎている場合
  • 中のロジックについて、テストを書くのが
    あまりにも大変になる...
  • その場合は、最低限テストで担保したい部分
    (書ける範囲)を書き、その後にリファクタリングする
    • テストなしでリファクタするのは ❌
  • 無理のある範囲をわざわざテストしなくても良い
    • もし手動の動作確認で事足りるなら、最低限テストを書いてリファクタリングした後に、きちんとしたテストを書けば 👌

5. private メソッドのテスト?

  • リファクタリングする前のコードにテストを書こうと
    したら、プライベートメソッドのテストを書くはめに
  • 書けなくもない... けど... 😑



     
  • どうしても private メソッドをテストしたいときは、
    設計がおかしいかもしれない
    • send でテストを書いた後で良いので、リファクタリングしていこう
    • クラスに切り出すと幸せになれるかもしれない
let(:creator) { Admin::DataCreator.new() }

subject(:contract) {
  # send を使って private メソッドを呼び出して検証することはできる...
  creator.send(:foo, 1, 2, 3)
}
  • 参考になった(気持ちがよく分かった)記事






     
    • リファクタリングする前提で、private メソッドの
      テストを書くのは致し方なしだと思う
      • いくつか(ガッツリ)private メソッドのテストがマージされている
      • もちろんリファクタ対象になっている

RSpec の

便利な書きかた Tips

1. これが便利 factorybot

  • create で関連するモデルも作れる
  • 最低限のバリデーションが通る factory を作ると、
    応用が利いて便利
# Table name: users
# ...
# id                     :bigint           not null, primary key
# name             :string(255)
# coach_id          :bigint
# ...

describe User, type: :model do
  describe '.coachs' do
    before do
      create(:user, name: 'テスト A さん', coach_id: nil)
      # 関連するモデルを作れる
      create(:user, name: 'テスト B さん', coach: create(:coach))
    end

    subject(:user_coachs) { User.coachs }

    context 'coach を持っているデータが存在する場合' do
      it 'coach を持っている user が取得できること' do
        expect(user_coachs.count).to eq 1
        expect(user_coachs.first.name).to eq 'テスト B さん'
      end
    end
  end
end

2. receive_message_chain に引数を
    複数指定したい

describe 'Api::ContactsController', type: :request do
  describe '.received_mail' do
    subject(:request) {
      post '/api/contacts', params: { contact: { name: '太郎', email: 'taro@co.jp', message: 'メッセージです' } }
    }

    # ...
    # リクエストが成功することなどのテスト
    # ...
    # Api::Mailer.received_mail(Contact.new).deliver
    
    before do
      allow(Api::Mailer).to receive_message_chain(:received_mail, :deliver)
        .with(instance_of(Contact)).with(no_args)
    end

    it 'mailer が呼び出されていること' do
      request
        
        expect(Api::Mailer).to have_received(:received_mail).once
      end
    end
    
  • with を複数つなげることができる
  • anything や no_args など引数マッチャを指定すると
    より明示的にすることができる

3. 標準出力のテスト

describe Foo, type: :service do
  describe Foo::Generator do
    describe '.call' do
      subject(:foo) { Foo::Generator.new }

      it 'メッセージが標準出力されること' do
        expect { foo.call }.to output("ファイルを出力しました。\n").to_stdout
        # .to_stderr で標準エラー出力も検証できる
        # expect { foo.call }.to output("エラーが発生しました。\n").to_stderr
        expect(File.exist?(file)).to be true
        end
    end
  end
emd
  • RSpec にある output().to_stdout が便利
    • output マッチャ以外にも便利なマッチャが沢山ある
  • テストを書くときは RSpec の公式ドキュメント
    Relish を読む

まとめ

まとめ

  • RSpec の良いところ
    • Spec がUnit テスト系から Integration テスト系まで手厚く用意されており、過不足なくテストが書けること
  • DSL で読みやすい
    • 読みやすいということは、ドキュメントとして仕様を把握しやすい
  • モックが簡単に書けたり、気の利いたマッチャが多く、
    テストが書きやすい
    • 書きやすいということは、機能の修正やリファクタリングがしやすい

つまり...?

変化に強くなれる💪

  • Rails や Gem のバージョンアップ・導入
  • 仕様変更や機能追加がしやすくなる
  • テストがあることによりこれらに
    安心感が持てる

今のプロダクトがリファクタリング期に
入るのは、あともう少しだけかかりそう
ですが...

少しずつ開発に

安心感が持てるようになってきた 🎉

  • System Spec とあと少しのテストを除いてテストが充足してきた

    • 機能改修にも着手しつつある

  • コーチに
    圧倒的感謝 🙏

ちなみに...

RSpec のカバレッジ

5%→15%

俺たちの戦いは...
まだ始まったばかりだ...!

ところで...

突然ですが...

 

rry、1月から
STORES.jp に転職します。

  • 12月いっぱいまで
    • フロントエンド(Nuxt.js)側の引き継ぎ
  • メンバーひとりひとりが RSpec の実装に慣れてきた
    • チームの雰囲気と開発速度が安定してきた
  • 新しいことにチャレンジしたいと思った 💪

ご静聴

ありがとうございました!

 

詳しい話や質問は懇親会
でお声がけください 🙋

 

引き続き平成 Ruby 会議を楽しみましょう!

逃げちゃダメだ!既存プロダクトに RSpec を導入していく戦い

By rry

逃げちゃダメだ!既存プロダクトに RSpec を導入していく戦い

平成 Ruby 会議 01 での発表資料です。

  • 3,167