Back to Repositories

Testing TimeZone Serialization and Version Management in PaperTrail

This test suite validates the Person model’s versioning functionality in PaperTrail, focusing on TimeZone attribute serialization and association tracking. It ensures proper version history management and data integrity across model changes.

Test Coverage Overview

The test suite provides comprehensive coverage of TimeZone attribute handling and versioning behavior in the Person model. Key areas tested include:

  • TimeZone object serialization and deserialization
  • Version history tracking for attribute changes
  • Object changes storage optimization
  • Association tracking for cars and bicycles

Implementation Analysis

The testing approach employs RSpec’s context-driven structure to validate PaperTrail’s versioning mechanisms. It uses detailed assertions to verify:

  • Custom TimeZoneSerializer implementation
  • Version object storage format and size constraints
  • Changeset accuracy and consistency
  • Model reification with associations

Technical Details

Testing tools and configuration include:

  • RSpec as the testing framework
  • Custom TimeZoneSerializer for attribute handling
  • ActiveSupport::TimeZone for time zone management
  • YAML for object serialization
  • HashWithIndifferentAccess for version data access

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Isolated test contexts for different scenarios
  • Comprehensive edge case coverage
  • Explicit version attribute verification
  • Clear test organization and naming
  • Thorough validation of serialization/deserialization processes

paper-trail-gem/paper_trail

spec/models/person_spec.rb

            
# frozen_string_literal: true

require "spec_helper"

# The `Person` model:
#
# - has a dozen associations of various types
# - has a custom serializer, TimeZoneSerializer, for its `time_zone` attribute
RSpec.describe Person, type: :model, versioning: true do
  describe "#time_zone" do
    it "returns an ActiveSupport::TimeZone" do
      person = described_class.new(time_zone: "Samoa")
      expect(person.time_zone.class).to(eq(ActiveSupport::TimeZone))
    end
  end

  context "when the model is saved" do
    it "version.object_changes should store long serialization of TimeZone object" do
      person = described_class.new(time_zone: "Samoa")
      person.save!
      len = person.versions.last.object_changes.length
      expect((len < 105)).to(be_truthy)
    end

    it "version.object_changes attribute should have stored the value from serializer" do
      person = described_class.new(time_zone: "Samoa")
      person.save!
      as_stored_in_version = HashWithIndifferentAccess[
        YAML.load(person.versions.last.object_changes)
      ]
      expect(as_stored_in_version[:time_zone]).to(eq([nil, "Samoa"]))
      serialized_value = Person::TimeZoneSerializer.dump(person.time_zone)
      expect(as_stored_in_version[:time_zone].last).to(eq(serialized_value))
    end

    it "version.changeset should convert attribute to original, unserialized value" do
      person = described_class.new(time_zone: "Samoa")
      person.save!
      unserialized_value = Person::TimeZoneSerializer.load(person.time_zone)
      expect(person.versions.last.changeset[:time_zone].last).to(eq(unserialized_value))
    end

    it "record.changes (before save) returns the original, unserialized values" do
      person = described_class.new(time_zone: "Samoa")
      changes_before_save = person.changes.dup
      person.save!
      expect(
        changes_before_save[:time_zone].map(&:class)
      ).to(eq([NilClass, ActiveSupport::TimeZone]))
    end

    it "version.changeset should be the same as record.changes was before the save" do
      person = described_class.new(time_zone: "Samoa")
      changes_before_save = person.changes.dup
      person.save!
      actual = person.versions.last.changeset.delete_if { |k, _v| (k.to_sym == :id) }
      expect(actual).to(eq(changes_before_save))
      actual = person.versions.last.changeset[:time_zone].map(&:class)
      expect(actual).to(eq([NilClass, ActiveSupport::TimeZone]))
    end

    context "when that attribute is updated" do
      it "object should not store long serialization of TimeZone object" do
        person = described_class.new(time_zone: "Samoa")
        person.save!
        person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
        person.save!
        len = person.versions.last.object.length
        expect((len < 105)).to(be_truthy)
      end

      it "object_changes should not store long serialization of TimeZone object" do
        person = described_class.new(time_zone: "Samoa")
        person.save!
        person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
        person.save!
        len = person.versions.last.object_changes.length
        expect(len < 118).to eq(true)
      end

      it "version.object attribute should have stored value from serializer" do
        person = described_class.new(time_zone: "Samoa")
        person.save!
        attribute_value_before_change = person.time_zone
        person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
        person.save!
        as_stored_in_version = HashWithIndifferentAccess[
          YAML.load(person.versions.last.object)
        ]
        expect(as_stored_in_version[:time_zone]).to(eq("Samoa"))
        serialized_value = Person::TimeZoneSerializer.dump(attribute_value_before_change)
        expect(as_stored_in_version[:time_zone]).to(eq(serialized_value))
      end

      it "version.object_changes attribute should have stored value from serializer" do
        person = described_class.new(time_zone: "Samoa")
        person.save!
        person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
        person.save!
        as_stored_in_version = HashWithIndifferentAccess[
          YAML.load(person.versions.last.object_changes)
        ]
        expect(as_stored_in_version[:time_zone]).to(eq(["Samoa", "Pacific Time (US & Canada)"]))
        serialized_value = Person::TimeZoneSerializer.dump(person.time_zone)
        expect(as_stored_in_version[:time_zone].last).to(eq(serialized_value))
      end

      it "version.reify should convert attribute to original, unserialized value" do
        person = described_class.new(time_zone: "Samoa")
        person.save!
        attribute_value_before_change = person.time_zone
        person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
        person.save!
        unserialized_value = Person::TimeZoneSerializer.load(attribute_value_before_change)
        expect(person.versions.last.reify.time_zone).to(eq(unserialized_value))
      end

      it "version.changeset should convert attribute to original, unserialized value" do
        person = described_class.new(time_zone: "Samoa")
        person.save!
        person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
        person.save!
        unserialized_value = Person::TimeZoneSerializer.load(person.time_zone)
        expect(person.versions.last.changeset[:time_zone].last).to(eq(unserialized_value))
      end

      it "record.changes (before save) returns the original, unserialized values" do
        person = described_class.new(time_zone: "Samoa")
        person.save!
        person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
        changes_before_save = person.changes.dup
        person.save!
        expect(
          changes_before_save[:time_zone].map(&:class)
        ).to(eq([ActiveSupport::TimeZone, ActiveSupport::TimeZone]))
      end

      it "version.changeset should be the same as record.changes was before the save" do
        person = described_class.new(time_zone: "Samoa")
        person.save!
        person.assign_attributes(time_zone: "Pacific Time (US & Canada)")
        changes_before_save = person.changes.dup
        person.save!
        expect(person.versions.last.changeset).to(eq(changes_before_save))
        expect(
          person.versions.last.changeset[:time_zone].map(&:class)
        ).to(eq([ActiveSupport::TimeZone, ActiveSupport::TimeZone]))
      end
    end
  end

  describe "#cars and bicycles" do
    it "can be reified" do
      person = described_class.create(name: "Frank")
      car = Car.create(name: "BMW 325")
      bicycle = Bicycle.create(name: "BMX 1.0")

      person.car = car
      person.bicycle = bicycle
      person.update(name: "Steve")

      car.update(name: "BMW 330")
      bicycle.update(name: "BMX 2.0")
      person.update(name: "Peter")

      expect(person.reload.versions.length).to(eq(3))

      # These will work when PT-AT adds support for the new `item_subtype` column
      #
      # - https://github.com/westonganger/paper_trail-association_tracking/pull/5
      # - https://github.com/paper-trail-gem/paper_trail/pull/1143
      # - https://github.com/paper-trail-gem/paper_trail/issues/594
      #
      # second_version = person.reload.versions.second.reify(has_one: true)
      # expect(second_version.car.name).to(eq("BMW 325"))
      # expect(second_version.bicycle.name).to(eq("BMX 1.0"))
    end
  end
end