如果没有 any_instance,如何断言没有进行方法调用?

How to assert that a method call was not made, without any_instance?

我有一个class,在一种情况下应该调用:my_method,但在另一种情况下不能调用方法:my_method。我想测试这两种情况。另外,我希望测试记录不应调用 :my_method 的情况。

Using any_instance is generally discouraged,所以我很乐意学习一个很好的方法来替换它。

此代码片段是我想编写的那种测试的简化示例。

class TestSubject
  def call
    call_me
  end

   def call_me; end
   def never_mind; end
end

require 'rspec'

spec = RSpec.describe 'TestSubject' do
  describe '#call' do
    it 'calls #call_me' do
      expect_any_instance_of(TestSubject).to receive(:call_me)
      TestSubject.new.call
    end

    it 'does not call #never_mind' do
      expect_any_instance_of(TestSubject).not_to receive(:never_mind)
      TestSubject.new.call
    end
  end
end

spec.run # => true

有效,但使用expect_any_instance_of方法,不推荐。

如何更换?

我会做类似的事情

describe TestSubject do
  describe '#call' do
    it 'does not call #something' do 
      subject = TestSubject.new
      allow(subject).to receive(:something)

      subject.call

      expect(subject).not_to have_received(:something)
    end
  end
end

希望对您有所帮助!

这就是我通常进行单元测试的方式。我更新了代码以支持您(或其他读者)将来可能遇到的其他问题。

class TestSubject
  def call
    some_call_me_value = call_me
    call_you(some_call_me_value)
  end

  def call_me; end
  def call_you(x); end
  def never_mind; end

  class << self
    def some_class_method_a; end

    def some_class_method_b(x, y); end
  end
end

require 'rspec'

spec = RSpec.describe TestSubject do
  context 'instance methods' do
    let(:test_subject) { TestSubject.new }

    describe '#call' do
      let(:args) { nil }
      let(:mocked_call_me_return_value) { 'somecallmevalue' }
      subject { test_subject.call(*args) }

      before do
        allow(test_subject).to receive(:call_me) do
          mocked_call_me_return_value
        end
      end

      it 'calls #call_me' do
        expect(test_subject).to receive(:call_me).once
        subject
      end

      it 'calls #call_you with call_me value as the argument' do
        expect(test_subject).to receive(:call_you).once.with(mocked_call_me_return_value)
        subject
      end

      it 'does not call #never_mind' do
        expect(test_subject).to_not receive(:never_mind)
        subject
      end

      it 'calls in order' do
        expect(test_subject).to receive(:call_me).once.ordered
        expect(test_subject).to receive(:call_you).once.ordered
        subject
      end
    end

    describe '#call_me' do
      let(:args) { nil }
      subject { test_subject.call_me(*args) }

      # it ...
    end

    describe '#call_you' do
      let(:args) { nil }
      subject { test_subject.call_you(*args) }

      shared_examples_for 'shared #call_you behaviours' do
        it 'calls your phone number'
        it 'creates a Conversation record'
      end

      # just an example of argument-dependent behaviour spec
      context 'when argument is true' do 
        let(:args) { [true] }

        it 'does something magical'
        it_behaves_like 'shared #call_you behaviours'
      end

      # just an example of argument-dependent behaviour spec
      context 'when argument is false' do
        let(:args) { [false] }

        it 'does something explosive'
        it_behaves_like 'shared #call_you behaviours'
      end
    end
  end

  context 'class methods' do
    let(:args) { nil }

    describe '#some_class_method_a' do
      let(:args) { nil }
      subject { TestSubject.some_class_method_a(*args) }

      # it ...
    end

    describe '#some_class_method_b' do
      let(:args) { [1, 2] }
      subject { TestSubject.some_class_method_b(*args) }

      # it ...
    end
  end
end

spec.run # => true

不测试某些方法是否被调用。
这将使您的测试更加严格地执行细节,并会迫使您在每次重构(在不更改行为的情况下更改实现细节)您的 class 被测时更改测试。

而是针对 return 值或更改的应用程序状态进行测试。
很难想出这个例子,你没有提供足够的关于测试下 class 的上下文。

class CreateEntity
  def initialize(name)
    @name = name
  end 

  def call
    if company_name?(@name)
      create_company
    else
      create_person
    end
  end

  def create_person
    Person.create!(:name => @name)
  end

  def create_company
    Company.create!(:name => @name)
  end
end

# tests

RSpec.describe CreateEntity do
  let(:create) { CreateEntity.new(name).call }

  describe '#call' do
    context 'when person name is given' do
      let(:name) { 'Firstname Lastname' }
      it 'creates a person' do
        expect { create }.to change { Person.count }.by(1) 
      end

      it 'do not create a company' do
        expect { create }.not_to change { Company.count }
      end
    end

    context 'when company name is given' do
      let(:name) { 'Name & Sons Ltd' }
      it 'creates a company' do
        expect { create }.to change { Company.count }.by(1) 
      end

      it 'do not create a person' do
        expect { create }.not_to change { Person.count }
      end
    end
  end
end

通过上述测试,只要行为保持不变,我就可以在不更改测试的情况下更改 CreateEntity.call 方法的实现方式。