Восстановить класс / модуль до девственного состояния

Мне интересно, как RSpec достигает этого:

expect( MyObject ).to receive(:the_answer).and_return(42)

Использует ли RSpec define_singleton_method вводить :the_answer в MyObject?

Это может сработать. Давайте представим, что это то, что MyObject похоже:

class MyObject
    def self.the_answer
        21 * 2
    end
end

Введенный the_answer Метод может содержать что-то вроде этого:

@the_answer_called = true
42

Однако как RSpec затем восстанавливает класс в его неизмененное состояние? Как это так восстановить MyObject.the_answer "действительно" возвращает 42 снова? (С помощью 21 * 2)

С момента написания этого вопроса я думаю, что нет. Методы класса остаются проверенными до тех пор, пока RSpec не прекратит работу.

Тем не менее, (и это суть вопроса), как можно отменить определение define_singleton_method?

Я думаю, что самый простой способ будет работать old_object = Object.dup до define_singleton_method используется, но потом, как мне восстановить old_object в Object?

Это также не кажется мне эффективным, когда все, что я хочу сделать, это восстановить один метод класса. Хотя в данный момент это не проблема, возможно, в будущем я также хочу сохранить некоторые переменные экземпляра внутри MyObject неповрежденными. Есть ли нехакерский способ дублировать код (21 * 2) и переопределить это как the_answer с помощью define_singleton_method вместо полной замены Object?

Все идеи приветствуются, я определенно хочу знать, как сделать Object === old_object несмотря на.

1 ответ

Решение

RSpec не дублирует / не заменяет экземпляр или его класс. Он динамически удаляет метод экземпляра и определяет новый. Вот как это работает: (вы можете сделать то же самое для методов класса)

Учитывая ваш класс MyObject и экземпляр o:

class MyObject
  def the_answer
    21 * 2
  end
end

o = MyObject.new
o.the_answer #=> 42

RSpec сначала сохраняет оригинальный метод, используя Module#instance_method, Возвращает UnboundMethod:

original_method = MyObject.instance_method(:the_answer)
#=> #<UnboundMethod: MyObject#the_answer>

затем он удаляет метод, используя Module#remove_method: (мы должны использовать send здесь, потому что remove_method является частным).

MyObject.send(:remove_method, :the_answer)

и определяет новый, используя Module#define_method:

MyObject.send(:define_method, :the_answer) { 'foo' }

Если вы позвоните the_answer Теперь вы мгновенно вызываете новый метод:

o.the_answer #=> "foo"

После примера RSpec удаляет новый метод

MyObject.send(:remove_method, :the_answer)

и восстанавливает оригинал (define_method принимает либо блок, либо метод):

MyObject.send(:define_method, :the_answer, original_method)

Вызов метода работает, как и ожидалось:

o.the_answer #=> 42

Реальный код RSpec намного сложнее, но вы поняли идею.

Другие вопросы по тегам