こんにちは。hacomono インテグレーション部でエンジニアをやっています htz (ひつじ) です。
引き続きエンジニアリングマネージャとして有りながらも、開発や改善活動にも積極的に取り組んでおり、今回はその中でも RSpec に焦点を当ててご紹介させていただきます。
はじめに
以前の記事で hacomono の Rails プロダクトにおける改善の取り組みについてご紹介させていただきましたが、主にテストを中心の改善として
- テストに関するルールや方針の整備
- 単体テストの充実
- CI 時間の短縮
という 3 本柱で取り組んでいます。その中でも今回は「テストに関するルールや方針の整備」について紹介します。
RSpec のルール整備
RSpec は Ruby のテストフレームワークで、Ruby on Rails でも広く使われており説明は不要かと思いますが、書き方に関しては自由度が高く、各社 (もっというと各プロジェクトごと) でも書き方がまちまちになることが多いです。
hacomono においてはプロダクトが大きくなり関わる人数も増えてきたことも有り、機能やチームごとでも書き方が異なり、テストの可読性や保守性が低下していることが課題として挙げられています。
ただ、この規模で余りにも細か過ぎるルールを定めてしまうと、逆に開発効率が落ちてしまうことも考えられます。また、様々な経験や意見を持つエンジニアが集まっているため、あまりにも細かくルールを定めてしまうと、それぞれの意見が反映されず、逆にエンジニアのモチベーションを下げてしまうことも考えられます。
そこで hacomono ではできる限り MUST なルールとはしないものの、推奨される書き方を中心として独自の「RSpec コーディングガイド」を作成しました。
RSpec を書く上で大事にしたいこと
- テストというよりも見せる設計 (spec) で有ることを意識する
- 不必要なやり過ぎた DRY は行わず、読み手が読みやすくする
- できる限り目線が上下しないような書き方にする
- テストを追加した時だけでなく、後からテストコードを修正する時にもわかりやすく誰でもメンテできるようにする
まず、これらのポイントはエンジニア全体の共通意識として持って頂くところから始めています。
主にはこのポイントを常に意識することで、以後説明を行うポイントについても自然と意識が向いていくようになると考え、コーディングガイドにおいてもこのポイントを最初に記載しています。
基本的な rspec のフォーマット
次に、RSpec の基本的なフォーマットについてです。RSpec は Ruby で記述するため、どのような書き方でも問題ないのですが、ベースの書き方が統一されていることで、他のエンジニアが見た時にもわかりやすくなります。
そこで新規で作成する RSpec ファイルについては以下のフォーマットを基本としてとすることとしています。
RSpec.describe Hoge do describe '#method_name' do context 'xxxがyyyの場合' do it '成功が返る' do end end context 'xxxがzzzの場合' do it '失敗が返る' do end end end end
ポイントとしては
- トップレベルの describe はテストの対象クラス (やモジュール) であること
- トップレベルの describe 直下は describe であり、基本的にテスト対象のメソッド名や機能単位であること (例
#method_name
.method_name
validation
など)- また、リクエストテストでは
POST /hoge
のような形を推奨
- また、リクエストテストでは
- context については後述
といったところです。これにより、初めて見るエンジニアでもどのようなテストを書いているのかがわかりやすくなります。
context のメッセージは条件であることをわかりやすく伝える
context の書き方として、
- 日本語で記述する場合は
....の場合
や....の時
というメッセージ - 英語で記述する場合は
when ...
やwith ...
というメッセージ
で書くことを統一するのが望ましいと考えています。 (こちらについては rubocop-rspec を利用して記述の強制化することも可能です)
逆に、これらの書き方にならない場合は describe を書こうとしている可能性があるため、ある程度の縛りのルールとすることで読み手にも書き手にもメリットが有ると思っています。
github.com
let をただの変数にしない (多用しすぎない)
RSpec には let
というメソッドがあり、テスト内で使いたい値を定義することができます。ただ、これを多用しすぎるとテストの可読性が下がることがあります。
RSpecの let
を適切に使うためには、テストの読み手が注目すべきポイントを明確にすることが重要です。let
を多用しすぎると、どの変数が対象の RSpec において重要なポイントで、どの変数が重要でないのかが分かりにくくなります。
特に、let
はこの context の条件と連動することが望ましいと考えており aaaがbbbでcccがdddの場合
という context だと
context 'aaaがbbbでcccがdddの場合' do let(:aaa) { bbb } let(:ccc) { ddd } end
のような書き方を推奨しています。もちろん context の条件が全て let
対応する訳では無いとは思いますが、できる限り合わせることで読みやすさが数段上がると考えています。
ワンライナー it (example) は控えめに
RSpec には it
というメソッドがあり、テストの内容を記述することができます。この it
はワンライナーで書くこともできます。ただ、これを多用しすぎるとテストの内容が分かりにくくなることがあります。
そのため、ワンライナーの it
は控えめにし、どのような結果を期待しているのかをメッセージで明確にすることが重要と考えています。
また、 RSpec は it
単位でのテストが実行されるため、ワンライナーで書いた場合にテストパフォーマンスが低下することも考えられます。
こちらについては賛否両論あるかとは思いますが、hacomono ではこれからも成長し続けるプロダクトの CI 時間を考えると、ワンライナーの it
は控えめにすることを推奨しています。
context はネストしない
こちらについても非常に賛否両論がある話だと思っています。
hacomono では 以前の記事 でも紹介をしたように、リクエストテストが全体の 8 割程度を占めており、大部分のロジックをこのリクエストテストでカバーしているという背景があります。
そのため、リクエストテスト側で多くのエッジケースや条件を網羅的に記述するために、context を多段ネストしたような記述が少なくないです。
どのようなプログラミング言語においても、 if 文がネストしすぎることが嫌われるように、RSpec においても context がネストしすぎることは避けるべきだと考えています。
そもそも context がネストする原因としては
- テストの対象以外の部分の仕様を記述してしまっている
- テストコード自体でもロジックが複雑になってしまっている
- 複雑な条件や状態が絡んでいる
のような事が挙げられます。
これらを避けるためには
テストの対象以外の部分の仕様を記述してしまっている
依存するクラスの単体テストを充実させ、テスト対象のクラスのみの仕様 (記述されている実装内容) を RSpec に記述する。
テストコード自体でもロジックが複雑になってしまっている
ロジック自体のリファクタリングを検討し、場合によってはメソッドの分割や別クラスへの責務の移譲を検討する。
複雑な条件が絡んでいる
if 条件のネストが深くなくとも、複雑な条件や状態によって違った動きを期待する場合にはテーブルテストを導入することを勧めています。
テーブルテストは
[ { count: 0, status: :active, expect: false }, { count: 0, status: :inactive, expect: false }, { count: 10, status: :active, expect: true }, { count: 10, status: :inactive, expect: false }, ].each do |data| context "カウントが#{data[:count]}でステータスが#{data[:status]}の場合" do let(:count) { data[:count] } let(:status) { data[:status] } it '#{data[:expect]}が返ること' do expect(hoge).eq (data[:expect]) end end end
のように書くことが出来、複雑な組み合わせ条件や状態によって違った動きを期待する場合には非常に有効です。 (hacomono ではより DSL っぽく書く事ができる rspec-parameterized という Gem を利用し始めました。) github.com
FactoryBot をできる限り使用する
hacomono の RSpec では様々な方法でテストデータが作成されており😅
- FactoryBot の利用
- Active Record (hacomono では Entity) のインスタンスを直接利用
- Repository 層を利用
- Usecase 層を利用
などが混在しており、テストデータの作成方法がバラバラになってしまっています。
リクエストテストで記述されている RSpec が多く複雑な前提条件やレコードが必要になってくるという背景もありますが、
- 不必要なビジネスロジックの呼び出しがテストデータの作成のためだけに動いてしまっている
- テストデータの作成方法が違う事が原因で、ユニーク項目の重複による不安定なテスト失敗が発生してしまっている
- DB に保存する必要のないテストデータを大量に作成してしまっている
などの課題があります。
FactoryBot を利用することで
- テストデータの作成方法を統一出来る
- レコードの作成に関しては FactoryBot で完結する (させる)
build
やbuild_stubbed
などのメソッドを利用することで、DB に保存する必要のないテストデータを作成することができる
を実現することができます。
やり過ぎた DRY は行わない
こちらについても非常に賛否両論がある話なので hacomono においては推奨という形にしています。
Ruby プログラミングは DRY (Don't Repeat Yourself) が重要視される言語ではありますが、RSpec においてはやり過ぎた DRY は行わない方が良いと考えています。
「できる限り目線が上下しないような書き方にする」だけでなく別ファイルにまで飛ばされる可能性もあるので、やり過ぎた DRY は読み手にストレスを与える可能性があると考えています。
RSpec では shared_examples, shared_context というような共通のテストや条件をまとめるメソッドも存在していますが、やり過ぎた DRY になる場合も多く、これらを利用する際には注意が必要としています。
まとめ
いかがでしたでしょうか。hacomono 独自のアーキテクチャや自社の課題感という部分も踏まえた RSpec のルールと言う形にはなっているため、他社でそのまま適用することが難しい部分もあるかと思います。
ただ、これらのルールを意識することで、テストコードの可読性や保守性が向上し、エンジニア全体の共通意識として持って頂くことで、より良いプロダクト開発に繋がると考えていますので、参考にしていただければ幸いです。
株式会社hacomonoでは一緒に働く仲間を募集しています。
エンジニア採用サイトや採用ウィッシュリストもぜひご覧ください!