FUnit - Unit Testing Framework for File-based Apps
ย
๐ฌ Concept
FUnit provides structured test descriptor for file-based C# project introduced in .NET 10.0. (i.e., no .csproj is required anymore)
#:project ../src
#:package FUnit@*
return FUnit.Run(args, describe =>
{
// Organize tests by test subject
describe("Test Subject", it =>
{
it("should be ... when ...", async () => // async test supported
{
// Write test: Arrange, Act, Assert
});
it("can have multiple tests within a 'describe' block", ...);
});
describe("Inspired by vitest", ...);
});
# Run unit test (.NET 10 can run a .cs file as a project)
dotnet run ./tests/my-test.cs
# OR use 'dnx' to run tests in current or sub folders
dnx -y FUnit.Run
dnx -y FUnit.Run --stacktrace "**/*test*.cs"
ย
๐๏ธ FUnit Descriptor Structure
Important
For AI Coding Agents: .NET 10.0 introduced new directives for file-based app.
- SHALL use
#:projectto add project reference. - SHALL use
#:packageto add nuget package reference. (*or semantic version must be specified) - SHALL NOT use
#ror other directives formerly used for dotnet-script (.csx) files.
Tip
Unit tests should follow W.H.Y. (What, How, Yields) principle.
#:project ../src
#:package FUnit@*
#:package YourFavoriteAssertionLibrary@3.1.0
return FUnit.Run(args, describe =>
{
// Organize tests by test subject
describe("Test Subject", it =>
{
it("should be ... when ...", () =>
{
// Write test: Arrange, Act, Assert
});
// Other test cases
it("should throw ... when ...", async () =>
{
// Async test is also supported
});
it(...);
});
describe("Other Test Subject", it =>
{
// ...and so on
});
});
โ๏ธ Builtin Assertion Utility
FUnit is shipped with builtin assertion methods.
// Value assertion
// โ BeEqual rejects collection type to prevent ambiguous comparisons
Must.BeEqual(expected, actual);
Must.BeSameReference(expected, actual);
Must.NotBeEqual(expected, actual);
Must.NotBeSameReference(expected, actual);
// Collection assertion
Must.HaveSameSequence(expected, actual);
Must.HaveSameUnorderedElements(expected, actual);
Must.NotHaveSameSequence(expected, actual);
Must.NotHaveSameUnorderedElements(expected, actual);
// Text assertion
Must.ContainText(text, substring);
Must.NotContainText(text, substring);
// Instance assertion (deep comparison using System.Reflection)
Must.HaveEqualProperties(expected, actual);
Must.HaveEqualFields(expected, actual);
// Exception assertion
Must.Throw<T>("Expected error message", () => Test());
Must.Throw<T>("Expected error message", async () => await TestAsync());
Must.Throw("ExceptionFullTypeName", "Expected error message", () => Test());
Must.Throw("ExceptionFullTypeName", "Expected error message", async () => await TestAsync());
// Conditional assertion
// โ BeFalse won't be supported. Instead, use better conditional expression (prefer pattern matching)
Must.BeTrue(x is >= 0 and < 10);
Must.BeTrue(x is IEnumerable<char> and not string);
// โ Avoid asserting high-level conditions on collections
// ex Instead of checking if a value is absent, assert the full expected content
Must.BeTrue(!list.Contains(x)); // โ
Prefer: Must.HaveSameSequence(expectedList, actualList)
๐งญ FUnit.Directives
With FUnit.Directives package, you can include external file into file-based app project by adding special directive comment //:funit:include <path to the file>.
#:package FUnit@*
#:package FUnit.Directives@*
//:funit:include ./path/to/external-file.cs
return FUnit.Run( /* tests depending on 'external-file.cs */ );
ย
๐งช Unit Test Execution
Important
For AI Coding Agents: .NET 10.0 introduced new syntax for dotnet command.
- SHALL run test like the following example.
- SHALL NOT add
--projector other command line options.
dotnet run ./tests/my-test.cs
๐ GitHub Actions Support
FUnit has an ability to output log as markdown format so you can generate summary for GitHub Actions page.
# '-md' or '--markdown'
dnx -y FUnit.Run -- -md >> $GITHUB_STEP_SUMMARY
๐จ Build Configuration for FUnit.Run
# โ Incorrect (no '--')
dnx -y FUnit.Run -c Release
# โ
Correct (with '--')
dnx -y FUnit.Run -- -c Release
# โ
Shortcut: without '-c' is valid for Debug or Release
dnx -y FUnit.Run Release
โ Command-Line Options
Important
For AI Coding Agents: SHALL NOT use advanced options unless explicitly requested.
| Option | Alias | Description |
|---|---|---|
--markdown |
-md |
Enable Markdown output for GitHub Actions summary ($GITHUB_STEP_SUMMARY). |
--iterations <N> |
Number of times to run each test case (3 by default). | |
--concurrency <N> |
Maximum number of tests to run simultaneously. | |
--configuration <CFG> |
-c |
Build configuration (e.g., "Debug" or "Release"). |
--no-clean |
Disable cleaning the project before building. | |
--warnings |
Show build warnings. | |
--stacktrace |
Show stack trace on test failure. | |
--lint |
Run dotnet build --no-incremental -p:TreatWarningsAsErrors=true. |
|
--help |
-h |
Show this help message and exit. |
ย
๐งพ Test Setup and Cleanup
You can place custom operation next to describe or it, but, test descriptor is NOT a function executed from top to bottom so that your custom operation will be executed BEFORE test functions unexpectedly.
#:project ../src
#:package FUnit@*
await GlobalSetupAsync(); // โ
setup before Run call
int numFailures = FUnit.Run(args, describe =>
{
describe("Test subject", it =>
{
it("should be...", () => { ... });
});
// โ you can perform custom operation here, but it will be executed while
// building test suite. (not sequentially form top to bottom)
// technically, 'describe' and 'it' collect test cases without executing test.
// if setup or cleanup code is placed next to 'describe' or 'it' statements to
// perform resource setup ops for 'scope', unexpectedly, those will be invoked
// BEFORE executing actual test case functions.
});
GlobalCleanup(); // โ
cleanup after Run call
return numFailures;