スコーンの開発日記

開発中の学びをまとめていく。

【RSpec】Rakeのテストの書き方

Rakeのテストの書き方。 例えば以下のようなキャンペーン作成タスクがあるとする。

# lib/tasks/campaign.rb

namespace :campaign do
  desc 'キャンペーン作成'
  task :create, %i(title, start_date, end_date)  => :environment do |_t, args|
    params = {
      title: args[:title],
      start_date: args[:start_date],
      end_date: args[:end_date]
    }
    Tasks::Campaign.create(params)
  end
end

テストはこのように書ける。

# spec/lib/tasks/campaign_spec.rb

require 'rails_helper'

RSpec.describe 'campaign' do
  before(:all) do
    @rake = Rake::Application.new
    Rake.application = @rake
    Rake.application.rake_require 'tasks/campaign'
    Rake::Task.define_task(:environment)
  end

  before(:each) do
    @rake[task_name].reenable
  end

  context '正しいパラメータを渡したとき' do
    let(:task_name) { 'campaign:create' }
    let(:title) { 'hoge' }
    let(:start_date) { Time.current }
    let(:end_date) { Time.current }

    
    it 'キャンペーンが作成される' do
      expect { @rake[task_name].invoke(title, start_date, end_date) }
        .to change(Campaign, :count).by(1)
      
      campaign = Campaign.find_by(title: title)
      expect(campaign.start_date).to eq(start_date)
      expect(campaign.end_date).to eq(end_date)
    end
  end
end

やっていることを、上から順に見ていきます。

require 'rails_helper'

spec/rails_helper.rbを読み込んでいる。 これはテスト用の設定を書くファイル。

spec/spec_helper.rbというファイルもある。 rails_helper.rbはRails固有の設定、spec_helper.rbはRSpec全般の設定を書くらしい。

rails_helper.rb内でspec_helper.rbをrequireすればよさそう。そうすればテストファイルではrails_helper.rbだけ読み込めばよい。

参考

before(:all)の中

  before(:all) do
    @rake = Rake::Application.new
    Rake.application = @rake
    Rake.application.rake_require 'tasks/campaign'
    Rake::Task.define_task(:environment)
  end

1行ずつ見ていこう。

before(:all) do
  # hoge
end

対象グループ内の全てのテストケースの前に1度だけブロックの内容をを実行する。

@rake = Rake::Application.new

Rake::Applicationオブジェクトをinitializeして、インスタンス変数@rakeに代入している。

Rake::Applicationクラスとはなんぞや。

docs.ruby-lang.org

Rake で使用するメインのクラスです。 コマンドラインで rake コマンドを実行した時に作成され、実行されます。

なるほど。 Rakeタスクを実行するのに必要なクラスということか。

では次の行。

Rake.application = @rake

Rakeクラスとはなんぞや。

docs.ruby-lang.org

Rake というコマンドラインツールを扱うライブラリです。

へえ、そうなんだ。

Rakeモジュールのソースコードを見ると…

# frozen_string_literal: true
require "rake/application"

module Rake

  class << self
    # Current Rake Application
    def application
      @application ||= Rake::Application.new
    end

    # Set the current Rake application object.
    def application=(app)
      @application = app
    end

    (以下略)
  end
end

applicationメソッドでRakeアプリケーションのオブジェクトをセットできるようだ。 となると、Rake.application = @rakeはRakeのアプリケーションオブジェクトとして、1行上でinitializeした@rakeをセットしているのね。

次の行に進む。

Rake.application.rake_require 'tasks/campaign'

rake_requireはなにをしているのだろう? 定義元を見てみると…

    # Similar to the regular Ruby +require+ command, but will check
    # for *.rake files in addition to *.rb files.
    def rake_require(file_name, paths=$LOAD_PATH, loaded=$") # :nodoc:
      fn = file_name + ".rake"
      return false if loaded.include?(fn)
      paths.each do |path|
        full_path = File.join(path, fn)
        if File.exist?(full_path)
          Rake.load_rakefile(full_path)
          loaded << fn
          return true
        end
      end
      fail LoadError, "Can't find #{file_name}"
    end

Rubyrequireメソッドと似ているが、rake_requireは.rbファイルに加えて.rakeファイルも見ているとのこと。

ということはRake.application.rake_require 'tasks/campaign'はさっき代入したRake::Applicationオブジェクトに指定のRakeファイルを読み込ませているってことか。

では次の行。

Rake::Task.define_task(:environment)

define_taskの定義元を見てみる。

      # Define a task given +args+ and an option block.  If a rule with the
      # given name already exists, the prerequisites and actions are added to
      # the existing task.  Returns the defined task.
      def define_task(*args, &block)
        Rake.application.define_task(self, *args, &block)
      end

よくわからん… とりあえず[Ruby on Rails]RSpecによるRakeのテスト | DevelopersIOを読む限り、実行環境を渡しているらしい。

これで、before(:all)の中は読めた。 まとめると、Rake::Applicationオブジェクトを生成して、テストしたいrakeファイルを読み込ませてるってことね。

before(:each)の中

では次のブロックを見てみよう。

  before(:each) do
    @rake[task_name].reenable
  end

before(:each)before(:all)の違いってなんなんだろ。

before and after hooks - Hooks - RSpec Core - RSpec - Relish

before(:each) blocks are run before each example

before(:all) blocks are run once before all of the examples in a group

なるほど、allはすべてのテストの前に1度だけ実行される。eachは各テストの前に毎回実行される。ということらしい。exampleの単位はitなのかな?contextではなく。

@rakeは上で作ったRake::Applicationオブジェクト。@rake['campaign:create']でRake::Taskオブジェクトがとれる。

> @rake['campaign:create']
=> <Rake::Task campaign:create => [environment]>

そしてreenableがなんなのかというと…

reenable (Rake::Task) - APIdock

Reenable the task, allowing its tasks to be executed if the task is invoked again.

なるほど、1度実行したテストを再度実行できるようにする。 今回の例だとテストが1つしかないから不要だけど、複数テストを書く場合は必要ということか。

@rake[task_name].invoke(*args)

次のブロックへ。

  context '正しいパラメータを渡したとき' do
    let(:task_name) { 'campaign:create' }
    let(:title) { 'hoge' }
    let(:start_date) { Time.current }
    let(:end_date) { Time.current }

    
    it 'キャンペーンが作成される' do
      expect { @rake[task_name].invoke(title, start_date, end_date) }
        .to change(Campaign, :count).by(1)
      
      campaign = Campaign.find_by(title: title)
      expect(campaign.start_date).to eq(start_date)
      expect(campaign.end_date).to eq(end_date)
    end
  end

letexpectはRakeのテストじゃなくても使うので説明は省略。

Rake特有なのはこの部分。

@rake[task_name].invoke(title, start_date, end_date)

上でセットしたRake::Taskを実行している。

class Rake::Task (Ruby 2.5.0)

invokeで実行できる。他にexecuteというメソッドもある。

  • invoke
    • 引数を渡せる
    • 2度実行できない(reenableが必要)
  • execute
    • 引数を1つまでしかわたせない

ということのようだ。

これで、Rakeのテストが読めるようになった。 いい勉強になりました。