Sometimes it's worth to test your framework features | Arkency Blog

Ask questions Research chat →

https://blog.arkency.com/sometimes-its-worth-to-test-your-framework-features/ · scraped

rails testing

Attachments

Scraped Content

— 626 words · 2026-02-14 17:38:33 UTC ·

Excerpt

![](https://prod-files-secure.s3.us-west-2.amazonaws.com/871f1661-80b8-4d0c-ac3b-2adfc6ff4c66/857abff4-745b-4a94-9b9f-a52987869286/text?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466X7ZXM7OI%2F20260214%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20260214T173833Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEEEaCXVzLXdlc3QtMiJIMEYCIQDS98IV6M%2BITqPKaqrAe2JAfW8KxfytZJl%2BC3awM%2BWs8wIhALYyw1Rzl12OUoSM1k4qLK5fJiVSW8mWQwrEE8hpakcLKv8DCAoQABoMNjM3NDIzMTgzODA1Igzp6440enmdPhLcivYq3AOTU5Ca%2BU5DrbZ8i07bR8TF9hME9E5v1TalJSkS4G4TtabQMg4VkFvfI90%2Be5EP2Z%2FeXNXz9MwkNtyoedTu3UEyfXfnR%2BHpKuaAxutnRqTmbS0ZKAJENWsk%2BmriY49EaTGMdPTHXISHtmE3i4E6eazfZC1f8q23NbUp6Dw3YbWp5J1cRp1SCJEPZSzQzH%2FeEKjrUJ2kVSMNYxX06pTaXLEObGKgA%2BH1nEipk5ntGLGmWT%2B8q29wYOeTx6m%2BdFGw6BqKUdgyrveBU7bLc4koOx9cKEynHXHkvUZxmdEyJikwdpf%2BMZJ9bmQ7BbHqEEhIPr0yX5nE3di0zs96ZZqA33SsdPyxcQ85ESJzyP9yxcyjHmRf1XnlefQ6BDmjaShTgOAq9XGYQ%2Bcm0JzHlOCAQbQLhMUK2VUKmsUrr4ihUVTPdSgqFgqL
![](https://prod-files-secure.s3.us-west-2.amazonaws.com/871f1661-80b8-4d0c-ac3b-2adfc6ff4c66/857abff4-745b-4a94-9b9f-a52987869286/text?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466X7ZXM7OI%2F20260214%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20260214T173833Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEEEaCXVzLXdlc3QtMiJIMEYCIQDS98IV6M%2BITqPKaqrAe2JAfW8KxfytZJl%2BC3awM%2BWs8wIhALYyw1Rzl12OUoSM1k4qLK5fJiVSW8mWQwrEE8hpakcLKv8DCAoQABoMNjM3NDIzMTgzODA1Igzp6440enmdPhLcivYq3AOTU5Ca%2BU5DrbZ8i07bR8TF9hME9E5v1TalJSkS4G4TtabQMg4VkFvfI90%2Be5EP2Z%2FeXNXz9MwkNtyoedTu3UEyfXfnR%2BHpKuaAxutnRqTmbS0ZKAJENWsk%2BmriY49EaTGMdPTHXISHtmE3i4E6eazfZC1f8q23NbUp6Dw3YbWp5J1cRp1SCJEPZSzQzH%2FeEKjrUJ2kVSMNYxX06pTaXLEObGKgA%2BH1nEipk5ntGLGmWT%2B8q29wYOeTx6m%2BdFGw6BqKUdgyrveBU7bLc4koOx9cKEynHXHkvUZxmdEyJikwdpf%2BMZJ9bmQ7BbHqEEhIPr0yX5nE3di0zs96ZZqA33SsdPyxcQ85ESJzyP9yxcyjHmRf1XnlefQ6BDmjaShTgOAq9XGYQ%2Bcm0JzHlOCAQbQLhMUK2VUKmsUrr4ihUVTPdSgqFgqLidEgqc9hqfn3uAVyfdKKJMoJLVmr8iv06GG%2BICHUMc%2FnGBOSpeZl1KuWcN6BCOaYlgSMKNR9rcSwS4iZuvvS2%2FqlWIBPnQVR25rYtNoALOcUyVldP5qHn7m%2BfUGVY76GhKBoKRbb9MLwm7HTADP98floSHFV%2FgKUztqshC0LIVZbD5NKT9um4tlSvVOltupqsYxJ3s4iLDDh0cLMBjqkAV7LkpJvkcO2avErEnerIWgOn7OYGfKayO0y%2Fq75O%2FrCyrela5dnYfcOXL8ND2SSAhCXtJQg1pRErB8YBxbBx50i4X8GVA30o70CSKToXA99v9gcGLhEfmtswG7BFiiQISiPz79s5pSKOXQulSPwSIIlhZUNYRderD46qwS0iScxuFXS%2FnWABJYWrG%2BdJTxzAI3faivMKyt1CC0m%2BOStbm5xYkkh&X-Amz-Signature=2740f213831a802520449cc2c971aadb2a9286d542d3ff63c15f8325ff33b27a&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject) … and check why 5600+ Rails engineers read also this Rails 6 introduced upsert_all which was a great alternative to raw SQL for inserting or updating multiple records at once. There were gems providing this feature for earlier versions of Rails like activerecord-import, it did a great job in Rails Event Store. ## Inconvenience in Rails 6 There was one minor disadvantage, the timestamps columns: created_at and updated_at weren’t updated automatically causing inserts to fail because of NOT NULL constraints in the database. It had to be done manually: ```plain text timestamp = Time.current FancyModel.upsert_all([{ foo: :bar, created_at: timestamp, updated_at: timestamp }], unique_by: [:custom_unique_index]) ``` It worked great for new objects, but not necessarily for the existing ones which were updated. We had found this out while investigating issue in the system. Those records which we knew that were updated had equal created_at and updated_at. We wanted to fix this case, so we started with a test: ```plain text class FancyModelTest < ActiveSupport::TestCase def test_timestampz FancyModel.create!(foo: :bar) timestamp = Time.current FancyModel.upsert_all( [{ foo: :baz, created_at: timestamp, updated_at: timestamp }], unique_by: [:custom_unique_index], ) fancy = FancyModel.find_by!(foo: :baz) assert(fancy.updated_at > fancy.created_at) end end ``` It failed, obviously. ## Rails 7 to the rescue We had few ideas how to fix this. The easiest solution was on the table since we were on Rails 7 already. They can handle timestamps on your behalf unless you disable it. Bad code setting identical timestamp for both columns was removed and ActiveRecord took care of timestamps handling again. Unfortunately, the test was constantly red: ```plain text class FancyModelTest < ActiveSupport::TestCase def test_timestampz FancyModel.create!(foo: :bar) FancyModel.upsert_all([{ foo: :baz }], unique_by: [:custom_unique_index]) fancy = FancyModel.find_by!(foo: :baz) assert fancy.updated_at > fancy.created_at end end ``` ## Too fast for you? What if it happens so fast, that assertion won’t even notice — we thought. Put a sleep(1) on it, make it pass: ```plain text class FancyModelTest < ActiveSupport::TestCase def test_timestampz FancyModel.create!(foo: :bar) sleep(1) FancyModel.upsert_all([{ foo: :baz }], unique_by: [:custom_unique_index]) fancy = FancyModel.find_by!(foo: :baz) assert(fancy.updated_at > fancy.created_at) end end ``` Nope, not gonna happen. ## What about time travel, Marty? Let’s create a record in the past, for sure this will work: ```plain text class FancyModelTest < ActiveSupport::TestCase def test_timestampz travel_to Time.zone.local(1985, 10, 26, 1, 24) do FancyModel.create!(foo: :bar) end FancyModel.upsert_all([{ foo: :baz }], unique_by: [:custom_unique_index]) fancy = FancyModel.find_by!(foo: :baz) assert(fancy.updated_at > fancy.created_at) end end ``` Red. Scratching head, losing faith in own skills moment appears. ## Transactional tests After digging throughout the Rails code, we had intuition that updated_at not being set to a different value might have something in common with the fact that tests are wrapped in a database transaction. Transaction is rolled back at the end of the test case to make every other test independent from each other We created a separate example not using transactions to prove our hypothesis: ```plain text class FancyModelTest < ActiveSupport::TestCase self.use_transactional_tests = false def test_timestampz FancyModel.create!(foo: :bar) FancyModel.upsert_all([{ foo: :baz }], unique_by: [:custom_unique_index]) fancy = FancyModel.find_by!(foo: :baz) assert(fancy.updated_at > fancy.created_at) end end ``` Green. ## We know the answer It turned out that PostgreSQL CURRENT_TIMESTAMP returns time at the start of the transaction (in our case the test–wrapping one). There’s no chance that created_at and updated_at will differ from each other after running upsert_all within the test. As PostgreSQL docs state: Since these functions return the start time of the current transaction, their values do not change during the transaction. This is considered a feature: the intent is to allow a single transaction to have a consistent notion of the „current” time, so that multiple modifications within the same transaction bear the same time stamp. NOW() in MySQL does the same. Have a look in a Rails codebase if you’re curious how CURRENT_TIMESTAMP is utilised.

Visibility

Visible to everyone

Reading Status

Related Bookmarks

My Note


Saved!

Annotations

Export as Markdown
+ Annotate selection

Add Annotation