認識 Rails 測試


Rails內建可進行單元測試、功能測試、整合測試與效能測試的方式,使用rails產生模型、控制器或郵件處理時,都會一併產生相關測試檔案,以 觀摩 Scaffold 中的例子來看:

~gossip\$ rails g scaffold Message name:string title:string content:text
      invoke  active_record
      ...略
      invoke    test_unit
      create      test/unit/message_test.rb
      create      test/fixtures/messages.yml
      ...略
      invoke  scaffold_controller
      ...略
      invoke    test_unit
      create      test/functional/messages_controller_test.rb
      invoke    helper
      ...略
      invoke      test_unit
      create        test/unit/helpers/messages_helper_test.rb
      ...格

可以觀察到,測試相關檔案,基本上都產生在test目錄中,在這個目錄中可以看到:

~gossip/test\$ ls
fixtures  functional  integration  performance  test_helper.rb  unit


unit、functional、integration與performance目錄,分別讓你放置單元測試、功能測試、整合測試與效能測試的程式,測試過程中要使用到的範例資料,可以將之整理在fixures目錄,test_helper.rb預設的定義是:
  • test_helper.rb
ENV["RAILS_ENV"] = "test"
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'

class ActiveSupport::TestCase
  # Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order.
  #
  # Note: You'll currently still have to declare fixtures explicitly in integration tests
  # -- they do not yet inherit this setting
  fixtures :all

  # Add more helper methods to be used by all tests here...
end

預設產生的單元測試案例,通常會require 'test_helper'。例如unit目錄中的message_test.rb:
  • message_test.rb
require 'test_helper'

class MessageTest < ActiveSupport::TestCase
  # test "the truth" do
  #   assert true
  # end
end

由於繼承了ActiveSupport::TestCase,因此在test_helper.rb中ActiveSupport::TestCase類別定義的內容,在每個測試案例中都可以使用,你可以在test_helper.rb中定義相關輔助方法。

在message_test.rb中看到了定義測試案例的基本方式,就是繼承ActiveSupport::TestCase,Rails提供的測試框架,實際上以Ruby的 Test::Unit 為基礎,在Rails中定義了test方法,可接上測試的字串描述。

舉個例子來說,若你新建了一個專案,在還沒有產生任何模型之前,可自定義一個rational_number_test.rb:
  • rational_number_test.rb
require 'test_helper'

class RationalNumberTest < ActiveSupport::TestCase
    test "rational number addition" do
        r1 = RationalNumber.new(1, 2)
        r2 = RationalNumber.new(2, 3)
        expected = RationalNumber.new(7, 6)
        result = r1 + r2
        assert_equal expected.numer, result.numer
        assert_equal expected.denom, result.denom
    end
end   

如上定義了一個單元測試,test方法會依字串描述,產生一個test_rational_number_addition方法,assert_equal斷言兩個物件是否相等,一個執行測試的例子如下:

~gossip\$ ruby -Itest test/unit/rational_number_test.rb
Loaded suite test/unit/rational_number_test
Started

RationaNumberlTest:
    ERROR rational addition (1.75s)
          NameError: uninitialized constant RationaNumberlTest::RationalNumber
          test/unit/rational_number_test.rb:5:in `block in <class:RationaNumberlTest>'


Finished in 1.762090 seconds.

1 tests, 0 assertions, 0 failures, 1 errors, 0 skips


這是個用ruby指令直接啟動測試的例子,由於要require的test_helper.rb是在test目錄下,因此要使用-I指定test目錄。目前實際上沒有撰寫RationalNumber的定義,因此測試流程根本無法完成,因此測試結果為ERROR。如果定義了RationalNumber如下:
  • rational_number.rb
class RationalNumber
    attr_accessor :numer, :denom
    def initialize(n, d) 
        @numer = n
        @denom = d
    end
    
    def +(that)
self
end end

這次順便示範一下如何指定測試方法:

~gossip\$ ruby -Itest -Itest/unit test/unit/rational_number_test.rb -n test_rational_number_addition
Loaded suite test/unit/rational_number_test
Started

RationaNumberlTest:
     FAIL rational number addition (1.02s)
          <7> expected but was
<1>.
          test/unit/rational_number_test.rb:9:in `block in <class:RationaNumberlTest>'


Finished in 1.025792 seconds.

1 tests, 1 assertions, 1 failures, 0 errors, 0 skips


可以看到斷言失敗,因此顯示FAIL,這是因為RationalNumber的+方法沒有正確撰寫,補上+方法內容順便加上-方法
  • rational_number.rb
class RationalNumber
    attr_accessor :numer, :denom
    def initialize(n, d) 
        @numer = n
        @denom = d
    end
    
    def +(that)
        RationalNumber.new(self.numer * that.denom + that.numer * self.denom, 
                     self.denom * that.denom)
    end

def -(that) RationalNumber.new(self.numer * that.denom - that.numer * self.denom, self.denom * that.denom) end
end

再度執行測試:

~gossip\$ ruby -Itest -Itest/unit test/unit/rational_number_test.rb
Loaded suite test/unit/rational_number_test
Started

RationaNumberlTest:
     PASS rational number addition (1.83s)

Finished in 1.831376 seconds.

1 tests, 2 assertions, 0 failures, 0 errors, 0 skips


這次執行測試成功,因此顯示PASS。你可以在一個測試案例類別中加上數個測試。例如:
  • rational_number_test.rb
require 'test_helper'

class RationaNumberlTest < ActiveSupport::TestCase
    test "rational number addition" do
        r1 = RationalNumber.new(1, 2)
        r2 = RationalNumber.new(2, 3)
        expected = RationalNumber.new(7, 6)
        result = r1 + r2
        assert_equal expected.numer, result.numer
        assert_equal expected.denom, result.denom
    end

    test "rational number subtraction" do
        r1 = RationalNumber.new(1, 2)
        r2 = RationalNumber.new(2, 3)
        expected = RationalNumber.new(-1, 6)
        result = r1 - r2
        assert_equal expected.numer, result.numer
        assert_equal expected.denom, result.denom
    end
end

可以看到兩個測試中都準備了r1與r2,對於每個test執行開始前要準備的資源,你可以定義setup,對於每個test執行後要回收的資源,你可以定義teardown。例如

  • rational_number_test.rb
require 'test_helper'

class RationaNumberlTest < ActiveSupport::TestCase
    def setup
        @r1 = RationalNumber.new(1, 2)
        @r2 = RationalNumber.new(2, 3)
    end

    def teardown
        @r1 = nil
        @r2 = nil
    end

    test "rational number addition" do
        expected = RationalNumber.new(7, 6)
        result = @r1 + @r2
        assert_equal expected.numer, result.numer
        assert_equal expected.denom, result.denom
    end

    test "rational number subtraction" do
        expected = RationalNumber.new(-1, 6)
        result = @r1 - @r2
        assert_equal expected.numer, result.numer
        assert_equal expected.denom, result.denom
    end
end

除了定義setup與teardown方法之外,ActiveSupport::TestCase本身也擁有setup與teardown,可以接受程式區塊、Symbol或lambda。