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, с удовольствием используем некоторые волшебные трюки, которые люди, которые используют менее выразительные языки программирования, не могут использовать. Но это тот случай, когда магия слишком темная и ее следует избегать. Вместо этого используйте общепринятый подход к внедрению зависимостей.