shoulda-context で focus を使うための shoulda-focus

# 概要

minitest-focus と shoulda-context との組み合わせがうまく動かないので、shoulda-context と共に使える focus を実装してみました。

# 背景

## minitest-focus

Minitest::Unit によるテストを便利にするための gem として minitest-focus というものがあります。

http://docs.seattlerb.org/minitest-focus/

これは、 編集中のテストケースの中で、ある特定のテストだけを実行したいときに そのテストに ``focus`` というキーワードを付けることで、同じテストケースの中の他のテストをすべてスキップできる、という便利な gem です。

特に guard-minitest でテストを自動実行しているときには非常に便利です。
https://github.com/guard/guard-minitest

こんな風にして使います。

```rb
require "minitest/autorun"
require "minitest/focus"

class MyTest < MiniTest::Unit::TestCase

  def test_unfocused
    # this test will be skipped
  end

  focus   # <<< FOCUSED!
  def test_focused_test
    # only this one will run
  end

  def test_unfocused_too
    # this test will be skipped, too
  end
end
```

## shoulda-context

一方、Minitest::Unit で BDD 的なテストケースを書ける shoulda-context という gem があって、rspec ではなく Minitest ベースでテストを書いている場合には、かなりよく使われているのだと思います。

https://github.com/thoughtbot/shoulda-context

# 問題点

問題は、minitest-focus は shoulda-context との組み合わせがうまく動かないことです。

shoulda-context は、その内部でテスト関数をたくさん作成するのですが、普通に書いている限りそこに focus を付けるすべがありません。

# 解決策

そこで、shoulda-context と合わせて使うための focus を以下のように作ってみました。これは minitest-focus とは独立して、単独で動作します（というか、たぶん両方入れると誤動作します）。

## 使用例

次のように、``context`` または ``should``, ``should_not`` に ``focus`` を付けることができます。

```rb
require 'shoulda-focus'

class MyTest < MiniTest::Unit::TestCase

  should "will not be tested" do assert false end

  context "Unfocused context" do
    should "will not be tested" do assert false end

    focus  # <<<<<< FOCUSED!
    should "will be tested" do assert true end

    should "will not be tested" do assert false end
  end

  focus  # <<<<<< FOCUSED!
  context "Focused context" do
    should "will be tested" do assert true end
    should "will be tested" do assert true end
    should "will be tested" do assert true end
    context "Focused sub context" do
      should "will be tested" do assert true end
    end
  end

end
```

## ソース

```rb:shoulda-focus.rb
# shoulda-focus.rb
#
# tested with: shoulda (3.5.0), minitest (4.7.5)
#
# Marking a context or a should by 'focus'
# allows testing only the focused tests, 
# skipping all other unfocused ones in a TestCase.
#
# REVISIONS:
#   2014-05-30 report number of skipped tests in the result
#
# EXAMPLE:
#
# class MyTest < ActionController::TestCase
# 
#   should "will not be tested" do assert false end
# 
#   context "Unfocused context" do
#     should "will not be tested" do assert false end
# 
#     focus  # <<<<<< FOCUSED!
#     should "will be tested" do assert true end
# 
#     should "will not be tested" do assert false end
#   end
# 
#   focus  # <<<<<< FOCUSED!
#   context "Focused context" do
#     should "will be tested" do assert true end
#     should "will be tested" do assert true end
#     should "will be tested" do assert true end
#     context "Focused sub context" do
#       should "will be tested" do assert true end
#     end
#   end
# 
# end
#

module Shoulda
  module Context

    class << self
    
      attr_accessor :focused      # indicates we are in a focused context
      attr_writer   :focus_next   # indicates focus just called, we will focus the next context/should
      attr_accessor :focus_used   # indicates focus is used, then we will skip unfocused shoulds

      # to see if we should focus the next context
      def to_focus?
        # either we are in a focused context or 'focus' was just called
        result = @focused || @focus_next
        @focus_next = false
        result
      end

    end
    
    module ClassMethods
    
      # call this method to mark the next context/should to be focused
      def focus
        unless Shoulda::Context.focus_used
          puts "", 
               "  * FOCUS CALLED FOR [#{self.name}] WE WILL SKIP UNFOCUSED TESTS",
               "    " + caller(2).first.sub(/:in .*/, ''),
               ""
        end
        Shoulda::Context.focus_used = true
        Shoulda::Context.focus_next = true
      end
      
    end

    class Context
    
      alias :initialize_without_focus :initialize
      def initialize(name, parent, &blk)
        was_focused = Shoulda::Context.focused
        Shoulda::Context.focused = Shoulda::Context.to_focus?

          initialize_without_focus(name, parent, &blk)

        Shoulda::Context.focused = was_focused
      end

      alias :should_without_focus :should
      def should(name_or_matcher, options = {}, &blk)
        to_focus = Shoulda::Context.to_focus?

        count_before = shoulds.count
        should_without_focus(name_or_matcher, options, &blk)
        
        if shoulds.count > count_before
          shoulds.last[:focused] = to_focus
        else
          should_eventuallys.last[:focused] = to_focus
        end
      end
      
      alias :should_not_without_focus :should_not
      def should_not(matcher)
        to_focus = Shoulda::Context.to_focus?
        should_not_without_focus(matcher)
        shoulds.last[:focused] = to_focus
      end
      
      alias :create_test_from_should_hash_without_focus :create_test_from_should_hash
      def create_test_from_should_hash(should)
        if not Shoulda::Context.focus_used or should[:focused]
          create_test_from_should_hash_without_focus(should)
        else
       else
#         to report skipped tests verbosely, use skip() instead (will be too noisy and slow)
#          should[:block] = Proc.new(){ skip() }
          should[:block] = Proc.new(){ skip_quietly() }
          create_test_from_should_hash_without_focus(should)
        end
      end
    end
  end
end

MiniTest::Unit::TestCase.class_eval do
  alias :run_without_quiet_skip :run
  def run(runner)
    @runner = runner
    run_without_quiet_skip(runner)
  end
  
  def skip_quietly()
    @runner.instance_eval do
      @skips += 1
    end
  end
end
```

# 勉強中

その他、shoulda を使った Rails のテストについて調べた内容を以下のページにまとめています

http://dora.bk.tsukuba.ac.jp/~takeuchi/index.php?%A5%BD%A5%D5%A5%C8%A5%A6%A5%A7%A5%A2%2Frails%2FActiveSupport_TestCase%20%2B%20shoulda%20%A4%CB%A4%E8%A4%EB%A5%C6%A5%B9%A5%C8#g183283d