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テストに以下を書くことで解決。
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した場合、そのままだとマージするためのキーも同じなので上書きされてしまいます。
そこで、SimpleCov
のat_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をマージすることができました!
テスト結果のsync
rspecのコードをかなり読み込みんで、
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
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元の結果として扱うことに成功しました。
そしてオールグリーン!
Rails 4以上をサポートする場合は複数appを作成できるのでrailtieやgeneratorのテストも簡単にできそう。