This guide describes the comprehensive testing infrastructure and patterns used in Cabriolet for testing all archive formats.
Overview
Cabriolet uses a three-layer testing approach:
-
Unit Tests: Test individual classes (Parser, Compressor, Decompressor)
-
Integration Tests: Test components working together with real fixtures
-
CLI Tests: Test command-line interface with real file operations
The testing infrastructure emphasizes: * Real behavior over mocks: Tests use actual fixture files and verify real outcomes * DRY patterns: RSpec’s let, context, subject, and its eliminate duplication * Fixture registry: Centralized fixture access via Fixtures.for(:format) * Meaningful assertions: Tests verify actual behavior (files created, content correct)
Fixture Infrastructure
Fixture Registry
The fixture registry provides centralized access to all format fixtures:
require_relative "../support/fixtures"
# Access CAB fixtures
basic_cab = Fixtures.for(:cab).path(:basic)
bad_signature = Fixtures.for(:cab).edge_case(:bad_signature)
# Access CHM fixtures
chm_office = Fixtures.for(:chm).path(:excel_vba)
# Get fixtures for a test scenario
split_cabs = Fixtures.for(:cab).scenario(:split)Fixture Organization
Fixtures are organized by format in spec/support/fixtures/:
-
cab_fixtures.rb- CAB cabinet files -
chm_fixtures.rb- CHM (HTML Help) files -
szdd_fixtures.rb- SZDD compressed files -
kwaj_fixtures.rb- KWAJ compressed files -
hlp_fixtures.rb- WinHelp files -
lit_fixtures.rb- LIT eBook files -
oab_fixtures.rb- OAB (Outlook Address Book) files
Each fixture module provides:
-
FILEShash - Named fixtures for common test cases -
EDGE_CASEShash - Security and corruption test files -
path(name)- Get absolute path to a named fixture -
edge_case(name)- Get absolute path to an edge case fixture -
scenario(name)- Get fixtures for a specific test scenario
Test Patterns
DRY Test Organization
Use RSpec’s let, context, subject, and its to eliminate repetition:
RSpec.describe Cabriolet::CAB::Parser do
let(:io_system) { Cabriolet::System::IOSystem.new }
let(:parser) { described_class.new(io_system) }
describe "#parse" do
context "with basic cabinet" do
let(:fixture) { Fixtures.for(:cab).path(:basic) }
subject(:parsed) { parser.parse(fixture) }
it { expect { parsed }.not_to raise_error }
its(:file_count) { is_expected.to eq(2) }
its(:folder_count) { is_expected.to eq(1) }
describe "cabinet structure" do
it { is_expected.to be_a(Cabriolet::Models::Cabinet) }
its(:length) { is_expected.to be > 0 }
end
end
end
endKey patterns:
-
let(:fixture)- Define reusable fixture path -
subject(:parsed)- Define the subject under test -
its(:attribute) { is_expected.to… }- Test attributes on subject -
describeblocks - Group related tests -
contextblocks - Describe different scenarios
Testing Real Behavior
Tests should verify actual outcomes, not just "no errors":
# BAD - Only tests no error
it "extracts files" do
expect { cli.extract(fixture, output_dir) }.not_to raise_error
end
# GOOD - Verifies actual behavior
it "extracts all files to output directory" do
Dir.mktmpdir do |output_dir|
cli.extract(fixture, output_dir)
extracted_files = Dir.glob("#{output_dir}/**/*").select { |f| File.file?(f) }
expect(extracted_files.length).to eq(2)
end
endTesting with Fixtures
Use the fixture registry to access test files:
# Standard fixtures
context "with basic cabinet" do
let(:fixture) { Fixtures.for(:cab).path(:basic) }
# ...
end
# Edge cases
context "with corrupted header" do
let(:fixture) { Fixtures.for(:cab).edge_case(:partial_shortheader) }
# ...
end
# Test scenarios
context "with split cabinets" do
Fixtures.for(:cab).scenario(:split).each_with_index do |fixture, i|
let(:split_fixture) { fixture }
it "parses split cabinet #{i + 1}" do
cabinet = parser.parse(split_fixture)
expect(cabinet.file_count).to be >= 0
end
end
endTesting CLI Commands
CLI tests verify real behavior without mocks:
RSpec.describe Cabriolet::CLI, "CAB commands" do
let(:cli) { described_class.new }
def invoke_command(command, *args, options: {})
cli.options = Thor::CoreExt::HashWithIndifferentAccess.new(options)
cli.public_send(command, *args)
end
describe "#extract" do
it "extracts all files from cabinet to output directory" do
Dir.mktmpdir do |output_dir|
invoke_command(:extract, basic_fixture, output_dir)
extracted_files = Dir.glob("#{output_dir}/**/*").select { |f| File.file?(f) }
expect(extracted_files.length).to eq(2)
end
end
end
endKey principles for CLI tests:
-
Test the CLI class methods directly (not spawned processes)
-
Verify actual file operations (files created, content correct)
-
Use
Dir.mktmpdirfor temporary test directories -
Test error cases (non-existent files, invalid signatures)
Format-Specific CLI Tests
Each format has dedicated CLI command tests:
CHM Commands (chm_command_spec.rb)
Tests CHM (HTML Help) CLI commands: * #chm_info - Display CHM file information * #chm_extract - Extract CHM content to directory * #chm_list - List files in CHM archive
HLP Commands (hlp_command_spec.rb)
Tests HLP (WinHelp) CLI commands: * #hlp_info - Display HLP file information * #hlp_extract - Extract HLP content to directory * #hlp_list - List files in HLP archive
KWAJ Commands (kwaj_command_spec.rb)
Tests KWAJ compressed file CLI commands: * #kwaj_info - Display KWAJ file information * #kwaj_extract - Extract KWAJ content * #kwaj_decompress - Decompress KWAJ files
LIT Commands (lit_command_spec.rb)
Tests LIT (Microsoft eBook) CLI commands: * #lit_info - Display LIT file information * #lit_extract - Extract LIT content to directory * #lit_list - List files in LIT archive
Adding New Fixtures
Adding Standard Fixtures
-
Place the fixture file in the appropriate
spec/fixtures/subdirectory -
Add an entry to the
FILEShash in the format fixture module:
module CabFixtures
FILES = {
existing: "path/to/existing.cab",
new_fixture: "path/to/new_file.cab", # Add this
}.freeze
endAdding Edge Cases
For security or corruption test files, add to EDGE_CASES:
module CabFixtures
EDGE_CASES = {
existing: "path/to/existing.cab",
new_vulnerability: "path/to/cve-XXXX-XXXX.cab", # Add this
}.freeze
endAdding Test Scenarios
For groups of related fixtures, add a scenario method:
def self.scenario(scenario)
case scenario
when :basic
[path(:simple), path(:basic)]
when :all_compression
[path(:mszip), path(:lzx)]
when :new_scenario # Add this
[path(:file1), path(:file2)]
else
raise ArgumentError, "Unknown scenario: #{scenario}"
end
endShared Examples
The edge_case_examples.rb file provides shared RSpec examples for common edge cases:
RSpec.shared_examples "edge case handling" do |format|
context "with non-existent file" do
it "raises IOError" do
parser = parser_for_format(format)
expect { parser.parse("/nonexistent.#{format}") }
.to raise_error(Cabriolet::IOError)
end
end
end
# Use in format-specific tests
RSpec.describe Cabriolet::CAB::Parser do
it_behaves_like "edge case handling", :cab
endFormat Test Helper
The format_test_helper.rb module provides common test scenarios:
include FormatTestHelper
# Test list command
test_list_command(:cab, basic_fixture)
# Test extract with expected count
test_extract_command(:cab, basic_fixture, expected_count: 2)
# Test API parser
test_api_parser(:cab, basic_fixture)
# Test API decompressor
test_api_decompressor(:cab, basic_fixture)Test Categories
Unit Tests (spec/{format}/)
Each format has its own spec directory following the same pattern:
-
parser_spec.rb- Tests parsing format files -
compressor_spec.rb- Tests creating format files -
decompressor_spec.rb- Tests extracting format files
Formats with unit tests: * spec/cab/ - CAB cabinet files * spec/chm/ - CHM (HTML Help) files * spec/hlp/ - HLP (WinHelp) files * spec/kwaj/ - KWAJ compressed files * spec/lit/ - LIT eBook files * spec/oab/ - OAB (Outlook Address Book) files * spec/szdd/ - SZDD compressed files
Note: OAB format has no parser class, only compressor/decompressor.
Integration Tests (spec/cli/)
-
cab_command_spec.rb- Tests CAB CLI commands -
chm_command_spec.rb- Tests CHM CLI commands -
hlp_command_spec.rb- Tests HLP CLI commands -
kwaj_command_spec.rb- Tests KWAJ CLI commands -
lit_command_spec.rb- Tests LIT CLI commands -
oab_command_spec.rb- Tests OAB CLI commands -
szdd_command_spec.rb- Tests SZDD CLI commands
Test Coverage Goals
For each format, tests should cover:
-
Parsing: Valid files, invalid signatures, edge cases
-
Extraction: Standard extraction, corrupted files, different compression types
-
Creation: Basic archives, compression options, multi-file archives
-
CLI Commands: list, extract, info, test, create (where applicable)
-
Edge Cases: CVE security files, truncated files, malformed data
Running Tests
Run all tests:
bundle exec rspecRun specific test files:
bundle exec rspec spec/cab/parser_spec.rbRun with documentation format:
bundle exec rspec --format documentationRun with focused examples:
bundle exec rspec --format documentation --focusBest Practices
Continuous Integration
The test suite runs on CI/CD pipelines. Ensure:
-
All tests pass before committing
-
New fixtures are documented in
spec/fixtures/README.adoc -
New test scenarios follow the established patterns
-
Tests are MECE (Mutually Exclusive, Collectively Exhaustive)