LoginSignup
8
8

More than 5 years have passed since last update.

RailtieとGeneratorのSpecテスト

Last updated at Posted at 2014-06-24

GitHubで公開しているrack-dev-markで、少し前にRailtieとGeneratorを追加したのですが、specテストが書けておらず微妙だな〜と思ってました。

Specテストが書けていなかった理由

Railsは複数回initializeできない

Rails 3.2ではRails::Applicationを継承したサブクラスを2つ以上作るとエラーになる。

app = Class.new(::Rails::Application)
app = Class.new(::Rails::Application)
> RuntimeError: You cannot have more than one Rails::Application

複数回initialize!を呼ぶのもNG。

app = Class.new(::Rails::Application)
app.initialize!
app.initialize!
> RuntimeError: Application has been already initialized.

Rails 4ではこの問題は起きないが、まだ3.2をテスト対象から外すには早すぎる。

攻略方法

そして、長ーく考えた結果、やっと攻略できました。

rspec内でfork

forkしたプロセスでテストを実行すればその中でRailsをinitializeできます。
この時考慮しなければいけないのは以下2点

  • Coverageのマージ
  • テスト結果のsync

Coverageのマージ

Coverallsはそもそも各rubyとgemの組み合わせのCoverageをマージしているので、forkしたとしてもマージしてくれる。
ローカルでsimplecovを使った場合はプロセス同士がcoverageの結果を上書きし合うので結果が正しくなくなる。

specテストに以下を書くことで解決。

spec/spec_helper.rb
resultset_path = SimpleCov::ResultMerger.resultset_path
FileUtils.rm resultset_path if File.exists? resultset_path

SimpleCov.use_merging true
SimpleCov.at_exit do
  SimpleCov.command_name "fork-#{$$}"
  SimpleCov.result.format!
end

まず、SimpleCov.use_merging trueですが、これは以前実行したcoverageの結果に対してマージを行う設定。
cucumberのテストとrspecのテストをマージする際などに使われる。

しかし、rspecのプロセスをforkした場合、そのままだとマージするためのキーも同じなので上書きされてしまいます。

そこで、SimpleCovat_exitフックでマージする前にキーを変更してやるのがコツ。

SimpleCov.at_exit do
  SimpleCov.command_name "fork-#{$$}"
  SimpleCov.result.format!
end

そして、そのままだと過去のテスト分もマージされてしまうので、開始時に初期化。

resultset_path = SimpleCov::ResultMerger.resultset_path
FileUtils.rm resultset_path if File.exists? resultset_path

これで無事、forkしたrspecのCoverageをマージすることができました!

Screen Shot 2014-06-24 at 10.32.45 AM.png

テスト結果のsync

rspecのコードをかなり読み込みんで、

spec/support/fork_helper.rb
shared_context 'forked spec' do
  around do |example|
    read, write = IO.pipe
    pid = fork do
      $stdout.sync = true
      $stderr.sync = true
      res = example.run
      Marshal.dump(res, write)
      write.close
    end 
    Process.waitpid2 pid 
    res = Marshal.load(read)
    example.example.send :set_exception, res if res && !res.empty?
    read.close
  end 
end
spec/rack/dev-mark/railtie_spec.rb
require 'spec_helper'

describe Rack::DevMark::Railtie do
  include_context 'forked spec'

  before do
    @app = Class.new(::Rails::Application)
    @app.config.active_support.deprecation = :stderr
    @app.config.eager_load = false
  end 
  context "rack_dev_mark enable" do
    before do
      @app.config.rack_dev_mark.enable = true
      @app.initialize!
    end 
    it 'inserts the middleware' do
      expect(@app.middleware.middlewares).to include(Rack::DevMark::Middleware)
    end 
  end
end

こんな感じのコードで突破しました。

流れとしてはforkしたプロセスとパイプでやり取りし、テスト結果をパイプで送って親プロセスのテスト結果にセットします。

そしてforkしたプロセスの結果をfork元の結果として扱うことに成功しました。

Screen Shot 2014-06-24 at 10.30.08 AM.png

そしてオールグリーン!

coveralls.jpeg

Rails 4以上をサポートする場合は複数appを作成できるのでrailtieやgeneratorのテストも簡単にできそう。

8
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
8