Rails - проверка метода, который использует DateTime.now
У меня есть метод, который использует DateTime.now для выполнения поиска по некоторым данным, я хочу протестировать метод с различными датами, но я не знаю, как заглушить DateTime.now и не могу заставить его работать с Timecop ( если он даже работает так).
Со временем я попробовал
it 'has the correct amount if falls in the previous month' do
t = "25 May".to_datetime
Timecop.travel(t)
puts DateTime.now
expect(@employee.monthly_sales).to eq 150
end
когда я запускаю спецификацию, я вижу, что putt DateTime.now дает 2015-05-25T01:00:00+01:00
, но с тем же самым puts DateTime.now в рамках метода, который я тестировал выходы 2015-07-24T08:57:53+01:00
(сегодняшняя дата).
Как я могу это сделать?
------------------ обновление ---------------------------- -----------------------
Я создавал записи (@employee и т.д.) в блоке before(:all)
, который, похоже, вызвал проблему. Он работает только тогда, когда настройка выполняется после блока Timecop do
. Почему это так?
Ответы
Ответ 1
TL; DR. Проблема заключалась в том, что DateTime.now
был вызван в Employee
до того, как в спецификациях был вызван Timecop.freeze
.
Timecop издевается над конструктором Time
, Date
и DateTime
. Любой экземпляр, созданный между freeze
и return
(или внутри блока freeze
), будет издеваться.
Любой экземпляр, созданный до freeze
или после return
, не будет затронут, потому что Timecop не взаимодействует с существующими объектами.
Из README (мой акцент):
Драгоценный камень, обеспечивающий возможности "путешествия во времени" и "замораживания времени", делая его мертвым простым для проверки кода, зависящего от времени. Он предоставляет единый метод для mock Time.now, Date.today и DateTime.now в один вызов.
Поэтому для создания объекта Time
, который вы хотите высмеять, необходимо вызвать Timecop.freeze
. Если вы freeze
в блоке RSpec before
, это будет выполняться до того, как будет оценен subject
. Однако, если у вас есть блок before
, в котором вы настроили свой объект (@employee
в вашем случае), и у вас есть другой блок before
во вложенном describe
, тогда ваш объект уже настроен, вызвав DateTime.new
, прежде чем вы замерли.
Что произойдет, если вы добавите следующее к своему Employee
class Employee
def now
DateTime.now
end
end
Затем вы запускаете следующую спецификацию:
describe '#now' do
let(:employee) { @employee }
it 'has the correct amount if falls in the previous month', focus: true do
t = "25 May".to_datetime
Timecop.freeze(t) do
expect(DateTime.now).to eq t
expect(employee.now).to eq t
expect(employee.now.class).to be DateTime
expect(employee.now.class.object_id).to be DateTime.object_id
end
end
end
Вместо использования блока freeze
вы также можете freeze
и return
в rspec before
и after
hooks:
describe Employee do
let(:frozen_time) { "25 May".to_datetime }
before { Timecop.freeze(frozen_time) }
after { Timecop.return }
subject { FactoryGirl.create :employee }
it 'has the correct amount if falls in the previous month' do
# spec here
end
end
Вне темы, но, возможно, посмотрите http://betterspecs.org/
Ответ 2
Timecop должен иметь возможность обрабатывать то, что вы хотите. Попробуйте заморозить время перед тем, как запустить тест вместо того, чтобы просто путешествовать, а затем разморозить, когда вы закончите. Вот так:
before do
t = "25 May".to_datetime
Timecop.freeze(t)
end
after do
Timecop.return
end
it 'has the correct amount if falls in the previous month' do
puts DateTime.now
expect(@employee.monthly_sales).to eq 150
end
От Timecop readme:
замораживание используется, чтобы статически издеваться над концепцией сейчас. По мере выполнения вашей программы Time.now не изменится, если вы не выполните последующие вызовы в API Timecop. с другой стороны, вычисляет смещение между тем, что мы сейчас считаем Time.now(напомним, что мы поддерживаем вложенное перемещение) и время, пройденное. Он использует это смещение для имитации прохождения времени.
Итак, вы хотите заморозить время в определенном месте, а не просто путешествовать к тому времени. Поскольку время пройдет с проездом, как обычно, но с другой отправной точки.
Если это все еще не работает, вы можете поместить вызов метода в блок с помощью Timecop, чтобы убедиться, что он блокирует время внутри блока, например:
t = "25 May".to_datetime
Timecop.travel(t) do # Or use freeze here, depending on what you need
puts DateTime.now
expect(@employee.monthly_sales).to eq 150
end
Ответ 3
Я столкнулся с несколькими проблемами с Timecop и другими магическими материалами, которые смешиваются с классами Date, Time и DateTime и их методами. Я обнаружил, что лучше просто использовать инъекцию зависимостей:
Код сотрудника
class Employee
def monthly_sales(for_date = nil)
for_date ||= DateTime.now
# now calculate sales for 'for_date', instead of current month
end
end
Spec
it 'has the correct amount if falls in the previous month' do
t = "25 May".to_datetime
expect(@employee.monthly_sales(t)).to eq 150
end
Мы, люди мира Ruby, с удовольствием используем некоторые волшебные трюки, которые люди, которые используют менее выразительные языки программирования, не могут использовать. Но это тот случай, когда магия слишком темная и ее следует избегать. Вместо этого используйте общепринятый подход к внедрению зависимостей.