Back to Repositories

Testing Article Version Management Implementation in PaperTrail

This test suite validates the versioning functionality of the Article model in the PaperTrail gem, focusing on version creation, attribute tracking, and metadata handling. It ensures proper version management across create, update, and destroy operations.

Test Coverage Overview

The test suite provides comprehensive coverage of PaperTrail’s versioning system, examining version creation and management across different scenarios.

  • Version creation during model operations (create, update, destroy)
  • Handling of ignored, selected, and skipped columns
  • Metadata storage and retrieval
  • Changeset validation

Implementation Analysis

The testing approach utilizes RSpec’s behavior-driven development patterns with nested contexts and descriptive scenarios. It implements various test cases using PaperTrail’s versioning configuration options and RSpec’s expectation syntax.

  • Context-based test organization
  • Shared example patterns
  • Dynamic metadata verification
  • Changeset validation patterns

Technical Details

  • RSpec test framework
  • PaperTrail versioning configuration
  • Custom metadata providers
  • Serialization handling
  • Model-specific version tracking

Best Practices Demonstrated

The test suite exemplifies strong testing practices through clear organization and comprehensive coverage of functionality.

  • Isolated test scenarios
  • Descriptive context naming
  • Thorough edge case coverage
  • Consistent expectation patterns
  • Clear setup and teardown procedures

paper-trail-gem/paper_trail

spec/models/article_spec.rb

            
# frozen_string_literal: true

require "spec_helper"

RSpec.describe Article, type: :model, versioning: true do
  describe ".create" do
    it "also creates a version record" do
      expect { described_class.create }.to(
        change { PaperTrail::Version.count }.by(+1)
      )
    end
  end

  context "when updating an ignored column" do
    it "not change the number of versions" do
      article = described_class.create
      article.update(title: "My first title")
      expect(PaperTrail::Version.count).to(eq(1))
    end
  end

  context "when updating an ignored column with truly Proc" do
    it "not change the number of versions" do
      article = described_class.create
      article.update(abstract: "ignore abstract")
      expect(PaperTrail::Version.count).to(eq(1))
    end
  end

  context "when updating an ignored column with falsy Proc" do
    it "change the number of versions" do
      article = described_class.create
      article.update(abstract: "do not ignore abstract!")
      expect(PaperTrail::Version.count).to(eq(2))
    end
  end

  context "when updating an ignored column, ignored with truly Proc and a selected column" do
    it "change the number of versions" do
      article = described_class.create
      article.update(
        title: "My first title",
        content: "Some text here.",
        abstract: "ignore abstract"
      )
      expect(PaperTrail::Version.count).to(eq(2))
      expect(article.versions.size).to(eq(2))
    end

    it "have stored only non-ignored attributes" do
      article = described_class.create
      article.update(
        title: "My first title",
        content: "Some text here.",
        abstract: "ignore abstract"
      )
      expected = { "content" => [nil, "Some text here."] }
      expect(article.versions.last.changeset).to(eq(expected))
    end
  end

  context "when updating an ignored column, ignored with falsy Proc and a selected column" do
    it "change the number of versions" do
      article = described_class.create
      article.update(
        title: "My first title",
        content: "Some text here.",
        abstract: "do not ignore abstract"
      )
      expect(PaperTrail::Version.count).to(eq(2))
      expect(article.versions.size).to(eq(2))
    end

    it "stores only non-ignored attributes" do
      article = described_class.create
      article.update(
        title: "My first title",
        content: "Some text here.",
        abstract: "do not ignore abstract"
      )
      expected = {
        "content" => [nil, "Some text here."],
        "abstract" => [nil, "do not ignore abstract"]
      }
      expect(article.versions.last.changeset).to(eq(expected))
    end
  end

  context "when updating a selected column" do
    it "change the number of versions" do
      article = described_class.create
      article.update(content: "Some text here.")
      expect(PaperTrail::Version.count).to(eq(2))
      expect(article.versions.size).to(eq(2))
    end
  end

  context "when updating a non-ignored and non-selected column" do
    it "not change the number of versions" do
      article = described_class.create
      article.update(abstract: "Other abstract")
      expect(PaperTrail::Version.count).to(eq(1))
    end
  end

  context "when updating a skipped column" do
    it "not change the number of versions" do
      article = described_class.create
      article.update(file_upload: "Your data goes here")
      expect(PaperTrail::Version.count).to(eq(1))
    end
  end

  context "when updating a skipped column and a selected column" do
    it "change the number of versions" do
      article = described_class.create
      article.update(
        file_upload: "Your data goes here",
        content: "Some text here."
      )
      expect(PaperTrail::Version.count).to(eq(2))
    end

    it "show the new version in the model's `versions` association" do
      article = described_class.create
      article.update(
        file_upload: "Your data goes here",
        content: "Some text here."
      )
      expect(article.versions.size).to(eq(2))
    end

    it "have stored only non-skipped attributes" do
      article = described_class.create
      article.update(
        file_upload: "Your data goes here",
        content: "Some text here."
      )
      expect(
        article.versions.last.changeset
      ).to(eq("content" => [nil, "Some text here."]))
    end

    context "when updated again" do
      it "have removed the skipped attributes when saving the previous version" do
        article = described_class.create
        article.update(
          file_upload: "Your data goes here",
          content: "Some text here."
        )
        article.update(
          file_upload: "More data goes here",
          content: "More text here."
        )
        old_article = article.versions.last
        expect(
          PaperTrail.serializer.load(old_article.object)["file_upload"]
        ).to(be_nil)
      end

      it "have kept the non-skipped attributes in the previous version" do
        article = described_class.create
        article.update(
          file_upload: "Your data goes here",
          content: "Some text here."
        )
        article.update(
          file_upload: "More data goes here",
          content: "More text here."
        )
        old_article = article.versions.last
        expect(
          PaperTrail.serializer.load(old_article.object)["content"]
        ).to(eq("Some text here."))
      end
    end
  end

  describe "#destroy" do
    it "creates a version record" do
      article = described_class.create
      article.destroy
      expect(PaperTrail::Version.count).to(eq(2))
      expect(article.versions.size).to(eq(2))
      expect(article.versions.map(&:event)).to(match_array(%w[create destroy]))
    end
  end

  context "with an item" do
    let(:article) { described_class.new(title: initial_title) }
    let(:initial_title) { "Foobar" }

    context "when it is created" do
      before { article.save }

      it "store fixed meta data" do
        expect(article.versions.last.answer).to(eq(42))
      end

      it "store dynamic meta data which is independent of the item" do
        expect(article.versions.last.question).to(eq("31 + 11 = 42"))
      end

      it "store dynamic meta data which depends on the item" do
        expect(article.versions.last.article_id).to(eq(article.id))
      end

      it "store dynamic meta data based on a method of the item" do
        expect(article.versions.last.action).to(eq(article.action_data_provider_method))
      end

      it "store dynamic meta data based on an attribute of the item at creation" do
        expect(article.versions.last.title).to(eq(initial_title))
      end
    end

    context "when it is created, then updated" do
      before do
        article.save
        article.update!(content: "Better text.", title: "Rhubarb")
      end

      it "store fixed meta data" do
        expect(article.versions.last.answer).to(eq(42))
      end

      it "store dynamic meta data which is independent of the item" do
        expect(article.versions.last.question).to(eq("31 + 11 = 42"))
      end

      it "store dynamic meta data which depends on the item" do
        expect(article.versions.last.article_id).to(eq(article.id))
      end

      it "store dynamic meta data based on an attribute of the item prior to the update" do
        expect(article.versions.last.title).to(eq(initial_title))
      end
    end

    context "when it is created, then destroyed" do
      before do
        article.save
        article.destroy
      end

      it "store fixed metadata" do
        expect(article.versions.last.answer).to(eq(42))
      end

      it "store dynamic metadata which is independent of the item" do
        expect(article.versions.last.question).to(eq("31 + 11 = 42"))
      end

      it "store dynamic metadata which depends on the item" do
        expect(article.versions.last.article_id).to(eq(article.id))
      end

      it "store dynamic metadata based on attribute of item prior to destruction" do
        expect(article.versions.last.title).to(eq(initial_title))
      end
    end
  end
end