作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
Maciek Rząsa的头像

Maciek Rząsa

知识共享倡导者, engineer, 和Scrum Master, 马切克在研究分布式系统, NLP, 编写重要的软件.

Share

测试的目的是防止应用程序不稳定. 但有时,测试本身也会变得不可靠——即使是最简单的测试. 下面是我们如何深入到一个有问题的测试 Ruby on Rails PostgreSQL支持的应用程序,以及我们发现的.

我们想要检查某个业务逻辑(由某个方法调用) perform)不会改变 calendar 模型(的实例) Calendar(一个Ruby on Rails的ActiveRecord模型类),所以我们写:

Let (:calendar) {create(:calendar)}
specify do
  expect do
    执行# call业务操作
    calendar.reload
  end
    .Not_to change(日历,:attributes)
end

这是在一个开发环境(MacOS)中通过的。, 但它在CI (Linux)中几乎总是失败。.

Fortunately, 我们设法在另一个开发环境(Linux)上复制了它。, 它失败的地方是:

expected `Calendar#attributes` not to have changed, but did change from {"calendar_auth_id"=>8,
"created_at"=>2020-01-02 13:36:22.459149334 +0000, "enabled"=>false, "events_...t_sync_token"=>nil,
"title"=>nil, "updated_at"=>2020-01-02 13:36:22.459149334 +0000, "user_id"=>100} to {
"calendar_auth_id"=>8, "created_at"=>2020-01-02 13:36:22.459149000 +0000, "enabled"=>false,
"events_...t_sync_token"=>nil, "title"=>nil, "updated_at"=>2020-01-02 13:36:22.459149000 +0000, "user_id"=>100}

看到可疑的东西了吗??

调查

仔细检查后,我们发现 created_at and updated_at 的内部对时间戳进行了轻微更改 expect block:

{"created_at"=>2020-01-02 13:36:22.459149334 +0000, "updated_at"=>2020-01-02 13:36:22.459149334 +0000}
{"created_at"=>2020-01-02 13:36:22.459149000 +0000, "updated_at"=>2020-01-02 13:36:22.459149000 +0000}

秒的小数部分被截断,这样 13:36:22.459149334 became 13:36:22.459149000.

我们确信 perform 没有更新 calendar 对象,因此我们形成了一个假设,即时间戳被数据库截断了. 为了测试这一点,我们使用了已知的最先进的调试技术.e., 将调试:

Let (:calendar) {create(:calendar)}
specify do
  expect do
    在perform: #{calendar之前放".created_at.to_f}"
    perform
    在perform: #{calendar后面放".created_at.to_f}"
    calendar.reload
    重新加载后:#{日历.created_at.to_f}"
  end
    .Not_to change(日历,:attributes)
end

但是在输出中看不到截断:

执行前:1577983568.550754
执行后:1577983568.550754
重载后:1577983568.550754

这是相当令人惊讶的访问器 #created_at 应该有相同的值作为属性哈希值 属性(“created_at”). 为了确保输出的值与断言中使用的值相同,我们改变了访问的方式 created_at.

而不是使用访问器 calendar.created_at.to_f,我们切换到直接从属性哈希中获取: calendar.属性(“created_at”).to_f. 我们对 calendar.reload were confirmed!

执行前:1577985089.0547702
执行后:1577985089.0547702
重载后:1577985089.05477

如你所见,打电话 perform didn’t change created_at, but reload did.

以确保更改不会发生在另一个实例上 calendar 得救之后,我们又做了一个实验. We reloaded calendar 考试前:

让(:calendar){创建(:calendar).reload }
specify do
  expect do
    perform
    calendar.reload
  end
    .Not_to change(日历,:attributes)
end

这使得测试结果是绿色的.

The Fix

知道是数据库截断了我们的时间戳并使我们的测试失败, 我们决定防止截断的发生. We generated a DateTime 对象,并将其舍入为整秒. 然后,我们使用这个对象显式地设置Rails的活动记录时间戳. 此更改修复并稳定了测试:

let(:time) { 1.day.ago.round }
Let (:calendar) {create(:calendar, created_at: time, updated_at: time)}

specify do
  expect do
    perform
    calendar.reload
  end
    .Not_to change(日历,:attributes)
end

The Cause

为什么会发生这种情况?? 活动记录时间戳为 由Rails设置的 ActiveRecord::时间戳 module using Time.now. Time precision is OS-dependent如文件所述 可能包括小数秒.

We tested Time.now 在MacOS和Linux上使用一个脚本来计算小数部分长度的频率:

pry> 10000.times.map { Time.now.to_f.to_s.match(/\.(\d+)/)[1].size }.group_by{| |一个}.映射{|k, v| [k, v.count]}.to_h

# MacOS => {6=>6581, 7=>2682, 5=>662, 4=>67, 3=>7, 2=>1}
# Linux => {6=>2399, 7=>7300, 5=>266, 4=>32, 3=>3}

As you can see, Linux上大约70%的时间戳在小数点后有7位精度, 而在MacOS上只有25%. 这就是为什么测试在MacOS上大多数时候通过,而在Linux上大多数时候失败的原因. 您可能已经注意到,测试输出具有九位数的精度—这是因为RSpec uses Time#nsec 格式化时间输出.

当Rails模型保存到数据库时, 它们拥有的任何时间戳都使用PostgreSQL中的一种名为 没有时区的时间戳, which has 微秒的决议—i.e.,小数点后六位. So when 1577987974.6472975 发送到PostgreSQL,它截断小数部分的最后一个数字,而不是保存 1577987974.647297.

The Questions

这其中的原因还是个问题 calendar.created_at 在我们调用时没有重新加载 calendar.reload, even though calendar.属性(“created_at”) was reloaded.

同样,结果 Time 精度测试有点令人惊讶. 我们期望在MacOS上,最大精度是6. 我们不知道为什么 sometimes 有七位数. 更让我们惊讶的是最后几位数的分布:

pry> 10000.times.map { Time.now}.map{|t| t.to_f.to_s.match(/\.(\d+)/)[1] }.select{|s| s.size == 7}.group_by {| | e e [1]}.映射{|k, v| [k, v.size]}.to_h

# MacOS => {"9"=>536, "1"=>555, "2"=>778, "8"=>807}
# Linux => {"5"=>981, "1"=>311, "3"=>1039, "9"=>309, "8"=>989, "6"=>1031, "2"=>979, "7"=>966, "4"=>978}

正如您所看到的,MacOS上的第七个数字总是1、2、8或9.

如果你知道这些问题的答案,请与我们分享一个解释.

The Future

事实上,Ruby on Rails Active Record 在应用程序端生成的时间戳也可能对保存到数据库的事件进行可靠和精确的排序造成损害. 由于应用服务器时钟可能不同步,事件排序由 created_at 可能以不同于实际发生的顺序出现. To get 更可靠的行为,最好让数据库服务器处理时间戳(例如.g., PostgreSQL’s now()).

然而,这是一个值得另写一篇文章的故事.


特别感谢 Gabriele Renzi 感谢您帮助创建这篇文章.

了解基本知识

  • 什么是Ruby中的ActiveRecord?

    ActiveRecord是Ruby on Rails提供的一个对象-关系映射库. 它允许您在数据库中持久化对象,然后检索保存的数据并实例化对象.

  • 什么是活动记录实现?

    活动记录是一种企业架构模式,用于在关系数据库中持久化内存中的对象. 在Ruby on Rails的实现中,ActiveRecord监视Rails模型属性的变化. 当保存模型时,ActiveRecord将更改以及所需的时间戳发送到数据库.

  • 时间戳为什么重要??

    ActiveRecord默认存储两个时间戳:created_at(模型第一次保存的时间)和updated_at(模型最后保存的时间). 它们提供了基本的审计功能,并允许应用程序从最新或最后更新的模型开始排序.

  • PostgreSQL中的时间戳是什么?

    时间戳是一种PostgreSQL数据类型,它同时存储日期和时间. 它具有一微秒的分辨率和精度, 这意味着它可以将秒数保持在六位数以内.

  • Ruby on Rails使用什么数据库?

    Ruby on Rails允许开发人员使用大多数流行的数据库. By default, Ruby on Rails使用ActiveRecord库, 它支持DB2, Firebird, FrontBase, MySQL, OpenBase, Oracle, PostgreSQL, SQLite, Microsoft SQL Server, and Sybase.

就这一主题咨询作者或专家.
预约电话

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal开发者

加入总冠军® community.