【问题标题】:RSpec: How to mock SQL NOW()RSpec:如何模拟 SQL NOW()
【发布时间】:2018-05-13 15:14:12
【问题描述】:

我可以用出色的timecop gem 模拟Time.now

Time.now
 => 2018-05-13 18:04:46 +0300

Timecop.travel(Time.parse('2018.03.12, 12:00'))

Time.now
 => 2018-03-12 12:00:04 +0300

TeacherVacation.first.ends_at
Thu, 15 Mar 2018 12:00:00 MSK +03:00

TeacherVacation.where('ends_at > ?', Time.now).count
1

但是(显然)这在查询中使用 NOW() 时不起作用:

TeacherVacation.where('ends_at > NOW()').count
0

我可以模拟NOW(),让它在一段时间内返回结果吗?

【问题讨论】:

  • 您是在谈论在数据库中创建自己的NOW 函数吗?如果是,您使用的是哪个 DBMS?
  • Timecop.freeze 可以工作吗?
  • Timecop.freeze 也模拟了 ruby​​ 方法 Time.now
  • 如果您正在谈论的是有一个名为NOW() 的自己的函数,而有一个具有相同名称的 NATIVE 函数,这称为 OVERLOADING 并且,对于据我所知,并非每种语言都支持这种东西。由于您标记了SQL,我会补充说(很可能)这不受支持。就像你尝试创建一个名为NOW() 的函数一样,你会收到一条错误消息。不过,我不能说我 100% 确定(尽管我认为我离那里并不太远)。
  • 我知道 Postgres 允许重载,但是你需要一个可以切换功能的环境变量之类的东西,或者只是创建你自己的函数,比如 MY_NOW 来决定使用哪个。据我所知,您不能在 SQL 中使用环境变量之类的东西。

标签: sql ruby-on-rails rspec mocking


【解决方案1】:

Timecop 是一个很棒的宝石!我建议使用Timecop.freeze 而不是为您的实例旅行;你想保持你的测试是确定性的。

据我所知,似乎没有办法模拟 SQL 的函数。 Postgres 等一些语言允许重载函数,但您仍然需要一种插入方式,而且似乎没有一种方法可以在 SQL 中使用环境变量。

一位同事似乎确信您实际上可以放弃系统/语言功能并制作自己的功能,但我担心在您这样做之后如何恢复它们。尝试走那条路听起来很痛苦。

解决方案?

这是我今天在解决这个问题时提出的几个“解决方案”。注意:说实话,我并不真正关心他们,但如果测试到位¯\_(ツ)_/¯ 他们至少提供了一种让事情“工作”的方法。

不幸的是,在 SQL 中没有时髦的 gem 来控制时间。我想你会需要一些疯狂的东西,比如数据库插件、黑客、钩子、中间人、一个容器,你可以欺骗 SQL 使其认为系统时间是别的东西。不幸的是,这些 hack 想法都不会是可移植/平台无关的。

显然有一些方法可以set time in a docker container,但这对于本地测试来说听起来像是一个痛苦的开销,并且不适合要设置的每个测试时间的粒度。

另外需要注意的是,对我来说,我们正在运行大型复杂的原始 SQL 查询,这就是为什么当我运行 SQL 文件进行测试时我可以有正确的日期很重要,否则我只能通过 activerecord 来完成就像你提到的那样。

字符串插值

我在一些正在运行的大型查询中遇到了这个问题。 如果您需要推送一些环境变量,这肯定会有所帮助,并且您可以根据需要注入自己的“current_date”。如果您需要在多个查询中利用特定时间,这也会有所帮助。

my_query.rb
<<~HEREDOC
  SELECT *
  FROM #{@prefix}.my_table
  WHERE date < #{@current_date} - INTERVAL '5 DAYS'
HEREDOC
sql_runner.rb
class SqlRunner
  def initialize(file_path)
    @file_path = file_path
    @prefix = ENV['table_prefix']
    @current_date = Date.today
  end

  def run
    execute(eval(File.read @file_path))
  end

  private

  def execute(sql)
    ActiveRecord::Base.connection.execute(sql)
  end
end

肮脏的更新

这个想法是从 ruby​​ land 更新值,将您的“时间复制”时间推送到数据库中,以覆盖 SQL DB 生成的值。您可能需要对时间的更新进行创意,例如查询大于给定时间的时间,该时间不以您将要更新行的 timecop 时间为目标。

我不关心这种方法的原因是因为它最终感觉你只是在测试 activerecord 的功能,因为你不依赖数据库来设置它应该设置的值。您可能在 SQL 中有计算,然后在测试中重新创建以将某些值设置为正确的日期,然后您不再在 SQL 中进行计算,因此您甚至没有实际测试它。 large_insert.sql

INSERT INTO some_table (
  name,
  created_on
)
SELECT
  name,
  current_date
FROM projects
JOIN people ON projects.id = people.project_id
insert_spec.rb
describe 'insert_test.sql' do
  ACTUAL_DATE = Date.today
  LARGE_INSERT_SQL = File.read('sql/large_insert.sql')

  before do
    Timecop.freeze Date.new(2018, 10, 28)
  end

  after do
    Timecop.return
  end

  context 'populated same_table' do
    before do
      execute(LARGE_INSERT_SQL)
      mock_current_dates(ACTUAL_DATE)
    end

    it 'has the right date' do
      expect(SomeTable.last.created_on).to eq(Date.parse('2018.10.28')
    end
  end

  def execute(sql_command)
    ActiveRecord::Base.connection.execute(sql_command)
  end

  def mock_current_dates(actual_date)
    rows = SomeTable.where(created_on: actual_date)
    # Use our timecop datetime
    rows.update_all(created_on: Date.today)
  end

有趣的警告:规范包含在自己的事务中(您可以将其关闭,但这是一个不错的功能),因此如果您的 SQL 中有事务,您需要编写代码将其删除以获取规范,或者让您的跑步者在需要时将您的代码包装在事务中。它们会运行,但随后您的 SQL 将终止规范事务,您将度过一段糟糕的时光。如果你在测试期间走清理路线,你可以创建一个spec/support 来帮助解决这个问题,如果我在一个较新的项目中,我会编写一个运行器,如果你需要它们,将查询包装在事务中——甚至尽管这在 SQL 文件#abstraction 中并不明显。

也许有一些东西可以让你设置系统时间,但修改系统的实际时间听起来很可怕。

【讨论】:

    【解决方案2】:

    我认为解决方案是 DI(依赖注入)

    def NOW(time = Time.now)
      time
    end
    

    测试中

    current_test = Time.new(2018, 5, 13)
    p NOW(current_test)
    

    生产中

    p NOW
    

    【讨论】:

    • 我不太明白你的回答。 NOW() 不是 ruby​​ 函数,它是 SQL 函数。
    猜你喜欢
    • 2021-04-03
    • 1970-01-01
    • 1970-01-01
    • 2013-07-06
    • 2012-12-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-12-26
    相关资源
    最近更新 更多