hindsight-core:event-testing
Copyright(c) 2025
LicenseBSD3
Maintainergael@hindsight.events
Stabilityexperimental
Safe HaskellNone
LanguageGHC2021

Test.Hindsight.Generate

Description

Problem

Events need tests to verify JSON serialization works correctly (roundtrip tests) and to catch unintended format changes (golden tests). When events have multiple versions, you need these tests for every version.

Writing these tests manually is repetitive. With an event having versions v0, v1, v2, you write essentially the same test three times. Across many events, this becomes tedious and error-prone.

Solution

This module automatically generates test suites for all versions of an event. The type system ensures exhaustiveness—if you add version v3 but forget its instances, compilation fails.

Usage

First, define your event with all required instances:

type UserCreated = "user_created"

-- Payload versions
data UserInfo0 = UserInfo0 { userId :: Int, userName :: Text }
    deriving (Show, Eq, Generic, FromJSON, ToJSON)

data UserInfo1 = UserInfo1 { userId :: Int, userName :: Text, email :: Maybe Text }
    deriving (Show, Eq, Generic, FromJSON, ToJSON)

data UserInfo2 = UserInfo2 { userId :: Int, userName :: Text, email :: Maybe Text, isActive :: Bool }
    deriving (Show, Eq, Generic, FromJSON, ToJSON)

-- Version declarations
type instance MaxVersion UserCreated = 2
type instance Versions UserCreated = '[UserInfo0, UserInfo1, UserInfo2]

instance Event UserCreated

-- Upcast chain
instance Upcast 0 UserCreated where
    upcast (UserInfo0 uid name) = UserInfo1 uid name Nothing

instance Upcast 1 UserCreated where
    upcast (UserInfo1 uid name email) = UserInfo2 uid name email True

-- Migration instances
instance MigrateVersion 0 UserCreated  -- v0 → v1 → v2
instance MigrateVersion 1 UserCreated  -- v1 → v2
instance MigrateVersion 2 UserCreated  -- v2 → v2 (identity)

-- Arbitrary instances for testing
instance Arbitrary UserInfo0 where arbitrary = UserInfo0 <$> arbitrary <*> arbitrary
instance Arbitrary UserInfo1 where arbitrary = UserInfo1 <$> arbitrary <*> arbitrary <*> arbitrary
instance Arbitrary UserInfo2 where arbitrary = UserInfo2 <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary

Then generate tests:

import Test.Hindsight.Generate

tests :: TestTree
tests = testGroup Events
    [ createRoundtripTests UserCreated
    , createGoldenTests UserCreated defaultGoldenTestConfig
    ]

This generates 6 tests total: roundtrip tests for v0, v1, v2 and golden tests for v0, v1, v2.

Type-Level Machinery

Both functions require the constraint HasFullEvidenceList event ValidTestPayloadForVersion, which the type system automatically satisfies when:

  1. The event has instance Event event
  2. All payload versions have Arbitrary and JSON instances
  3. All version migrations are properly defined

If any version lacks required instances, you get a compile error pointing to the missing constraint.

When to Use What

  • Roundtrip tests: Always. These verify basic serialization correctness.
  • Golden tests: For stable APIs or when JSON format matters to external systems. Changes to golden files are explicit in diffs, making unintended format changes visible in code review.
Synopsis

Roundtrip Tests

createRoundtripTests :: forall (event :: Symbol) -> (Event event, HasFullEvidenceList event ValidTestPayloadForVersion) => TestTree Source #

Generate roundtrip tests for all versions of an event.

Creates a test group containing property-based roundtrip tests for every version of the specified event. Each test verifies that JSON serialization is reversible: decode (encode x) ≡ Just x.

Type Requirements

The event type must satisfy:

  • Event event - Proper event definition with version information
  • HasFullEvidenceList event ValidTestPayloadForVersion - All payload versions have Arbitrary and JSON instances (automatically derived)

Example

-- In your test suite
import Test.Hindsight.Generate
import Test.Hindsight.Examples (UserCreated)

tests :: TestTree
tests = testGroup "Event Tests"
    [ createRoundtripTests UserCreated
    ]

This generates tests like:

  • UserCreated Roundtrip Tests / Version 0 roundtrip
  • UserCreated Roundtrip Tests / Version 1 roundtrip
  • UserCreated Roundtrip Tests / Version 2 roundtrip

When to Use

Always include these tests. They catch basic serialization bugs and verify that your JSON instances work correctly for all versions.

Golden Tests

createGoldenTests :: forall (event :: Symbol) -> (Event event, HasFullEvidenceList event ValidTestPayloadForVersion) => GoldenTestConfig -> TestTree Source #

Generate golden tests for all versions of an event.

Creates snapshot tests that compare generated JSON output against committed golden files. When the output changes (intentionally or not), the test fails with a diff, making format changes explicit in code review.

Type Requirements

Same as createRoundtripTests:

Example

import Test.Hindsight.Generate
import Test.Hindsight.Examples (UserCreated)

tests :: TestTree
tests = testGroup "Event Tests"
    [ createRoundtripTests UserCreated
    , createGoldenTests UserCreated config
    ]
  where
    config = defaultGoldenTestConfig
        { goldenPathFor = \(_ :: Proxy event) (_ :: Proxy ver) ->
            "golden" / "events" / getEventName event / show (reifyPeanoNat @ver) <> ".json"
        , goldenTestCaseCount = 10
        , goldenTestSeed = 12345
        , goldenTestSizeParam = 30
        }

This generates tests like:

  • UserCreated Golden Tests / Version 0 golden
  • UserCreated Golden Tests / Version 1 golden
  • UserCreated Golden Tests / Version 2 golden

Each test compares generated JSON against its golden file (e.g., golden/events/user_created/0.json).

When to Use

Use golden tests when:

  • JSON format stability matters (external APIs, stored events)
  • You want to catch unintended format changes in code review
  • The event structure is relatively stable

Skip them for rapidly evolving internal events where format changes are frequent.

Workflow

  1. Initial setup: Run tests, they fail with "golden file missing"
  2. Review generated output, commit golden files if correct
  3. Future changes: If JSON format changes, test fails with diff
  4. Review diff: If change is intentional, update golden file

data GoldenTestConfig Source #

Configuration for golden test generation.

Golden tests compare generated JSON output against committed files. This config controls generation parameters and file paths.

Fields

  • goldenPathFor: Maps event type and version to golden file path. Default: "test/golden/events/<event-name>/<version>.json"
  • goldenTestCaseCount: Number of sample values to generate per version. More samples = better coverage but larger golden files. Default: 10
  • goldenTestSeed: Random seed for reproducible generation. Same seed always produces same test cases. Default: 42
  • goldenTestSizeParam: QuickCheck's size parameter controlling complexity. Higher values generate larger/deeper structures. Default: 30

Example Customization

myConfig = defaultGoldenTestConfig
    { goldenPathFor = \(_ :: Proxy event) (_ :: Proxy ver) ->
        "test" / "snapshots" / getEventName event / show (reifyPeanoNat @ver) <> ".json"
    , goldenTestCaseCount = 5   -- Fewer samples for faster tests
    , goldenTestSeed = 99999    -- Different seed for variation
    }

Constructors

GoldenTestConfig 

Fields

defaultGoldenTestConfig :: GoldenTestConfig Source #

Default golden test configuration.

Provides sensible defaults for most projects:

  • Golden files in test/golden/events/<event-name>/<version>.json
  • 10 test cases per version
  • Seed 42 for reproducibility
  • Size parameter 30 (moderate structural complexity)

Customize by overriding specific fields:

createGoldenTests @UserCreated $ defaultGoldenTestConfig
    { goldenTestCaseCount = 5  -- Override just the count
    }

Technical Details

Internal constraint machinery. You typically don't need to use this directly—it's automatically satisfied when your payload types have the required instances (FromJSON, ToJSON, Eq, Show, Typeable, Arbitrary).

This is exported primarily for:

  • Understanding compiler error messages that mention ValidTestPayloadForVersion
  • Advanced use cases requiring explicit constraint manipulation

If you see a compiler error mentioning this constraint, it means one of your payload versions is missing a required instance.

class TestPayloadRequirements event idx payload => ValidTestPayloadForVersion (event :: Symbol) (idx :: PeanoNat) payload Source #

Type class providing evidence that a payload is valid for testing a specific event version.

This class uses a blanket instance - any type satisfying TestPayloadRequirements automatically gets this evidence. The class exists to participate in the constraint machinery that walks through all event versions.

Users don't need to write instances manually; they're derived automatically when payloads have the required JSON and Arbitrary instances.

Instances

Instances details
TestPayloadRequirements event idx payload => ValidTestPayloadForVersion event idx payload Source # 
Instance details

Defined in Test.Hindsight.Generate