Back to Repositories

Testing Cassette Serializer Implementation in VCR

This test suite validates the serializer functionality in VCR’s cassette system, ensuring proper data serialization and deserialization across multiple formats including YAML, JSON, and compressed data.

Test Coverage Overview

The test suite comprehensively covers VCR’s serializer implementations across multiple formats.

  • Tests YAML, JSON, Syck, Psych, and compressed serializers
  • Validates encoding error handling for each serializer
  • Verifies syntax error handling and proper error messages
  • Tests custom serializer implementation

Implementation Analysis

The testing approach uses RSpec shared examples to ensure consistent behavior across different serializers.

  • Implements shared_examples for common serializer behaviors
  • Tests lazy loading mechanisms for serializers
  • Validates file extension handling
  • Verifies serialization/deserialization of complex hash structures

Technical Details

The test suite utilizes several technical components:

  • RSpec as the testing framework
  • Multiple YAML engines (Psych, Syck)
  • JSON serialization library
  • Zlib compression for compressed serializer
  • Custom Ruby serializer implementation

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • DRY principle through shared example groups
  • Comprehensive error case handling
  • Proper isolation of test cases
  • Clear test organization and naming
  • Thorough edge case coverage

vcr/vcr

spec/lib/vcr/cassette/serializers_spec.rb

            
require 'support/ruby_interpreter'
require 'vcr/cassette/serializers'
require 'json'

begin
  require 'psych' # ensure psych is loaded for these tests if its available
rescue LoadError
end

module VCR
  class Cassette
    ::RSpec.describe Serializers do
      shared_examples_for "encoding error handling" do |name, error_class|
        context "the #{name} serializer" do
          it 'appends info about the :preserve_exact_body_bytes option to the error' do
            expect {
              result = serializer.serialize("a" => string)
              serializer.deserialize(result)
            }.to raise_error(error_class, /preserve_exact_body_bytes/)
          end
        end
      end

      shared_examples_for "syntax error handling" do |name, error_class|
        context "the #{name} serializer" do
          it 'appends info about the :erb option to the error' do
            expect {
              begin
                serialized
              rescue => e
                e.message << "\n#{serializer.serialize("a" => "bc").inspect}"
                raise
              end
              serializer.deserialize(serialized)
            }.to raise_error(error_class, /erb/)
          end
        end
      end

      shared_examples_for "a serializer" do |name, file_extension, lazily_loaded|
        let(:serializer) { subject[name] }

        context "the #{name} serializer" do
          it 'lazily loads the serializer' do
            serializers = subject.instance_variable_get(:@serializers)
            expect(serializers).not_to have_key(name)
            expect(subject[name]).not_to be_nil
            expect(serializers).to have_key(name)
          end if lazily_loaded

          it "returns '#{file_extension}' as the file extension" do
            expect(serializer.file_extension).to eq(file_extension)
          end

          it "can serialize and deserialize a hash" do
            hash = { "a" => 7, "null value" => nil, "nested" => { "hash" => [1, 2, 3] }}
            serialized = serializer.serialize(hash)
            expect(serialized).not_to eq(hash)
            expect(serialized).to be_a(String)
            deserialized = serializer.deserialize(serialized)
            expect(deserialized).to eq(hash)
          end
        end
      end

      it_behaves_like "a serializer", :yaml,  "yml",  :lazily_loaded do
        it_behaves_like "encoding error handling", :yaml, ArgumentError do
          let(:string) { "\xFA".force_encoding("UTF-8") }
          before { ::YAML::ENGINE.yamler = 'psych' if defined?(::YAML::ENGINE) }
        end if ''.respond_to?(:encoding)

        it_behaves_like "syntax error handling", :yaml, ::Psych::SyntaxError do
          let(:serialized) { <<~YAML }
            ---
            a: <%=
              'bc'.to_json
            %>
          YAML
          before { ::YAML::ENGINE.yamler = 'psych' if defined?(::YAML::ENGINE) }
        end
      end

      it_behaves_like "a serializer", :syck,  "yml",  :lazily_loaded do
        it_behaves_like "encoding error handling", :syck, ArgumentError do
          let(:string) { "\xFA".force_encoding("UTF-8") }
        end if ''.respond_to?(:encoding)

        it_behaves_like "syntax error handling", :syck, ::Psych::SyntaxError do
          let(:serialized) { <<~YAML }
            ---
            a: <%=
              'bc'.to_json
            %>
          YAML
        end
      end

      it_behaves_like "a serializer", :psych, "yml",  :lazily_loaded do
        it_behaves_like "encoding error handling", :psych, ArgumentError do
          let(:string) { "\xFA".force_encoding("UTF-8") }
        end if ''.respond_to?(:encoding)

        it_behaves_like "syntax error handling", :psych, ::Psych::SyntaxError do
          let(:serialized) { <<~YAML }
            ---
            a: <%=
              'bc'.to_json
            %>
          YAML
        end
      end if defined?(::Psych)

      it_behaves_like "a serializer", :compressed, "zz",  :lazily_loaded do
        it_behaves_like "encoding error handling", :compressed, ArgumentError do
          let(:string) { "\xFA".force_encoding("UTF-8") }
        end if ''.respond_to?(:encoding)

        it_behaves_like "syntax error handling", :compressed, ::Psych::SyntaxError do
          let(:serialized) { Zlib::Deflate.deflate(<<~YAML) }
            ---
            a: <%=
              'bc'.to_json
            %>
          YAML
        end
      end

      it_behaves_like "a serializer", :json,  "json", :lazily_loaded do
        it_behaves_like "encoding error handling", :json, ::JSON::GeneratorError do
          let(:string) { "\xFA" }
        end

        it_behaves_like "syntax error handling", :json, ::JSON::ParserError do
          let(:serialized) { <<~JSON }
            {
              "a": <%=
                'bc'.to_json
              %>
            }
          JSON
        end
      end

      context "a custom :ruby serializer" do
        let(:custom_serializer) do
          Object.new.tap do |obj|
            def obj.file_extension
              "rb"
            end

            def obj.serialize(hash)
              hash.inspect
            end

            def obj.deserialize(string)
              eval(string)
            end
          end
        end

        before(:each) do
          subject[:ruby] = custom_serializer
        end

        it_behaves_like "a serializer", :ruby, "rb", false
      end

      describe "#[]=" do
        context 'when there is already a serializer registered for the given name' do
          before(:each) do
            subject[:foo] = :old_serializer
            allow(subject).to receive :warn
          end

          it 'overrides the existing serializer' do
            subject[:foo] = :new_serializer
            expect(subject[:foo]).to be(:new_serializer)
          end

          it 'warns that there is a name collision' do
            expect(subject).to receive(:warn).with(
              /WARNING: There is already a VCR cassette serializer registered for :foo\. Overriding it/
            )
            subject[:foo] = :new_serializer
          end
        end
      end

      describe "#[]" do
        it 'raises an error when given an unrecognized serializer name' do
          expect { subject[:foo] }.to raise_error(ArgumentError)
        end

        it 'returns the named serializer' do
          expect(subject[:yaml]).to be(VCR::Cassette::Serializers::YAML)
        end
      end

      # see https://gist.github.com/815769
      problematic_syck_string = "1\n \n2"

      describe "psych serializer" do
        it 'serializes things using pysch even if syck is configured as the default YAML engine' do
          ::YAML::ENGINE.yamler = 'syck'
          serialized = subject[:psych].serialize(problematic_syck_string)
          expect(subject[:psych].deserialize(serialized)).to eq(problematic_syck_string)
        end if defined?(::Psych) && defined?(::YAML::ENGINE)

        it 'raises an error if psych cannot be loaded' do
          expect { subject[:psych] }.to raise_error(LoadError)
        end unless defined?(::Psych)
      end
    end
  end
end