RSpec: Как я могу не использовать `allow_any_instance_of` для объектов, экземпляры которых создаются в функциях, которые я вызываю?

У меня есть класс A с методом M, для которого я хочу написать тест T. Проблема в том, что метод M создает новый объект O. Я хочу издеваться над методом F этого нового объекта O.

class A

  def M(p1, p2)
    @o = O.new(p1, p2)
  end

end

class O

  def F(q)
    ...
  end

end

Я могу очень легко сделать это с помощью функции allow_any_instance_of в RSpec, но я действительно не вижу способа сделать это только с помощью allow или expect. Я понимаю, что могу имитировать метод существующего экземпляра и класса, но из моих тестов я не смог заставить его работать с методами объектов, которые создаются в методе, который я тестирую.

T :process do
  it "works" do
    # This works
    allow_any_instance_of(O).to receive(:F).and_return(123)
    ...
  end

  it "does not works" do
    # This fails
    allow(O).to receive(:F).and_return(123)
    ...
  end
end

Как я узнаю, что это не удается?

Я изменил свой метод F на puts() и вижу результат на экране, когда использую allow(O). Он вообще не появляется, когда я использую allow_any_instance_of(). Поэтому я знаю, что он работает так, как ожидалось, только в последнем.

  def F(q)
    puts("If I see this, then F() was not mocked properly.")
    ...
  end

Я бы подумал, что allow(O)... должен подключаться к классу, поэтому всякий раз, когда создается новый экземпляр, следуют имитированные функции, но, по-видимому, нет.

Есть ли у вас тесты RSpec, обрабатывающие такие случаи насмешек по-другому, без использования функции allow_any_instance_of()?

Я спрашиваю, потому что это помечен как устаревший (@allow-old-syntax) начиная с RSpec 3.3, поэтому похоже, что мы больше не должны использовать эту функцию, особенно когда выйдет RSpec 4.x, она, вероятно, исчезнет.


person Alexis Wilke    schedule 19.11.2019    source источник


Ответы (2)


Причина этого

allow(O).to receive(:F).and_return(123)

Не работает то, что :F не является методом O, поэтому O никогда не получает это сообщение (вызов метода).

Лучшим решением для вас будет рефакторинг вашего кода для использования внедрения зависимостей. (Обратите внимание, что ваш пример до крайности абстрактен, если вы предоставили пример из реальной жизни - ближе к земле - возможно, был бы возможен лучший рефакторинг)

class A
  attr_accessor :o_implementation

  def initialize(o_implementation)
    @o_implementation = o_implementation
  end

  def M(p1, p2)
    @o = o_implementation.new(p1, p2)
  end

end

RSpec.describe A do
  subject { described_class.new(klass) }
  let(:klass) { O }
  let(:a_double) { instance_double(klass) }


  it do 
     allow(klass).to receive(:new).and_return(a_mock)
     allow(a_double).to receive(:F).and_return(123)
  end
end

С внедрением зависимостей вы выходите за рамки решения о том, какой класс создавать. Это разделяет ваш код (перестает быть связанным с O, теперь он зависит только от интерфейса O, который он использует) и упрощает* его тестирование.

(*) Кто-то может возразить, что allow_any_instance проще (меньше усилий, меньше ввода), но у него есть некоторые проблемы, и их следует по возможности избегать.

person Grzegorz    schedule 19.11.2019
comment
Превосходно. Это может быть немного сложнее, чем то, что мы хотим сделать прямо сейчас, но это хорошая идея, и да, идея состоит в том, чтобы удалить все allow_any_instance. (Кстати, в C++ мы бы назвали этот метод доступа либо обратным вызовом, либо фабрикой.) - person Alexis Wilke; 19.11.2019

(в качестве небольшого отступления: я могу понять возможную потребность в очень тщательной анонимизации вашего кода, но вы все равно можете следовать руководству по стилю ruby: методы начинаются с нижнего регистра, только классы начинаются с верхнего регистра)

Итак, во-первых: allow(O) работает, но захватывает только методы класса. Если вам нужно захватить методы экземпляра, вам нужно вызвать allow для конкретного экземпляра.

Так как ваш пример довольно разреженный, я не вижу причин, по которым мы не могли бы отделить создание объекта от теста? Если это возможно, очень простым подходом было бы написать что-то вроде:

describe :process do 
  before do 
    @o = A.o_maker(p1,p2) 
    allow(@o).to receive(:some_function) { 123 } 
  end 
  it "works" do 
    # do something with `@o` that should call the function
  end
end 

Я лично предпочитаю этот подход созданию фиктивного класса, как предлагалось ранее. Это, вероятно, хорошо известно, но для ясности: проблема с фиктивным классом imho заключается в том, что вы больше не тестируете класс A, а фиктивный. В некоторых случаях это может быть полезно, но из исходного вопроса неясно, применимо ли это в данном случае и не является ли это излишне сложным. И во-вторых: если ваш код это такой сложный (например, какой-то метод, который создает новый объект, а затем вызывает F), я бы предпочел 1) реорганизовать мой код, чтобы сделать его пригодным для тестирования, и/или 2 ) тестовые побочные эффекты (например, F добавляет строку журнала аудита, устанавливает состояние, ...). Мне не нужно «тестировать» мою реализацию (правильно ли называется метод), но выполняется ли она (и, конечно, как всегда, есть исключения, например, при вызове внешних служб или что-то в этом роде -- но опять же все, что невозможно вывести из исходного вопроса).

person nathanvda    schedule 19.11.2019
comment
Я новичок в Ruby, поэтому для меня все это немного сложно. В нашем случае F проверяет базу данных, чего в тесте мы хотим избежать. Также мы хотим убедиться, что если F возвращает определенное значение, то A реагирует соответствующим образом. Так что это, безусловно, случай внешней службы (базы данных). При этом вполне вероятно, что текущая реализация может быть изменена, чтобы сделать ее более тестируемой. - person Alexis Wilke; 19.11.2019