• トップ
  • ブログ一覧
  • テストしやすいRailsコード
  • テストしやすいRailsコード

    はじめに

    Railsで開発を進めていてると、最初は快適だったのに機能が増えるに連れてテストが書きづらくなり、修正が怖くなるようなことはないでしょうか?
    この記事では、「テストしやすいRailsコード」とは何かを考えつつ、テストファーストの視点から設計を見直すためのヒントを紹介します。

    なぜ「テストしやすさ」が重要なのか?

    Railsは開発スピードに強みがありますが、適切な設計をしないと、テストが困難な肥大化したコードが生まれやすいという弱点もあります。
    一方でテストファーストという視点を持つと、以下のようなメリットがあります。

    • 自然と責務が明確なコードになる
    • フレームワーク依存を避けやすくなる
    • コードが疎結合で再利用しやすくなる

    テストしづらいコードの例

    1def create
    2  @user = User.new(user_params)
    3  if @user.save
    4    NotifierMailer.welcom(@user).deliver_now
    5    redirect_to dashboard_path
    6    
    7  else
    8    render :new
    9  end
    10end

    このコードでは以下の問題があります

    • DB保存とメール送信という、複数の責務が1つのメソッドに詰め込まれています。
    • NotifierMailerのような外部依存が直接呼ばれている
    • 単体テストをしようとしても、コントローラーのテストでしか確認できません

    改善する

    1. コントローラの責務を限定する

    テストしやすいコードとするには、「コントローラを薄く保つ」ことです。
    ビジネスロジックはServiceクラスなどに分離させることでコントローラを薄くすることができます。

    1def create
    2  result = RegisterUserService.call(user_params)
    3
    4  if result.success?
    5    redirect_to dashboard_path
    6  else
    7    @user = result.user
    8    render :new
    9  end
    10end

    2. 副作用のある処理を注入可能にする

    外部サービスの呼び出しは、依存性注入(DI)を活用すると、テストしやすくなります。
    以下のように、キーワード引数にNotifierMailerを渡すことで、テスト時にNotiferMailerをスタブやモックに置き換えやすく、クラスの中で何を使っているのかが明示的になります。
    スタブやモックに置き換えることで実際にメールが送られることもなく、結果的に副作用のないテストが可能になります

    1class  RegisterUserService
    2  def self.call(params, mailer: NotifierMailer)
    3    user = User.new(params)
    4    
    5    if user.save
    6      mailer.welcome(user).deliver_later
    7      OpenStruct.new(success?: true)
    8    else
    9      OpenStruct.new(success?: false, user: user)
    10    end
    11  end
    12end

    テストコード例

    1class DummyMailer
    2  def self.welcome(user)
    3    OpenStruct.new(deliver_now: true)
    4  end
    5end
    6
    7it 'メールを送信する' do
    8  expect {
    9    RegisterUserService.call({ name: "test", email: "test@example.com" }, mailer: DummyMailer)
    10  }.not_to raise_error
    11end

    以上のように外部依存がある処理はDIを検討するべきです

    DIを検討すべき処理

    • メール送信
    • API通信
    • バッチ処理
    • ログ出力
    • 時間

    3. メソッドを小さく保ち、返り値を明確に

    テストファーストで考えると、返り値を明確に定義した状態でコードを書くことができます。
    私は、「このメソッドはこの仕事だけをする」と定義してから実装に入ることが多いです。

    最初に小さなゴールを設定しているので、目的を見失いづらく、テストを書く際も仕事が限定されているので作業がスムーズになります。

    4. FormObject・ValueObjectの活用

    バリデーションや値の変換ロジックをモデルに詰め込まず、FormObjectやValueObjectに切り出すと単体テストがしやくなります。

    1class UserRegistrationForm
    2  include ActiveModel::Model
    3  
    4  attr_accessor :name, :email
    5  
    6  validates :name, presence: true
    7  validates :email, format: { with: /@/}
    8  
    9  def save
    10    return false unless valid?
    11    User.create(name: name, email: email)
    12  end
    13end

    入力項目の多いフォームだと、これらのバリデーションがModelに大量に記述され、見通しが悪くなりやすいです。
    それに伴いモデルのテストコードも肥大化していきます。

    テストファーストがもたらす副次的な効果

    「この処理はどこで何をする?」という境界が自然と明確になる
    メソッドの使い方を先に定義して実装に入るので、迷いが少なくなる
    設計や要件が曖昧なとき、テストを書く過程で明確になっていく

    私が仕事をする中で、感じたメリットになります。

    「どんなテストになるのか?」 = 「どんな挙動をすればいいのか?」であると考えています。

    その過程で、要件漏れや現状の使用との衝突に気がつくこともあります。

    実際にテストコードを書かずとも、先に「どんな挙動をすればいいのか?」を明確にしてから作業に入ると、漏れや追加の要件の確認などの作業が格段に減ったように感じます。

    終わりに

    テストファーストは「最初にテストを書く」という技術的な手法だけではなく、設計の質を高めるための思考法でもあります。
    テストしやすいコードはメンテナンスしやすく、チーム開発で信頼されるためにとても大切なことです
    引き続きテストしやすい設計かという問いを持ちながら、コードと向き合っていこうと思います。

    以上になります。
    記事をお読みいただき、ありがとうございました。

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    やのけん(エンジニア)
    やのけん(エンジニア)
    Show more...

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background