Expecto, a testing library for F# built on tests as values
A smooth testing lib for F#. APIs made for humans! Strong testing methodologies for everyone!
At a glance
- What is it?
- haf/expecto is an Apache-2.0 testing library for F# whose defining idea is that tests are values, so they can be composed, reduced, filtered, repeated and passed around, with setup and teardown as plain functions rather than attributes. Tests run in parallel and asynchronously by default, and the library includes a test runner, performance comparison tooling, FsCheck integration and stress testing.
- Who is it for?
- Adopt Expecto if you write F# and want a test suite where composition and parallelism are the default rather than something you configure. Skip it if you are on C# or another.NET language, since the design is built around F# values and discriminated unions.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 14 days ago.
- What is it written in?
- Mainly F#, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Tests as values, and what that buys
Expecto is described as an advanced testing library for F# and as a smooth testing lib, with the promise of APIs made for humans and strong testing methodologies for everyone.
The central idea is stated in the README: you write tests as values. Tests can be composed, reduced, filtered, repeated and passed as values, because they are values. The claim that this gives the programmer a lot of control when writing tests is the marketing, and the mechanics underneath it are what matter.
In F#, a test is naturally a function that returns a list of results. That representation makes the combinators natural rather than bolted on. Composing tests is function composition. Filtering is a predicate over the list. Repeating is mapping with a count. And because the test is a value rather than an attribute marking a method, you can write a test that generates other tests, which is how you get property-based and parameterised testing without a separate framework.
The other consequence is that setup and teardown are just simple functions, with no need for attributes. The README points at this as part of what the value model gives you, and it is a real difference from attribute-based frameworks where the lifecycle is a property of the class rather than of the test.
The library comes with batteries included with an integrated test runner, but the README is explicit that it is still open for extension because of the compositional model. That is the right way round for a testing library. The runner is a convenience; the model is the product.
The licence is Apache-2.0, the project is on NuGet as Expecto, and the last push was on 2026-10-09. The published versions are 10.2.0 and 10.2.1 from March 2024, with 10.1.0 before that, so the released line is a little behind the repository.
Parallel by default, and what that catches
The claim about parallelism is the one worth examining, because it is both a feature and a source of surprise.
Expecto tests are parallel and async by default, so that you can use all your cores for testing your software. The README also makes the claim that this opens up a new way of catching threading and memory issues for free using stress testing.
The reasoning is sound. A suite that runs tests concurrently against shared state finds data races and ordering assumptions that a sequential runner never will, because the interleavings differ on every run. That is the same reason property-based testing finds bugs that example-based testing does not, and combining the two is more effective still.
The cost is that a test which passes alone may fail in the suite, and the failure is a real race in the code or a hidden dependency between tests rather than a flaw in the framework. The README has a section on what the expected to have type TestCode message means, and one on tests hanging and not seeing why, which are both the questions a team asks when the parallel runner first surprises them.
The stress testing section is worth reading as a deliberate feature rather than a fallback. Running the suite repeatedly, with the tests shuffled, is a cheap way to shake out order dependence. The README also documents a shuffle option, alongside a filter option for running a subset.
The practical advice for anyone adopting this is to start with the default and fix your tests to be independent rather than turning parallelism off. A suite that only passes sequentially is telling you something true about the code, and Expecto is built to tell you that sooner rather than later.
The API surface and the runner
The quickstart is two dotnet commands:
dotnet new install "Expecto.Template::*"
dotnet new expecto -n PROJECT_NAME -o FOLDER_NAMEand the README points at a Smooth Testing channel on YouTube for the basics, which is a reasonable choice for a library whose selling point is ergonomics rather than raw capability.
The runner has four entry points, and the difference between them is the scope and the cancellation behaviour. runTestsWithCLIArgs runs the current project, runTestsWithCLIArgsAndCancel adds cancellation support, runTestsInAssemblyWithCLIArgs runs a whole assembly rather than the current one, and runTestsInAssemblyWithCLIArgsAndCancel combines both. The pattern is that the cancellation variants exist because a test run that hangs is a common enough problem to deserve a first-class answer.
The expectations come from an Expect module, and there is a separate Performance module for the performance comparison work. The README describes Performance.findFastest as the entry point for that, and the comparison is described as statistically sound, which is a specific claim. Performance testing that ignores variance produces numbers that look like results and are not, and the emphasis on statistical soundness suggests the library does repeated measurement with proper handling of the spread rather than a single timed run.
The test constructs cover the usual ground with F#-specific shapes. testList groups tests, testCase is the unit of work, and there are fixtures, theory tests, pending tests for things not yet implemented, focus for narrowing a run, and sequenced tests where order matters. testParam is the parameterised test combinator, and setup and teardown are plain functions as noted earlier.
Property based testing is a first-class part of the library rather than an add-on, with a link collection for the generators and a section for code from FsCheck, which is the underlying property testing library.
There is also a good deal of operational tooling around the runner. There is a section on prettifying stack traces and shipping test logs, a section on the TestResults file, and a section on sending an email on failure through custom printers, which is the kind of feature that matters more in a .NET shop than the library's author would put in a README, and its presence tells you about the audience.
Where it fits against the alternatives
For F#, the alternatives are xUnit, NUnit and the built-in MSTest support, and the comparison is less about features than about language fit.
An attribute-based framework in F# is awkward. You write a class with attributes on methods, and the test body is a method rather than a value, so you cannot build tests out of other tests. Expecto's model removes that friction entirely, and the composability is what the composability section of the README is really selling. A property-based test that generates a hundred cases is a list of a hundred test values, and that is not something an attribute framework can express.
The performance testing is the other differentiator, and it is unusual for a test library to include it. FsCheck does property testing, and the community has a strong preference for testing as values, so Expecto is not alone in that view. What Expecto adds is the performance module and a stress-oriented runner, which together mean you can catch a performance regression in the same suite that catches a correctness regression, rather than in a separate benchmarking project with its own configuration.
The cost of the parallel-by-default design is that adopting it means dealing with test isolation, which a sequential runner would have let you get away with. The README's sections on the expected to have type TestCode message and on hanging tests are the two questions you will spend time on.
The .NET integration section is worth reading for a specific reason. Dotnet test and the usual .NET test infrastructure assume a particular discovery mechanism, and a framework whose tests are values rather than attributed methods has to bridge that. The TestResults file and the stack trace prettifying exist to make the output readable in the tools an F# developer already has open, and that bridging work is the main integration cost of choosing a non-attribute framework inside a .NET shop.
If you are on F# and want a testing library that treats your language as a functional language rather than as a slightly awkward C#, Expecto is the direct answer. If you are on another .NET language, none of this applies and the standard attribute frameworks are the right choice.
Editorial conclusion
Adopt Expecto if you write F# and want a test suite where composition and parallelism are the default rather than something you configure. Skip it if you are on C# or another.NET language, since the design is built around F# values and discriminated unions. Verify first by creating a project with the expecto template, adding a test that asserts a list, and running it with the shuffle option to confirm the parallel runner behaves the way your suite needs.
Frequently asked questions
What is the central idea behind the Expecto testing library?
That tests are values. Because a test in F# is naturally a function returning a list of results, tests can be composed, reduced, filtered, repeated and passed around, and setup and teardown are plain functions rather than attributes.
How do I start a new project with Expecto?
Install the template with dotnet new install "Expecto.Template::*" and then create a project with dotnet new expecto -n PROJECT_NAME -o FOLDER_NAME. The package is published on NuGet as Expecto and the licence is Apache-2.0.
Do Expecto tests run in parallel by default?
Yes, Expecto tests are parallel and async by default so all cores can be used, and the README argues this catches threading and memory issues for free through stress testing. The runner also offers a shuffle option and a stress testing mode, and documents what to do when tests hang.
What test kinds does Expecto support?
Normal tests, testList for grouping, fixtures, theory tests, pending tests, focus for narrowing a run, sequenced tests where order matters, and parameterised tests through testParam. Property based testing is included with a link collection for generators and support for code from FsCheck.
Does Expecto do performance testing?
Yes, it includes a Performance module with a findFastest entry point, described as making statistically sound performance comparison simple. There is also a section on BenchmarkDotNet usage for projects that prefer an external benchmark harness.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/haf-expecto)Was this useful?