AnyUnit.Style.Expecto
1.2.2
dotnet add package AnyUnit.Style.Expecto --version 1.2.2
NuGet\Install-Package AnyUnit.Style.Expecto -Version 1.2.2
<PackageReference Include="AnyUnit.Style.Expecto" Version="1.2.2" />
<PackageVersion Include="AnyUnit.Style.Expecto" Version="1.2.2" />
<PackageReference Include="AnyUnit.Style.Expecto" />
paket add AnyUnit.Style.Expecto --version 1.2.2
#r "nuget: AnyUnit.Style.Expecto, 1.2.2"
#:package AnyUnit.Style.Expecto@1.2.2
#addin nuget:?package=AnyUnit.Style.Expecto&version=1.2.2
#tool nuget:?package=AnyUnit.Style.Expecto&version=1.2.2
AnyUnit.Style.Expecto
Expecto-style F# tests for
AnyUnit: testList/testCase
values and the Expect assertion vocabulary, close enough that an
existing Expecto suite's call sites usually don't change.
module MyTests
open AnyUnit.Style.Expecto
[<Tests>]
let tests =
testList "math" [
testList "addition" [
testCase "handles negatives" <| fun () ->
Expect.equal (-2 + -3) -5 "negatives should add"
]
testCaseAsync "async work" <| async {
let! v = async { return 42 }
Expect.equal v 42 "should round-trip"
}
ptestCase "not ready yet" <| fun () ->
failtest "reported Ignored, never run"
]
Add the assembly-level opt-in once, anywhere in the project:
[<assembly: AnyUnit.Style.Expecto.Discovery.ExpectoStyle>]
do ()
Then run it with any AnyUnit runner - anyunit-runner, dotnet test
via AnyUnit.TestingPlatform, or browser-wasm.
Porting notes
Argument order is Expecto's, including the required message last:
Expect.equal actual expected message. That is preserved literally, even
where it reads backwards to someone arriving from NUnit or xUnit.
open Expecto becomes open AnyUnit.Style.Expecto. The builders are
[<AutoOpen>], so that single line is usually the whole edit.
The project becomes a library. An Expecto test project is an Exe
with its own entry point (or one generated by YoloDev.Expecto.TestSdk);
AnyUnit's runners load a library. Switch OutputType to Library, drop
the Expecto/test-SDK packages, and add
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> - a
library doesn't copy FSharp.Core.dll beside its output the way an exe
does, and the runner needs it there.
Expecto.Logging has a minimal stand-in under
AnyUnit.Style.Expecto.Logging: Log.create, logSimple, the
info/warn/... members, LogLevel, and Message.event/eventX. It
is Expecto's own recommended way to write output from a test, so real
suites use it. Output goes into the running test's log - so it appears
in that test's results next to its assertions - or to the console when
called outside a test (creating a logger at module scope is normal and
must not throw). None of Expecto's targets, formatting or sinks.
Nesting is flattened, because AnyUnit's model is a fixed
Assembly → Fixture → Test. The root testList names the fixture; every
leaf keeps the rest of its path, joined with / - which is how Expecto
prints them anyway:
fixture "math"
test "addition/handles negatives"
Two bindings in one module become two fixtures, so [<Tests>] is the
grouping unit rather than the module.
What's covered
testList, testCase, testCaseAsync, testCaseTask, and the
ptest* pending forms of each. [<Tests>] discovery.
Expect: equal, notEqual, same, isTrue, isFalse, isNull,
isNotNull, isSome, isNone, isOk, isError, wantOk,
wantError, wantSome, isEmpty, isNonEmpty, contains,
hasLength, hasCountOf, isGreaterThan, isLessThan,
stringContains, sequenceEqual, all, throws, throwsT. Top-level
failtest/failtestf, and Tests.failtest/Tests.failtestf for
suites that build their own helpers on them.
What's not
Focused tests (ftestCase, ftestList, [<FTests>]) are
deliberately absent. Focusing is a hand-editing debugging aid that is a
bug once committed - Expecto itself ships --fail-on-focused-tests to
catch exactly that - so a stray one here is a compile error rather
than a silent change in which tests run.
testProperty needs FsCheck, which the core deliberately doesn't
depend on. Expect.isFasterThan and the rest of Expecto.Performance
are out for the same reason.
runTestsWithCLIArgs and Expecto's own CLI have no equivalent -
AnyUnit has its own runners, which is the point of the exercise.
Parallel execution. AnyUnit's engine runs tests sequentially by
design, so Expecto's default parallelism (and Sequenced/ParallelWith)
has nothing to configure.
Custom helpers, Expect.pass, and an escape hatch
Expecto's idiom for a custom assertion is "throw on failure, do nothing on success":
let hasOkValue v x =
match x with
| Ok x when x = v -> ()
| Ok x -> Tests.failtestf "Expected Ok(%A), was Ok(%A)." v x
| Error x -> Tests.failtestf "Expected Ok, was Error(%A)." x
Expecto has no assert count, so that's fine there. Under AnyUnit, a
success path that does nothing is indistinguishable from a test that
asserted nothing, and the test reports NoError rather than Success.
In a real port (FsToolkit.ErrorHandling, below) that was 511 of
1,461 tests.
The fix is one line on each helper's success path:
| Ok x when x = v -> Expect.pass ()
Expect.pass () registers a successful assertion without checking
anything - an AnyUnit addition with no Expecto counterpart. In that
port, 11 such lines in the suite's one helper file brought the 511
down to 45, and those 45 genuinely assert nothing (they are
compile-shape tests: fun _ -> ignore Result.ignore).
The escape hatch, for a port that wants Expecto's verdicts first and that cleanup later:
[<assembly: AnyUnit.Style.Expecto.Discovery.ExpectoStyle(CompletionIsPass = true)>]
or per binding, [<Tests; CompletionIsPass>]. A test that completes
without throwing then reports Success even with no Expect call. The
cost is the NoError signal, for the whole assembly or that binding -
which is a real loss: it has found genuinely assertion-free tests in
every real suite ported so far. It's off by default for that reason,
and it only ever upgrades NoError; a failing test still fails.
One more AnyUnit addition
requires has no Expecto counterpart. Attribute-based styles declare a
platform requirement with [RequiresCapability(...)]; a value-based
style has no method or class to attach an attribute to, so the
requirement composes into the tree instead:
requires AnyUnit.Run.TestCapabilities.AsyncYield (
testCaseAsync "genuinely suspends" <| async {
do! Async.Sleep 1
Expect.isTrue true "resumes where the platform can yield"
})
Wrapped around a testList, it applies to every leaf beneath it. Tests
whose awaits all complete synchronously - the overwhelmingly common case
- need nothing.
How assertions reach AnyUnit
Expect.equal actual expected "msg" is a free function with no receiver,
and keeping that call site is the whole point of the style. It asserts
through the running test (AnyUnit.Run.AmbientTest, which the engine
sets around every test body), so the counts are real: a test that makes no assertion at all still reports
NoError rather than a false Success, which is the distinction
AnyUnit's instance-scoped Assert exists to preserve.
It is deliberately not Assert.GlobalStyle, which hands back a
throwaway Assert whose count reaches nobody and flips a process-wide
flag degrading that same distinction for every other style in the run.
The practical consequence: Expect only works inside a running test.
Calling it from module initialisation, or from a helper invoked outside
a test body, raises a descriptive InvalidOperationException rather than
quietly doing nothing.
Validated against a real suite
Ported cwtools' CWToolsTests -
54 tests across 21 [<Tests>] bindings, a real parser/validator suite
with fixture files, ptestCase, nested testLists, and Expecto.Logging
wired into the library's own log hooks. Its CI is green; locally on macOS
it runs 47 pass / 3 fail / 4 skip under Expecto (the 3 are a
locale-dependent type initialiser in the code under test, not the
tests), with Expecto.parallel=false as its CI uses.
After the port, on AnyUnit: the same 54 tests - the same 3 failing, the same 4 skipped - reported as 43 Success, 4 NoError, 3 Error, 4 Ignore.
The whole port was 5 files, 12 lines in, 12 out, plus a 4-line file for the assembly attribute:
| Edit | Count |
|---|---|
open Expecto → open AnyUnit.Style.Expecto (and .Logging, .Logging.Message) |
6 lines |
One fully-qualified Expecto.Logging.Log.create → the new namespace |
1 line |
Exe → Library, CopyLocalLockFileAssemblies, package → project swap |
csproj |
[<assembly: ExpectoStyle>] |
1 new file |
No test body changed. Every testList, testCase, ptestCase and
Expect call compiled and ran as written.
Found by the port, now fixed
Expect.contains, isNonEmpty, hasLength and hasCountOf were
missing, and so was Expecto.Logging entirely. All added.
Two differences that are the point, not problems
The 3 failures report as Error, not Fail: they are an
AggregateException from the code under test, not a failed
expectation. Expecto calls both "Failed"; AnyUnit tells them apart.
4 tests report NoError - they assert nothing (a match whose
success arm is ()). Expecto reports them as passing, and cannot tell
them from a real pass. That distinction is what the ambient assert here
was designed to preserve - see "How assertions reach AnyUnit" above.
Validated against a second suite
FsToolkit.ErrorHandling's
main test project: 1,461 tests, about 1,180 of them
testCaseAsync/testCaseTask, with its own Expect helper module
built on Tests.failtestf. CI green; locally under Expecto:
1,453 passed, 8 ignored, 0 failed.
After the port, with CompletionIsPass = true: 1,453 Success,
8 Ignore - identical. Without it, after the 11 Expect.pass edits
above: 1,408 Success, 45 NoError, 8 Ignore - and the 45 are the
compile-shape tests.
The port: open Expecto swapped in 40 files, the entry point removed
(the [<Tests>] root it already carried is the discovery hook),
Exe → Library with CopyLocalLockFileAssemblies, the Expecto and
test-SDK packages swapped for this one, and one unused ftestCaseTask
helper deleted from the suite's own shim - focus is deliberately absent
here, so it failed to compile rather than silently changing behaviour.
Found by this port, now fixed
Expect.equalcompared arrays by reference. It usedObject.Equals; Expecto's is F#'s structural=. 20 tests failed with a message that printed the same array twice. Now=.Expect.wantOk/wantError/wantSome,Expect.same, andTests.failtest/failtestfwere missing.- Every F# style package demanded FSharp.Core >= 10.1.x - the SDK's implicit reference, carried into the nuspec as a hard floor. This suite pins 9.0.300 and could not restore at all. All F# projects in this repo now build against 6.0.1, the conventional floor for a netstandard2.0 F# library, and the test payloads run on it in CI so the floor is known to work rather than merely compile.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- AnyUnit (>= 1.2.2)
- FSharp.Core (>= 6.0.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.