Skip to main content

Command Palette

Search for a command to run...

How to Use Meta-Tests to Future-Proof Your Business Logic

Updated
2 min read

You can write simple tests to ensure future changes in your codebase don’t silently break existing business logic.

Take this example: your application allows users to copy a product. Some associations should be copied (like category mappings), while others should not (like customer comments).

class Product < ApplicationRecord
  has_many :category_mappings, dependent: :destroy
  has_many :customer_comments, dependent: :destroy


  def copy
    Product.transaction do
      copy = dup.save!

      category_mappings.find_each do |mapping|
        copy.category_mappings.create!(mapping.attributes.except('id', 'product_id', 'created_at', 'updated_at'))
      end

      copy
    end
  end
end

When implementing such a feature, you'll review each existing association and decide whether it should be copied. But when a new association is added later, it’s easy to forget to update the copy logic.

You can prevent this by adding a meta-test that fails whenever a new association hasn’t been explicitly accounted for:

require "test_helper"

class ProductTest < ActiveSupport::TestCase
  test "all associations have been accounted for in copy logic" do
    accounted_associations = %i[category_mappings customer_comments]
    unaccounted_associations = Product.reflect_on_all_associations.map(&:name) - accounted_associations

    assert unaccounted_associations.empty?,
      "Please account whether these associations should be included in Product#copy \
       and then add them to the accounted_associations variable of this test: 
       #{unaccounted_associations.join(', ')}"
  end
end

Now, if someone adds for example has_many :variants to Product without updating the test, it fails, reminding them to make an intentional decision.

This pattern applies anywhere you maintain an explicit list of elements that define behavior.

For instance, you can add similar tests to ensure all attributes or states are consciously handled:

  • API exposure: every attribute of a model is either exposed or explicitly hidden

  • Audit logging: every field change that matters is intentionally logged

  • Exports: CSV or JSON exports always include all relevant fields