Hedgehog for .NET

repository·master·Indexed 43 days ago

https://github.com/hedgehogqa/fsharp-hedgehog

A property-based testing library for .NET that provides powerful generators and automated shrinking to find and minimize edge cases. Compatible with F# and C#, it features support for async properties, auto-generation for records and discriminated unions, and a composable API built around Gen<T>, Range<T>, and Property<T>.

Tokens
23.3K
Snippets
76
Records
103
Agent score
54%

What's inside Hedgehog

  1. What is Hedgehog for .NET

    master
    Hedgehog is a property-based testing library for .NET that explores a wide range of inputs to find edge cases. When a test fails, Hedgehog automatically performs 'shrinking' to reduce the failing input to the smallest, most minimal valid example that still triggers the failure. It is designed to preserve invariants by construction during the shrinking process. Hedgehog is compatible with any .NET testing framework and can be used in F#, C#, or other .NET languages.
  2. What is Property-Based Testing with Hedgehog?

    master

    Property-based testing allows you to describe invariants (properties) that should hold true for all possible inputs, rather than writing individual test cases with specific values. Hedgehog automatically generates hundreds of random test cases to attempt to falsify these properties.

    For example, instead of testing reverse with [1, 2, 3], you define the property: reverse (reverse xs) = xs for all lists xs.

    property {
        let! xs = Gen.list (Range.linear 0 100) Gen.alpha
        return List.rev (List.rev xs) = xs
    }
    |> Property.render
  3. How AutoGenConfig layering and priority works

    master

    Hedgehog merges AutoGenConfig settings from multiple levels. When merging, generators at more specific levels override those from outer levels. The priority order (from lowest to highest) is:

    1. Default generators: Hedgehog's built-in generators for common types.
    2. PropertiesAttribute level: Configurations applied to all properties in a class/module.
    3. PropertyAttribute level: Configurations for individual property tests.
    4. GenAttribute level: Generators for specific parameters (highest priority).

    Layering Example (F#)

    type SmallInts = 
        static member __ = 
            AutoGenConfig.empty 
            |> AutoGenConfig.addGenerator (Gen.int32 (Range.constant 1 10))
    
    type WithStrings = 
        static member __ = 
            AutoGenConfig.empty 
            |> AutoGenConfig.addGenerator (Gen.string (Range.constant 5 10) Gen.alpha)
    
    [<Properties(typeof<SmallInts>)>]
    module ``Layering example`` =
        
        [<Property>]
        let ``uses class-level config`` (x: int) = 
            x >= 1 && x <= 10
        
        [<Property(typeof<WithStrings>)>]
        let ``merges class and method config`` (x: int) (s: string) = 
            x >= 1 && x <= 10 && s.Length >= 5 && s.Length <= 10
  4. How async support works in Hedgehog

    master

    Hedgehog property testing operates in two distinct phases:

    1. Generation Phase: The tree of test values (including shrinks) is built synchronously. This ensures reproducible random generation via proper seed threading.
    2. Execution Phase: The property is evaluated. When using checkAsync or reportAsync, this phase is asynchronous and non-blocking.

    Crucial Warning: Blocking during Generation If you use the result of an asynchronous operation to determine the parameters of a subsequent generator, Hedgehog must block the synchronous generation phase to resolve that value. This negates the benefits of async execution.

    Example of what to avoid (Blocking):

    property {
        let! x = Gen.int32 (Range.linear 1 100)
        let! asyncValue = async { return 42 } 
        let! y = Gen.int32 (Range.linear asyncValue 100) // BLOCKS here to resolve asyncValue
        return y > 0
    }
  5. Use Inverse/Roundtrip Properties for reversible operations

    master

    An Inverse (or Roundtrip) property verifies that an operation can be undone. If you perform an operation and then its inverse, you should return to the original state.

    Common Examples:

    • Serialize $\rightarrow$ Deserialize = identity
    • Encode $\rightarrow$ Decode = identity
    • Compress $\rightarrow$ Decompress = identity
    • Add $\rightarrow$ Subtract = identity
    open Hedgehog
    open Hedgehog.FSharp
    open System.Text.Json
    open Swensen.Unquote
    
    [<Fact>]
    let ``Serialize should roundtrip when deserializing`` () =
        property {
            let! person = Gen.auto<Person>
            let json = JsonSerializer.Serialize(person)
            let restored = JsonSerializer.Deserialize<Person>(json)
            test <@ restored = person @>
        }
        |> Property.check
  6. The Mental Shift: From Examples to Properties

    master

    Property-based testing (PBT) shifts the focus from testing specific values to testing universal truths. Instead of verifying that add(1, 2) == 3, you verify that for any two integers a and b, add(a, b) == add(b, a) (commutativity).

    Core Principles:

    • Properties over Examples: Seek universal truths that hold for all valid inputs.
    • Generative Thinking: Consider the entire space of possible inputs (empty collections, max/min values, Unicode, etc.).
    • Shrinking: When a test fails, Hedgehog automatically finds the minimal failing case to simplify debugging.
    • Specification by Properties: Use properties as executable documentation of how your code behaves.
  7. Best practice: Use AutoGenConfig.empty instead of AutoGenConfig.defaults

    master

    When creating custom AutoGenConfig instances, always start with AutoGenConfig.empty.

    Why? AutoGenConfig.defaults includes generators for all built-in types. If you use defaults and then add a custom generator at a higher layer (like a Property level), the default generator from the defaults object might override your custom one during the merge process. Starting with empty ensures you only include the generators you explicitly add, as the default layer is already applied by Hedgehog automatically.

  8. Core Property Testing Patterns

    master

    When designing property-based tests, use these common patterns to define what your code should satisfy:

    PatternWhat It Tests
    InvariantsProperties that always hold about the result (e.g., "Sorting twice = sorting once")
    Business RulesDomain constraints that must never be violated (e.g., "Discount never exceeds price")
    Inverse/RoundtripOperations that undo each other (e.g., "Serialize then deserialize = identity")
    IdempotenceApplying N times = applying once (e.g., "Normalize(Normalize(x)) = Normalize(x)")
    OracleCompare against a reference/known truth (e.g., "Custom sort = standard library sort")
    MetamorphicHow output changes with input transformations (e.g., "Double inputs → double output")
    Model-BasedGenerate valid output first, derive input that should produce it
  9. Avoid overlapping and redundant properties

    master

    To minimize maintenance burden, ensure your properties are distinct and non-overlapping. Use the following elimination workflow:

    1. Discovery: List all candidate properties.
    2. Analysis: Examine relationships between them.
    3. Elimination: Remove redundancy using these criteria:
      • If property A failing implies property B must also fail, keep only the stronger one.
      • If property A is a special case of B, keep B.
      • If they express the same invariant with different wording, keep the clearest one.
    4. Proposal: Present the minimal set of properties.
  10. Use Oracle Properties to compare against a reference implementation

    master

    An Oracle property compares your implementation against a known 'truth'—such as a standard library function, a mathematical formula, or a simpler (but slower) legacy implementation.

    Common Examples:

    • Custom sort should match List.sort.
    • sqrt(x)^2 should approximately equal x.
    • Custom parser should match a standard parser.
    open Hedgehog
    open Hedgehog.FSharp
    open Swensen.Unquote
    
    [<Fact>]
    let ``Custom sort should match standard sort`` () =
        property {
            let! list = Gen.list (Range.linear 0 100) Gen.int32
            let custom = myCustomSort list
            let standard = List.sort list
            test <@ custom = standard @>
        }
        |> Property.check
  11. Register Custom Generator Classes

    master

    You can provide fine-grained control by creating a custom generator class containing public static methods. When you register this class using AutoGenConfig.addGenerators<T> (F#) or .AddGenerators<T>() (C#), Hedgehog will automatically discover and use these methods when it encounters the corresponding return type.

    Method Requirements

    • Must be public static.
    • Must return Gen<T> where T is the target type.
    • For generic types (e.g., Gen<List<T>>), the method can accept parameters like Gen<T> or IAutoGenContext.

    F# Implementation

    type MyGenerators = 
        static member Email() : Gen<Email> = ... 
        static member DateTime() : Gen<DateTime> = ...
    
    let config = 
        AutoGenConfig.defaults 
        |> AutoGenConfig.addGenerators<MyGenerators>
    
    let! user = Gen.autoWith<User> config

    C# Implementation

    public class MyGenerators {
        public static Gen<Email> Email() => ...
        public static Gen<DateTime> DateTime() => ...
    }
    
    var config = AutoGenConfig.Defaults.AddGenerators<MyGenerators>();
    
    var userGen = Gen.AutoWith<User>(config);
    type MyGenerators = 
        static member Email() : Gen<Email> = 
            gen {
                let! name = Gen.string (Range.linear 3 10) Gen.alphaNum
                let! domain = Gen.item ["com"; "net"; "org"]
                return Email $"{name}@example.{domain}"
            }
        
        static member DateTime() : Gen<DateTime> = 
            gen {
                let! days = Gen.int32 (Range.linear 0 365)
                return DateTime.Now.AddDays(-float days)
            }
    
    let config =
        AutoGenConfig.defaults
        |> AutoGenConfig.addGenerators<MyGenerators>
  12. Use Invariant Properties to verify what never changes

    master

    An Invariant is a property that always holds about the result, regardless of the input. Use this when certain aspects of your data must be preserved by an operation.

    Common Examples:

    • Sorting preserves the count and contents of a list.
    • Mapping over a list preserves its length.
    • Filtering never increases the size of a collection.
    • String trimming never increases the string length.
    open Hedgehog
    open Hedgehog.FSharp
    open Swensen.Unquote
    
    [<Fact>]
    let ``Sort should preserve all elements`` () =
        property {
            let! list = Gen.list (Range.linear 0 100) Gen.int32
            let sorted = List.sort list
            test <@ List.length sorted = List.length list @>
            test <@ List.forall (fun x -> List.contains x sorted) list @>
        }
        |> Property.check