Expecto

repository·main·Indexed 20 days ago

https://github.com/haf/expecto

An advanced, parallel, and async testing library for F# that treats tests as first-class values. It supports unit, stress, and property-based testing, featuring integration with FsCheck for property tests and BenchmarkDotNet for performance testing. The library provides a comprehensive Expect module for assertions, support for test fixtures, theory tests, and the ability to filter or shuffle tests. It can be installed via NuGet templates or Paket and integrates with Visual Studio through the Expecto.VisualStudio.TestAdapter.

Tokens
6.1K
Snippets
20
Records
28
Agent score
23%

What's inside Expecto

  1. How test parallelism affects logging and state

    main

    Expecto runs tests in parallel by default. This has two major implications:

    1. State Management: Avoid using global variables, global singletons, or mutating code in your tests. If you must use them, you must sequence your tests (using --sequenced) or use locks.
    2. Logging: Standard printfn and Console.X functions are not thread-safe and will result in interleaved output. To log correctly within the test context, use Expecto.Logging.

    Thread-safe logging example:

    open Expecto.Logging
    open Expecto.Logging.Message
    
    let logger = Log.create "MyTests"
    
    testCase "reading prop" <| fun () ->
      let subject = MyComponent()
      logger.info(
        eventX "Has prop {property}"
        >> setField "property" subject.property)
      Expect.equal subject.property "Goodbye" "Should have goodbye as its property"
  2. Mark tests as Pending or Focused

    main

    Expecto allows you to control which tests run during a session:

    Pending Tests (Skipped)

    To prevent tests from running, prefix the test name with p (e.g., ptestCase, ptestList, ptestTheory). If using reflection-based discovery, use the [<PTests>] attribute.

    Focused Tests (Only these run)

    To run only a specific subset of tests, prefix the name with f (e.g., ftestCase, ftestList, ftestTheory). If using reflection-based discovery, use the [<FTests>] attribute.

    CI Tip: Use the CLI argument --fail-on-focused-tests in your build scripts to ensure developers don't accidentally commit focused tests to your repository.

    open Expecto
    
    // Focused test
    [<FTests>]
    let someFocusedTest = test "will run" { Expect.equal (2+2) 4 "2+2" }
    
    // Pending test
    [<PTests>]
    let skippedTest = testCase "skipped" <| fun () -> Expect.equal (2+2) 4 "2+2"
  3. Perform relative performance testing with `isFasterThan`

    main

    Expecto allows you to assert that one implementation is faster than another using statistical testing (Welch's t-test).

    Usage Patterns

    1. Simple Comparison: Use Expect.isFasterThan (unit -> 'a) (unit -> 'a) string to compare two functions.
    2. Setup/Teardown Comparison: Use Expect.isFasterThanSub when you need to perform setup or teardown (like resetting a buffer) before the measurement. It accepts a function with the signature Performance.Measurer<unit,'a> -> 'a.
    3. Sequenced Tests: Performance tests should be wrapped in testSequenced to ensure they are handled correctly by the runner.

    Important Notes

    • The functions being compared must return the same result for the same input.
    • The test is relative and runs quickly; it increases sample sizes until the difference is statistically significant (rejecting the null hypothesis with < 0.01% probability).
    • If performance is very close, it declares them equal with 99.99% confidence if they differ by less than 0.5%.
    [<Tests>]
    let performance =
      testSequenced <| testList "performance" [
        testCase "half is faster" <| fun () ->
          Expect.isFasterThan (fun () -> repeat10000 log 76.0)
                              (fun () -> repeat10000 log 76.0 |> ignore; repeat10000 log 76.0)
                              "half is faster"
      ]
  4. Quickstart with Expecto template

    main

    To quickly set up a new Expecto project using the official .NET template, use the following commands in your terminal:

    1. Install the template: dotnet new install "Expecto.Template::*"
    2. Create a new project: dotnet new expecto -n PROJECT_NAME -o FOLDER_NAME

    Replace PROJECT_NAME and FOLDER_NAME with your desired names.

    dotnet new install "Expecto.Template::*"
    dotnet new expecto -n PROJECT_NAME -o FOLDER_NAME
  5. Run Expecto tests from code or command line

    main

    You can execute tests directly from your F# code using Tests.runTestsInAssemblyWithCLIArgs or via the command line using dotnet run or dotnet watch. When running from the command line, use -- to pass arguments to the underlying application.

    Command line examples:

    # Run with help
    dotnet run -p Expecto.Tests -f net10.0 -c release -- --help
    
    # Run with 256 colors using dotnet watch
    dotnet watch -p Expecto.Tests run -f net10.0 -c release -- --colours 256
    Tests.runTestsInAssemblyWithCLIArgs [Stress 0.1;Stress_Timeout 0.2] [||]
  6. Configure custom email notifications on test failure

    main

    Expecto's printing mechanism is based on the Logary Facade. You can extend this to send notifications (e.g., via email using Mailgun) whenever tests fail. This requires initializing the Logary Facade with an Expecto logger and configuring Logary targets in your main function.

    Example setup with Mailgun:

    open Logary
    open Logary.Configuration
    open Logary.Adapters.Facade
    open Logary.Targets
    open Hopac
    open Mailgun
    open System.Net.Mail
    
    let main argv = 
      let mgc = MailgunLogaryConf.Create(
          MailAddress("travis@example.com"),
          [ MailAddress("Your.Mail.Here@example.com") ],
          { apiKey = "deadbeef-2345678" },
          "example.com", 
          Error)
    
      use logary = 
        withLogaryManager "MyTests" (
          withTargets [
            LiterateConsole.create LiterateConsole.empty "stdout"
            Mailgun.create mgc "mail"
          ]
          >> withRules [
            Rule.createForTarget "stdout"
            Rule.createForTarget "mail"
          ])
        |> run
    
      LogaryFacadeAdapter.initialise<Expecto.Logging.Logger> logary
      Tests.runTestsInAssemblyWithCLIArgs [] argv
  7. Install Expecto via Paket

    main

    If you are using Paket for dependency management, add the following lines to your paket.dependencies file to include Expecto and its common companion libraries:

    • Expecto: Core testing library.
    • Expecto.BenchmarkDotNet: For performance testing.
    • Expecto.FsCheck: For property-based testing.
    • Expecto.Hopac: For advanced logging integration.
    nuget Expecto
    nuget Expecto.BenchmarkDotNet
    nuget Expecto.FsCheck
    nuget Expecto.Hopac
  8. Install the Expecto .NET Template from NuGet

    main

    To use the Expecto template via NuGet, install the template into your .NET CLI and then create a new project using the expecto short name. Ensure you specify the language as F# to avoid potential CLI issues.

    dotnet new -i Expecto.Template
    dotnet new expecto -n PROJECT_NAME -o FOLDER_NAME -lang F#
  9. Install the Expecto .NET Template locally

    main

    If you prefer to install the template from a local directory instead of NuGet:

    1. Download the repository.
    2. Install the template by pointing dotnet new -i to the folder path containing the template.
    3. Create the project using the expecto template. Note that due to a known issue in the .NET CLI, the -lang F# parameter is currently required.
    4. Restore dependencies and run the project.
    dotnet new -i PATH
    dotnet new expecto -n PROJECT_NAME -o FOLDER_NAME -lang F#
    dotnet restore
    dotnet run
  10. Migration guide for Expecto 11.0.0

    main

    When upgrading to version 11.0.0, note the following breaking changes:

    • Test SDK Compatibility: Expecto 11.0.0-alpha5 breaks compatibility with YoloDev.Expecto.TestSdk <= 0.14.3. You must use YoloDev.Expecto.TestSdk >= 0.15 for VisualStudio, Rider, dotnet test, and other vstest adapter systems.
    • FsCheck Replay Config: Usages of the replay config (or stdGen with etestProperty* functions) must be updated to use uint64 by appending UL to literals. They also now require a third item indicating the size.
      • Old format: (1865288075, 296281834)
      • New format: (1865288075UL, 296281834UL, Some 3)
      • FsCheck 2 note: The size is ignored in FsCheck 2; you can use None (e.g., (1865288075UL, 296281834UL, None)).
    • FsCheck Version: Expecto now uses FsCheck 3 by default. If you require FsCheck 2, use the versioned package [11.0.0-fscheck2](https://www.nuget.org/packages/Expecto.FsCheck/11.0.0-alpha1-fscheck2).
  11. Configure Logary for prettified stacktraces and test logs

    main

    To enable complete logging solutions, stacktrace highlighting, and the ability to ship build logs, add Logary.Adapters.Facade (prerelease) to your project.

    In your test entry point, initialize the Logary facade adapter before invoking the Expecto test runner. This ensures that your application logs and Expecto's test output are unified and not interlaced during concurrent execution.

    open Hopac
    open Logary
    open Logary.Configuration
    open Logary.Adapters.Facade
    open Logary.Targets
    
    [<EntryPoint>]
    let main argv =
      let logary = 
        Config.create "MyProject.Tests" "localhost"
        |> Config.targets [ LiterateConsole.create LiterateConsole.empty "console" ]
        |> Config.processing (Events.events |> Events.sink ["console";])
        |> Config.build
        |> run
      LogaryFacadeAdapter.initialise<Expecto.Logging.Logger> logary
    
      // Invoke Expecto:
      runTestsInAssemblyWithCLIArgs [] argv