Documents
Golden File Testing
Golden File Testing
Type
Topic
Status
Published
Created
Feb 27, 2026
Updated
Apr 19, 2026
Created by
Dosu Bot
Updated by
Dosu Bot

Golden File Testing#

Overview#

Golden file testing (also known as snapshot testing) is a software testing technique that validates program output by comparing it against pre-approved reference files. In the opnDossier project, golden file testing is implemented using sebdah/goldie/v2 to ensure consistency in markdown report generation, JSON/YAML exports, and HTML diff outputs.

The pattern works by generating output from test input, comparing it against stored "golden" reference files, and regenerating the reference only when changes are intentional. This approach is particularly valuable for programmatic markdown generation, where output validation across multiple formats ensures consistency and catches unintended changes in report structure.

A key design principle in opnDossier's implementation is that dynamic values (timestamps, versions) are injected at construction time via builder options (builder.WithGeneratedTime, builder.WithVersion), producing deterministic output. This eliminates the need for post-hoc normalization and allows goldie to compare bytes directly, while golden files remain human-readable with consistent fixed values.

Core Implementation#

Test File Structure#

The opnDossier project implements golden file testing across three primary test files:

Table-Driven Test Pattern#

The implementation follows a consistent table-driven test pattern with test case structs:

type goldenTestCase struct {
    name string
    dataFile string
    comprehensive bool
    goldenFile string
}

Test cases are defined in helper functions and iterated through in the main test:

func TestGolden_ProgrammaticReportGeneration(t *testing.T) {
    testCases := goldenTestCases()

    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            // Generate report...
            g := newGoldie(t)
            g.Assert(t, tc.goldenFile, []byte(output))
        })
    }
}

Goldie Configuration#

Each output format has a dedicated goldie instance creator with custom configuration:

func newGoldie(t *testing.T) *goldie.Goldie {
    t.Helper()
    return goldie.New(
        t,
        goldie.WithFixtureDir("testdata/golden"),
        goldie.WithNameSuffix(".golden.md"),
        goldie.WithDiffEngine(goldie.ColoredDiff),
    )
}

The configuration specifies the fixture directory location, file suffix, and colored diff output for failures. No custom equality function is needed because deterministic output is achieved by injecting fixed values at builder construction time.

Builder Options for Deterministic Output#

Injecting Fixed Values#

The MarkdownBuilder supports construction-time options for injecting deterministic values:

func createDeterministicBuilder(t *testing.T) *builder.MarkdownBuilder {
    t.Helper()

    return builder.NewMarkdownBuilder(
        builder.WithGeneratedTime(time.Date(2026, 1, 2, 15, 4, 5, 0, time.UTC)),
        builder.WithVersion("test"),
    )
}

The WithGeneratedTime and WithVersion options override the default time.Now() and constants.Version values, producing byte-for-byte reproducible output. This eliminates the need for regex-based normalization functions and allows goldie to compare golden files directly.

Golden File Content#

Golden files contain fixed, human-readable values:

- **Generated On**: 2026-01-02T15:04:05Z
- **Parsed By**: opnDossier vtest

These fixed values are injected by the test builder and remain consistent across all test runs, making golden files both deterministic and readable.

Format-Specific Considerations#

JSON/YAML tests use simpler whitespace normalization:

func normalizeSerializedOutput(output []byte) []byte {
    result := strings.ReplaceAll(string(output), "\r\n", "\n")
    result = strings.TrimRight(result, "\n\t ")
    return []byte(result)
}

No timestamp injection is needed for JSON/YAML because the export functions don't inject time-based values into these outputs. The normalization focuses solely on platform-independent line endings and trailing whitespace.

Golden File Organization#

Directory Structure#

Golden files are stored in internal/converter/testdata/golden/ with a consistent naming convention:

internal/converter/testdata/
├── golden/
│ ├── complete_comprehensive.golden.md
│ ├── complete_redacted.golden.json
│ ├── complete_redacted.golden.yaml
│ ├── complete_standard.golden.md
│ ├── complete_unredacted.golden.json
│ ├── complete_unredacted.golden.yaml
│ ├── edge_cases_comprehensive.golden.md
│ ├── edge_cases_standard.golden.md
│ ├── minimal_standard.golden.md
│ └── ...
├── complete.json
├── edge_cases.json
└── minimal.json

Naming Conventions#

The project uses a structured naming pattern that encodes test scenario information:

  1. Base name identifies the test data source: minimal, complete, edge_cases
  2. Mode suffix indicates report type: _standard, _comprehensive, _redacted, _unredacted
  3. Extension indicates format: .golden.md, .golden.json, .golden.yaml

This naming scheme makes it easy to identify which test data and configuration generated each golden file, and enables systematic regeneration when needed.

Golden File Regeneration#

Update Flag Usage#

The -update flag is documented in test comments and automatically handled by goldie:

# Update golden files when output changes intentionally
go test -v ./internal/converter -run TestGolden -update

When run with -update, goldie writes the actual output to golden files instead of comparing them. This allows developers to review changes via git diff before committing.

Regeneration Workflows#

Intentional feature changes:

  1. Make changes to generation code
  2. Run tests without -update to review expected changes
  3. Regenerate golden files with -update flag
  4. Review changes with git diff testdata/*.golden
  5. Commit with clear message describing the output change

Shared function updates:

When shared rendering functions are modified, regenerate golden files across ALL affected formatters. Changes to shared functions like goldmark configuration in internal/markdown/ require regenerating golden files for all formatters that depend on them.

Markdown Validation and Round-Trip Testing#

ValidateMarkdown Function#

The core validation function in internal/markdown/formatters.go uses goldmark parser:

func ValidateMarkdown(content string) error {
    md := goldmark.New(
        goldmark.WithExtensions(extension.Table),
        goldmark.WithParserOptions(
            goldmark_parser.WithAutoHeadingID(),
        ),
        goldmark.WithRendererOptions(
            html.WithHardWraps(),
        ),
    )

    var buf strings.Builder
    if err := md.Convert([]byte(content), &buf); err != nil {
        return fmt.Errorf("failed to parse markdown content: %w", err)
    }

    return nil
}

This validates markdown by attempting to parse and convert it to HTML. The goldmark configuration includes table extension support and auto-heading ID generation, matching the extensions used in production rendering.

Section-Level Validation Tests#

internal/converter/builder/writer_test.go validates all report sections:

func TestMarkdownBuilder_ValidateMarkdownSyntax(t *testing.T) {
    t.Parallel()

    b := builder.NewMarkdownBuilder()
    data := createTestDocumentWithAllFeatures()

    tests := []struct {
        name string
        generate func() string
    }{
        {
            name: "SystemSection",
            generate: func() string {
                return b.BuildSystemSection(data)
            },
        },
        // ... more sections
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            output := tt.generate()
            err := internalMarkdown.ValidateMarkdown(output)
            if err != nil {
                t.Errorf("%s produced invalid markdown: %v", tt.name, err)
            }
        })
    }
}

This test ensures each section builder produces valid markdown syntax independently, catching issues before they propagate to full reports.

Multi-Parser Round-Trip Testing#

internal/export/file_test.go validates with multiple markdown parsers:

validate: func(t *testing.T, content []byte) {
    t.Helper()
    // Test with multiple markdown parsers

    // 1. Test with goldmark
    err := markdown.ValidateMarkdown(string(content))
    require.NoError(t, err, "Markdown should pass goldmark validation")

    // 2. Test with glamour (terminal markdown renderer)
    _, err = glamour.Render(string(content), "dark")
    require.NoError(t, err, "Markdown should pass glamour validation")

    // 3. Basic markdown structure validation
    contentStr := string(content)
    assert.Contains(t, contentStr, "#", "Markdown should contain headers")
    assert.NotContains(t, contentStr, "\x1b[", "Should not contain ANSI escapes")
}

Testing with multiple parsers ensures the generated markdown is compatible with various rendering engines, not just the one used for validation.

External Linting Tools#

The project uses two external markdown linters:

  • markdownlint-cli2 - Validates heading increments, list style, HTML elements, and more
  • mdformat - Formats and validates markdown with semantic breaks in lists

Both are integrated into pre-commit hooks, providing an additional layer of validation beyond programmatic checks.

Test Assertion Specificity#

Assertion Levels#

Different levels of assertion strictness can be combined for comprehensive testing:

func TestMarkdownBuilder_SpecificAssertions(t *testing.T) {
    device := loadTestConfig("testdata/sample.config.xml")
    builder := converter.NewMarkdownBuilder()
    result := builder.BuildSystemSection(device)

    // Level 1: Content presence (least strict)
    assert.Contains(t, result, "# System Information")
    assert.Contains(t, result, device.System.Hostname)

    // Level 2: Structure validation (moderate)
    lines := strings.Split(result, "\n")
    assert.Greater(t, len(lines), 10, "should have substantial content")

    // Level 3: Golden file (most strict)
    g := goldie.New(t)
    g.Assert(t, t.Name(), []byte(result))
}

Level 1 assertions verify critical content is present but allow formatting flexibility. Level 2 validates structural properties like minimum content size. Level 3 golden file assertions catch any change to the output format, including whitespace and ordering.

Format-Specific Validation#

When testing formatted output, verify the actual format patterns, not just content presence. For example, check for [wan] text in markdown links, #wan-interface fragment identifiers, and , separator patterns in markdown tables. This ensures that formatting changes (like switching from [wan] to wan without brackets) are caught by tests.

Testing Multiline Secret Redaction in Serialized Output#

When testing that sensitive multiline data (such as PEM-encoded private keys) is properly redacted in JSON/YAML serialized output, avoid substring assertions like assert.NotContains(t, jsonStr, rawPEMKey). This pattern is ineffective because:

  • encoding/json escapes embedded newlines as \n
  • yaml.v3 may emit block scalars with indentation
  • The raw multiline string substring won't match the encoded form in the serialized output

Instead, unmarshal the JSON/YAML output back into a typed struct and assert on the parsed field values directly:

// Define a minimal struct matching the serialized structure
type certRedactionReport struct {
    NormalizedConfig struct {
        Certificates []common.Certificate `json:"certificates" yaml:"certificates"`
    } `json:"normalizedConfig" yaml:"normalizedconfig"`
}

func TestReport_CertificatePrivateKeysRedacted(t *testing.T) {
    const rawPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\nSECRET\n-----END RSA PRIVATE KEY-----"

    // ... create report with certificates containing rawPrivateKey ...

    jsonStr, err := report.ToJSON()
    require.NoError(t, err)

    // Unmarshal and verify the parsed structure
    var parsed certRedactionReport
    require.NoError(t, json.Unmarshal([]byte(jsonStr), &parsed))

    assert.Equal(t, "[REDACTED]", parsed.NormalizedConfig.Certificates[0].PrivateKey,
        "cert-001 PrivateKey must be [REDACTED]")
    assert.NotEqual(t, rawPrivateKey, parsed.NormalizedConfig.Certificates[0].PrivateKey,
        "cert-001 must not contain raw private key")
}

This approach validates redaction at the data structure level, catching failures that string-based assertions miss. See TestReport_CertificatePrivateKeysRedacted in internal/processor/report_ids_test.go for a complete implementation.

This pattern is particularly important when verifying that sensitive fields are properly redacted in golden files used for JSON/YAML serialization tests.

Best Practices#

  1. Inject Dynamic Values at Construction Time: Use builder options (e.g., builder.WithGeneratedTime, builder.WithVersion) to produce deterministic output. This eliminates the need for post-hoc normalization and allows goldie to compare bytes directly.
  2. Meaningful Test Names: Use descriptive test names that identify specific scenarios (e.g., "complete_configuration", "edge_case_special_characters")
  3. Review Before Update: Always review -update changes before committing to catch unintended modifications
  4. Combine Validation Methods: Use golden files for format consistency alongside specific assertions for critical content
  5. Document Builder Configuration: Comment why specific builder options are used in test code
  6. Test Data Organization: Keep test fixtures in testdata/ organized by feature/component
  7. Coordinated Regeneration: When shared rendering functions change, regenerate golden files across all dependent formatters

Relevant Code Files#

File PathDescription
internal/converter/golden_test.goPrimary markdown report golden file tests (321 lines)
internal/converter/golden_json_yaml_test.goJSON/YAML serialization golden file tests (197 lines)
internal/diff/formatters/golden_test.goHTML, Markdown, and JSON diff formatter tests (272 lines)
internal/converter/test_helpers.goTest data loading helpers and utilities
internal/markdown/formatters.goValidateMarkdown() function implementation
internal/converter/builder/writer_test.goSection-level markdown validation tests
internal/export/file_test.goMulti-parser round-trip validation tests
internal/converter/html.goShared goldmark renderer configuration
.markdownlint-cli2.jsoncExternal markdown linter configuration
.mdformat.tomlMarkdown formatter configuration
internal/converter/testdata/golden/Golden file storage directory
internal/processor/report_ids_test.goStructural redaction tests for JSON/YAML serialization
  • Snapshot Testing - General pattern of comparing output against reference files
  • Test-Driven Development (TDD) - Development methodology emphasizing tests
  • Regression Testing - Testing approach to detect unintended changes
  • Markdown Parsing and Validation - Techniques for validating markdown syntax
  • Test Fixtures and Testdata Management - Organizing and maintaining test data
  • Table-Driven Tests in Go - Pattern for parameterized testing in Go
Golden File Testing | Dosu