| Copyright | (c) 2025 |
|---|---|
| License | BSD3 |
| Maintainer | gael@hindsight.events |
| Stability | experimental |
| Safe Haskell | None |
| Language | GHC2021 |
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:
- The event has
instance Event event - All payload versions have
Arbitraryand JSON instances - 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
- createRoundtripTests :: forall (event :: Symbol) -> (Event event, HasFullEvidenceList event ValidTestPayloadForVersion) => TestTree
- createGoldenTests :: forall (event :: Symbol) -> (Event event, HasFullEvidenceList event ValidTestPayloadForVersion) => GoldenTestConfig -> TestTree
- data GoldenTestConfig = GoldenTestConfig {
- goldenPathFor :: forall (event :: Symbol) (ver :: PeanoNat). (Event event, ReifiablePeanoNat ver) => Proxy event -> Proxy ver -> FilePath
- goldenTestCaseCount :: forall a. Num a => a
- goldenTestSeed :: forall a. Num a => a
- goldenTestSizeParam :: forall a. Num a => a
- defaultGoldenTestConfig :: GoldenTestConfig
- class TestPayloadRequirements event idx payload => ValidTestPayloadForVersion (event :: Symbol) (idx :: PeanoNat) payload
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:
- Proper event definition with version informationEventevent- All payload versions haveHasFullEvidenceListevent ValidTestPayloadForVersionArbitraryand 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:
- Proper event definitionEventevent- All versions testableHasFullEvidenceListevent ValidTestPayloadForVersion
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
- Initial setup: Run tests, they fail with "golden file missing"
- Review generated output, commit golden files if correct
- Future changes: If JSON format changes, test fails with diff
- 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: 10goldenTestSeed: Random seed for reproducible generation. Same seed always produces same test cases. Default: 42goldenTestSizeParam: 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
| TestPayloadRequirements event idx payload => ValidTestPayloadForVersion event idx payload Source # | |
Defined in Test.Hindsight.Generate | |