Mocking in Ruby with Minitest

在做单元测试的时候,如果要测试的方法需要引用外部依赖的对象,比如:发送邮件,网络通讯,记录Log, 文件系统之类的。而我们没法控制这些外部依赖的对象。为了解决这个问题,我们需要用到 StubMock 来模拟这些外部依赖的对象。

基本概念

Dummies

dummy 是最简单的概念。当我们在写测试的时候,为了避免 ArgumentError 错误,需要给方法传递一些并不真实使用的对象,通常都是使用 Object.new 来模拟。

Stub

stubdummy 很像,但是 stub 的优势是可以响应方法调用,其实现一般会硬编码一些输入和输出。stub 存在的意图是为了让测试对象可以正常的执行。

stub 也即“桩”,很早就有这个说法了,主要出现在集成测试的过程中,从上往下的集成时,作为下方程序的替代。作用如其名,就是在需要时,能够发现它存在,即可。就好像点名,“到”即可。

Mock

mock 除了有 stub 的功能之外,还可深入的模拟对象之间的交互方式,如:调用了几次、在某种情况下是否会抛出异常。

mock,主要是指某个程序的傀儡,也即一个虚假的程序,可以按照测试者的意愿做出响应,返回被测对象需要得到的信息。也即是要风得风、要雨得雨、要返回什么值就返回什么值。

示例

rails g model user name:string
rails g model subscription expires_at:date user:references
class User < ApplicationRecord
  has_one :subscription
end

# app/services/subscription_service.rb
class SubscriptionService
  SUBSCRIPTION_LENGTH = 1.month

  def initialize(user)
    @user = user
  end

  def apply
    if Subscription.exists?(user_id: @user.id)
      extend_subscription
    else
      create_subscription
    end
  end

  private

  def create_subscription
    subscription = Subscription.new(
      user: @user,
      expires_at: SUBSCRIPTION_LENGTH.from_now
    )

    subscription.save
  end

  def extend_subscription
    subscription = Subscription.find_by user_id: @user.id

    subscription.expires_at = subscription.expires_at + SUBSCRIPTION_LENGTH
    subscription.save
  end
end
# test/services/subscription_service_test.rb
require 'test_helper'
class SubscriptionServiceTest < ActiveSupport::TestCase
  test '#create_or_extend new subscription' do
    user = users :no_sub
    subscription_service = SubscriptionService.new user
    assert_difference 'Subscription.count' do
      assert subscription_service.apply
    end
  end

  test '#create_or_extend existing subscription' do
    user = users :one
    subscription_service = SubscriptionService.new user
    assert_no_difference 'Subscription.count' do
      assert subscription_service.apply
    end
  end
end

Stub

# test/models/user_test.rb
require 'test_helper'
class UserTest < ActiveSupport::TestCase
  test '#apply_subscription' do
    mock = Minitest::Mock.new
    def mock.apply; true; end

    SubscriptionService.stub :new, mock do
      user = users(:one)
      assert user.apply_subscription
    end
  end
end

# app/models/user.rb
class User < ApplicationRecord
  has_one :subscription

  def apply_subscription
    SubscriptionService.new(self).apply
  end
end

Mocking

# test/models/user_test.rb

require 'test_helper'

class UserTest < ActiveSupport::TestCase
  test '#apply_subscription' do
    mock = Minitest::Mock.new
    mock.expect :apply, true

    SubscriptionService.stub :new, mock do
      user = users(:one)
      assert user.apply_subscription
    end

    assert_mock mock # New in Minitest 5.9.0
    assert mock.verify # Old way of verifying mocks
  end
end

参考

如果觉得我的文章对您有用,请在支付宝公益平台找个项目捐点钱。 @Victor May 12, 2017

奉献爱心