在测试Rails应用程序时的时间旅行

128 阅读5分钟

在测试一个用Rails编写的应用程序时,经常需要检查一些事件在执行某些操作后必须在未来发生。例如,当一个用户在 "2017年7月22日 "注册时,他得到了一个奖金。为了手动测试这一点,必须将当前时间设置为 "2017年7月22日"。乍一看,这看起来是一个相当琐碎的任务。只要打开系统设置,禁用与提供当前时间的服务器的同步,改变操作系统的本地当前时间。这将会起作用,这里没有什么惊喜。但是,当当前时间不能返回到真正的当前时间时,问题就来了。这就是可能出现的问题清单:聊天中的信息被弄乱了,安全的网站无法打开,因为抱怨HTTPS的问题,一个提交可以创建错误的时间戳,版本控制系统的历史被弄乱了,等等,等等。所以,如果这些问题对你来说很熟悉,欢迎你加入,并享受思想的流动吧

题外话

我经常需要测试一个Rails应用程序,它必须在某个特定的时间完成,大多是在未来。而我总是通过改变操作系统的时间,导致上述问题的出现。有一天,这让我非常恼火,于是我开始考虑不改变操作系统时间但可以模拟当前时间的解决方案。受到基于Linux的机器上允许通过设置TZ 环境变量来欺骗时区的功能的启发,我开始朝这个方向思考。但不幸的是,我没能在系统层面上找到一些欺骗当前时间的机会。最后我在Ruby gem的基础上发明了自己的解决方案。我使用Timecop完成了这个任务。但你也可以随时使用任何其他的宝石。在这篇文章中,我只是描述了大致的想法。所以,千万不要因为上面描述的问题而手动改变你的操作系统时间,请继续阅读!

解决方案

在展示解决方案背后的主要想法之前,我想分享一些黑客,这些黑客不仅在这个特定的情况下,而且在其他情况下也会很方便。

有时需要在本地修改Rails应用程序的代码,但这些修改不能被部署到生产服务器或提交到版本控制系统。例如,我想在本地为某个命名空间设置一个别名,因为它太长了,而且我不想在调试或在rails控制台调查某些东西时打很多字。即,我想做GW = ActiveMerchant::Billing::TrustCommerceGateway 。如果不是每次打开rails控制台后都手动设置这个别名,那就太好了。一个可能的解决方案是定义一个自定义的初始化器(我希望这对你来说不是新闻,什么是初始化器,众所周知,初始化器位于Rails应用程序的config/initializers/ 文件夹中,更多关于Rails初始化器的信息在这里)。但它会被版本控制系统自动跟踪。这不是一个问题--可以将这些配置为被忽略。例如,对于 Git 来说,这个文件的路径可以放在.gitignore 中,这样就可以了。

我建议定义一个这样的初始化器(例如config/initializers/dev.rb ),它将适用于你所有的别名、本地猴子补丁等等。顺便说一下,看看一个非常有用的猴子补丁,你会喜欢的:

class BigDecimal
 def inspect
   to_s('F')
 end
end

现在,当你在Rails控制台从DB中获取ActiveRecord实例时,你将看到人类可读的浮点数,而不是不可读的东西,BigDecimal's。例如,之前是#<BigDecimal:7fb8301a25a8,'0.5005E3',18(18)> ,之后是 -500.50

足够的介绍和借口。这就是改变当前时间的代码片段:

class TravelTime
  def self.take
    File.read(ENV['TRAVEL_TIME']).chomp
  end
end

class TravelTimeMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    Timecop.travel(TravelTime.take) do
      @app.call(env)
    end
  end
end

Rails::Application.subclasses.first.class_eval do
  if ENV['TRAVEL_TIME']
    require 'timecop'

    config.middleware.use TravelTimeMiddleware

    Timecop.travel(TravelTime.take) # For Rake tasks, console and other similar processes

    # Uncomment the code below if you use Delayed Job as a queue in your application
    # class Delayed::Worker
    #   alias origin_run run
    #
    #   def run(job)
    #     Timecop.travel(TravelTime.take) do
    #       origin_run(job)
    #     end
    #   end
    # end
  end
end

为了使用这段代码,只需把它放到一个本地初始化器中。创建一个文本文件,里面有一些与Rails兼容的格式的时间。这是在测试应用程序时应该前往的时间。比如说:

echo "Fri, 19 Jun 2017 10:46:52 EDT -04:00" > travel_time

然后设置环境变量TRAVEL_TIME ,指向这个文件:

export TRAVEL_TIME=travel_time

这就是了。现在,当你运行rails服务器或rails控制台时,当前时间将被设置为 "19 Jun 2017 10:46:52"。将当前时间定义在一个文件中,可以在任何时候改变它,并且它将被更新,而不需要重新启动rails服务器(注意,控制台应该被重新启动)。如果您在您的应用程序中使用延迟作业,并希望在其工作者中模拟当前时间,请取消注释代码。与延迟工作相关的代码类似,可以为其他队列编写类似的代码。

在该片段中,使用了一个中间件。如果您对这个概念不熟悉,请参考此指南

总结

在这篇文章中,你已经看到了如何在Rails应用程序中模拟当前时间而不改变操作系统的设置。这样就可以避免很多可能出现的关键问题。此外,它还描述了一些有用的本地初始化器的技巧,在不同的情况下都很有用。