チーム開発におけるTDDのすゝめ
前提
- コードレビューの文化がないチームのことは考えてません。本記事では「当然あるべきもの」として扱います。
- ふと思いついたものなので、弊開発チームではまだ採用できてません。。
- 後日「やってみた」が投稿できるといいなあ。
要約
チーム開発におけるコードレビューのプロセスに 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 機能をうまく使ってください。
- RSpec:
-
pending
,skip
,xdescribe
, ... - https://relishapp.com/rspec/rspec-core/v/3-10/docs/pending-and-skipped-examples
-
- Jest:
-
describe.skip
,test.skip
,xdescribe
, ... - https://jestjs.io/docs/ja/api#describeskipname-fn
-
- etc.
プロダクションコードを書こう
テストコードが通るように、プロダクションコードを書いていきましょう。
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