Восстановить класс / модуль до девственного состояния
Мне интересно, как 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 намного сложнее, но вы поняли идею.