Можно ли протестировать метод класса с помощью MiniTest (например, Foo.bar)?

Как я могу проверить, что Foo.bar вызывается в следующем примере, не проверяя поведение метода bar (который уже проверен в другом месте)?

# Code
class Alpha
  def process
    Foo.bar
  end
end

Следующая спецификация - это то, что у меня есть до сих пор. К сожалению, этот подход выдает предупреждение «класс уже определен», поскольку Foo уже определен в другом месте моего проекта.

 # Spec
 let(:alpha) { Alpha.new }
 let(:klass) { MiniTest::Mock.new }

 subject { alpha.process }

 it "calls Foo.bar" do
   klass.expect(:bar, '')     # Define method call expectation
   Foo = klass                # Redefine Foo as a mock object
   subject                    # Run method being tested
   klass.verify               # Confirm method was called
 end

Я не хочу, чтобы мой тест зависел от класса Foo, поскольку это внешняя зависимость, и я не хочу проверять значение ответа Foo.bar, поскольку оно может произвольно измениться.


person Matt Hodan    schedule 28.12.2012    source источник


Ответы (2)


Чтобы смоделировать такой класс, вам нужно вставить точку инъекции следующим образом:

class Alpha
  def initialize(opts = {})
    @foo_class = opts[:foo_class] || Foo
  end

  def process
    @foo_class.bar
  end
end

Это работает, потому что имя класса — это просто константа в Ruby, которую можно присвоить, как и любое другое значение. Таким образом, вместо того, чтобы жестко кодировать вызов класса Foo в Alpha, вы теперь вызываете метод для того, на что указывает ваша новая переменная экземпляра @foo_class. В большинстве случаев это будет класс Foo, если только вы не передадите что-то другое. Обычно я скрываю такие параметры макета как необязательные параметры, как я сделал здесь. Я также не включаю ссылки на них в документацию, предназначенную для конечных пользователей, потому что технически они не являются частью того, что я считаю общедоступным API.

Затем в своем тесте вы можете инициализировать свой объект Alpha следующим образом:

fooClassMock = MiniTest::Mock.new
fooClassMock.expect(:bar, '')
alpha = Alpha.new(:foo_class => fooClassMock)

И вы должны получить результаты, которые вы ищете.

person Lee    schedule 28.12.2012
comment
Правильно, действительно. Что, если мой метод процесса (как в приведенном выше примере) должен делать @foo_class.new.bar? Каковы должны быть мои ожидания? fooClassMock.expect(:bar, '') это просто, но другой должен быть fooClassMock.expect(:new, Foo) или что-то еще? Я не могу издеваться над чем-то подобным. Любой намек? - person Marco Bresciani; 02.11.2013
comment
@user1955626 user1955626 На самом деле, вместо того, чтобы ожидать :new, я считаю, что вы должны ожидать :initialize. Возвращаемое значение initialize должно быть другим Mock, который ожидает :bar и возвращает соответствующее значение. Но независимо от того, ожидает ли он :new или :initialize, ключ должен вернуть другой Mock, который ожидает :bar. - person Lee; 02.11.2013
comment
Хорошо, ожидая :initialize. Как насчет возвращенного результата? Должно ли это быть fooClassMock.expect(:initialize, the_other_mock.class) или fooClasssMock.expect(:initialize, the_other_mock.new) или что-то в этом роде? Очевидно, написав новый макет, например the_other_moc = MiniTest::Mock.new, и конкретное ожидание от метода :bar. - person Marco Bresciani; 03.11.2013
comment
Это становится слишком длинным для комментариев, не стесняйтесь открывать вопрос по нему, и я отвечу на него там, где у меня нет только 500 символов, и я могу дать вам ответ с выделенным синтаксисом. - person Lee; 03.11.2013

Я знаю, что это немного старый вопрос, но я искал для него решение, которое не требовало бы изменения бизнес-кода для упрощения тестов, и я думаю, что нашел решение.

Даже если вы используете Rails, вам нужно добавить gem "minitest" к Gemfile, а затем добавить require "minitest/mock" к test_helper.rb.

it "calls Foo.bar" do
  bar_is_called = false
  bar_lambda = ->{
    bar_is_called = true
  }

  Foo.stub(:bar, bar_lambda) do
    Alpha.new.process
  end
  bar_is_called.must_equal true
end

.stub может передаваться возвращаемое значение, или при передаче чего-то, что отвечает на .call, вызывается .call (Документы для метода-заглушки). В этом примере. bar_lambda вызывается, изменяя значение bar_is_called. Это подтверждает, что Foo.bar был вызван.

Аналогично работает и в тестовом синтаксисе:

test 'Foo.bar is called' do
  bar_is_called = false
  bar_lambda = ->{
    bar_is_called = true
  }

  Foo.stub(:bar, bar_lambda) do
    Alpha.new.process
  end
  assert bar_is_called
end
person Ryan V.    schedule 27.09.2016
comment
Люблю этот ответ, определенно помог мне. Просто добавим, что если :bar ожидает такой аргумент, как Foo.bar(some_arg), вы можете добавить его в свою лямбу, например. bar_lambda = ->(some_arg) { bar_is_called = true } - person scottknight; 05.02.2021