🤖

チーム開発におけるTDDのすゝめ

2021/03/06に公開

前提

  • コードレビューの文化がないチームのことは考えてません。本記事では「当然あるべきもの」として扱います。
  • ふと思いついたものなので、弊開発チームではまだ採用できてません。。
    • 後日「やってみた」が投稿できるといいなあ。

要約

チーム開発におけるコードレビューのプロセスに TDD (Test-Driven Development) を組み込むことで、
「より手戻りを減らせるのでは?」という提案です。

目的

コードレビューを通過するまでの時間を短縮したい。

手段

  • コード仕様のレビュー
  • 実装方法のレビュー

これらを2段階以上に分割することで、議論の長期化を避ける。

テストを書こう

TDD をやるんだから、まずはテストを書きましょう!

…はい、思考停止ですね。目的と手段がごっちゃになっちゃってます。
まずこの時点で行うことは、

  • プロダクションコードの『外部から見た振る舞い=仕様 (Specification)』を確定すること

です。したがって、

  • 完璧なテストコードは不要
    • 外部ライブラリの mock/stub などは現時点では必須ではない
  • 必要なもの
    • (どんなコンテキストで)
    • どんな入力(引数)を与えたら
    • どんな出力(戻り値)が得られるべきなのか
    • (どんな副作用があるのか)

をテストコードとして表現していきましょう。

user_repository_spec.rb

RSpec.describe 'UserRepository' do
  let(:repository) { UserRepository.new }

  describe 'UserRepository#find' do
    subject { repository.find(id) }

    let(:id) { "user_id" }

    context 'when user with id is present' do
      before do
        # TODO
      end

      it 'returns user with id' do
        expect(subject).to be_instance_of User
        expect(subject.id).to eq id
      end
    end

    context 'when user with id is not present' do
      before do
        # TODO
      end

      it 'raises RecordNotFound error' do
        expect { subject }.to raise_error UserRepository::RecordNotFound
      end
    end
  end
end

Pull Request を出そう

テストコードを書き終えた時点で、Pull Request を出してレビューしてもらいましょう。
この時点でも、

  • id に対応するユーザーが存在しないときは例外を発行するべき? nil を返すべき?

など、コード仕様に関する議論は十分にできるはずです。
そして、プロダクションコードをまだ書いていないので、実装方法についてのコメントはしようがありません。
自然と議論の内容を「コード仕様がどうあるべきか」に集中させることができます。

Pull Request を修正しよう

レビュアーと「修正が必要だ」と合意した箇所を修正しましょう。
プロダクションコードをまだ書いていないので、テストコードを修正するだけで済みます。楽チンですね!

補足:

テストコードの修正が完了した時点で一旦 PR をマージし、次以降の step は別の PR で行う、というのも良いと思います。
その場合はテストライブラリの skip 機能をうまく使ってください。

プロダクションコードを書こう

テストコードが通るように、プロダクションコードを書いていきましょう。

user_repository.rb

class UserRepository
  class Error < ::StandardError; end
  class RecordNotFound < Error; end

  def initialize(http_client = Faraday.new(...))
    @http_client = http_client
  end

  def find(id)
    response = @http_client.get("/users/#{id}")
    user_data = JSON.parse(response.body)

    User.new(user_data)
  rescue
    raise RecordNotFound
  end
end

user_repository_spec.rb

 RSpec.describe 'UserRepository' do
-  let(:repository) { UserRepository.new }
+  let(:repository) { UserRepository.new(http_client_double) }
+  let(:http_client_double) { instance_double(Faraday) }
 
   describe 'UserRepository#find' do
     subject { repository.find(id) }
 
     let(:id) { "user_id" }
 
     context 'when user with id is present' do
       before do
-        # TODO
+        allow(http_client_double).to receive(:get).with(id) do |uid|
+          instance_double(Faraday::Response, body: JSON.generate({ id: uid }))
+        end
       end
 
       it 'returns user with id' do
         expect(subject).to be_instance_of User
         expect(subject.id).to eq id
       end
     end
 
     context 'when user with id is not present' do
       before do
-        # TODO
+        allow(http_client_double).to receive(:get).with(id).and_raise(Faraday::ResourceNotFound)
       end
 
       it 'raises RecordNotFound error' do
         expect { subject }.to raise_error UserRepository::RecordNotFound
       end
     end
   end
 end

リファクタリングをしよう

書いたプロダクションコードをレビューに出し、再度レビュアーのコメントを貰いましょう。
テストコードを書いた時点で合意した内容について再度議論が加熱することはほぼなく、
今度はコーディングのテクニックに集中してコメントを貰えるはずです。

まとめ

  • チーム開発におけるコードレビューって、必要だとは思うけど時間かかるよね
    • TDD を応用したら、レビューの観点が分散しなくなって、議論の長期化が避けられるのでは?

Discussion