Basic passing and failing cases
using VanillaTest;
var test = new VanillaTestSuite();
test.Expects("2 + 2 equals 4");
if (2 + 2 == 4)
{
test.Pass();
}
else
{
test.Fail();
}
test.Done();
test.Expects("uppercase preserves the word");
if ("csharp".ToUpperInvariant() == "CSHARP")
{
test.Pass();
}
else
{
test.Fail();
}
test.Done();
TestResult result = test.Report();
Console.WriteLine(result.Report);
return result.Ok ? 0 : 1;The first decision for a case wins. Done() moves the completed numbered row into Passed or Failed, and Report() seals the suite.
Inspect the immutable result
TestResult result = test.Report();
Console.WriteLine($"Total: {result.Total}");
Console.WriteLine($"Failures: {result.FailureCount}");
Console.WriteLine($"OK: {result.Ok}");
foreach (string row in result.Passed)
{
Console.WriteLine($"PASS {row}");
}
foreach (string row in result.Failed)
{
Console.Error.WriteLine($"FAIL {row}");
}
TestResult cached = test.Report();
Console.WriteLine(ReferenceEquals(result, cached)); // TruePassed and Failed are read-only snapshots. Each value includes its one-based case number and the .expects label, such as 1) .expects addition preserves the total.
Handle typed lifecycle errors
var test = new VanillaTestSuite();
test.Expects("only one active case");
try
{
test.Expects("blocked while the first is active");
}
catch (VanillaTestException error)
when (error.Error == VanillaTestError.TestAlreadyActive)
{
Console.WriteLine(error.Message);
}
test.Pass();
test.Done();
Console.WriteLine(test.Report().Report);The rejected transition leaves the suite unchanged. That is why the active case can still pass, finish, and report normally after the exception.
Use the lifecycle inside MSTest
using Microsoft.VisualStudio.TestTools.UnitTesting;
using VanillaTest;
namespace Sample.Tests;
[TestClass]
public sealed class CalculationTests
{
[TestMethod]
public void AdditionPreservesTheTotal()
{
var test = new VanillaTestSuite();
test.Expects("addition preserves the total");
if (1 + 2 == 3) test.Pass(); else test.Fail();
test.Done();
TestResult result = test.Report();
Assert.IsTrue(result.Ok, result.Report);
Assert.AreEqual(1, result.Total);
}
[TestMethod]
public async Task AsyncWorkIsSupported()
{
await Task.Yield();
Assert.IsTrue(new VanillaTestSuite().Report().Ok);
}
}dotnet test --configuration ReleaseThese public, parameterless methods also fit the browser host. Standard native MSTest remains available for methods that need data rows, injected runner context, filters, or other adapter services.
The repository's seven-method core inventory
The live C# browser example does not maintain a second test copy. It publishes these same seven methods from VanillaTest.Tests:
| Method | Contract covered |
|---|---|
EmptyReportIsPassingCachedAndFinal | Empty pass, cached result, sealed suite. |
LifecyclePreservesFirstDecisionsOrderAndReport | First decision, undecided failure, ordering, exact report. |
GivenWhenThenUserFlowReportsSuccessInStepOrder | Behavioral user journey, successful ordered steps, exact report. |
InvalidTransitionsAreTypedAndDoNotAdvanceNumbering | Stable lifecycle errors and unchanged state. |
SuitesIsolateUnicodeDescriptionsAndOutcomes | Suite isolation, exact Unicode descriptions. |
ErrorMessagesAndNullContractAreStable | Error messages, codes, and null handling. |
ResultCollectionsAreReadOnlySnapshots | Immutable result collections and cached identity. |
Build the same MSTest assembly for the browser
dotnet workload install wasm-tools
dotnet tool install --global vanilla-test.tool --version 2.1.0 --add-source ./packages
dotnet vanilla-test --browser \
--project tests/Sample.Tests/Sample.Tests.csproj \
--out-dir dist/vanilla-testThe tool emits a .NET 10 browser-wasm AppBundle. Deploy all of dist/vanilla-test/, including its _framework/ directory; this is intentionally not a single-file artifact.
Use a compatible custom browser page
<!doctype html>
<html lang="en" data-csharp-browser="running">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dashboard browser tests</title>
</head>
<body>
<h1
data-csharp-browser-site-check="dashboard renders its heading"
data-csharp-browser-expected="Dashboard browser tests"
>Dashboard browser tests</h1>
<p>Runtime: <code data-csharp-browser-target>browser-wasm</code></p>
<p data-csharp-browser-result role="status" aria-live="polite">Starting…</p>
<strong data-csharp-browser-score>0/?</strong>
<ol data-csharp-browser-checks></ol>
<pre data-csharp-browser-console role="log"></pre>
<script type="module" src="./browser.js"></script>
</body>
</html>dotnet vanilla-test --browser \
--project tests/Sample.Tests/Sample.Tests.csproj \
--page tests/browser.htmlThe result, score, checks, target, console, and module markers connect the custom page to the generated adapter. The visible heading marker adds one exact browser-site assertion before .NET starts.
Run the native and browser benchmark harness
dotnet run \
--project dotnet/benchmarks/VanillaTest.Benchmarks/VanillaTest.Benchmarks.csproj \
--configuration Release
dotnet vanilla-test --browser --bench \
--project dotnet/benchmarks/VanillaTest.Benchmarks/VanillaTest.Benchmarks.csprojBoth commands execute the exact same fixed workload: 1,000,000 cases across 1,000 fresh suites, with the resolved runner-entry invocation, arithmetic/checksum work, lifecycle calls, report materialization, per-suite checks, final validation, and runtime-managed work inside each sample. Each runtime initializes first, discards one warmup, and records five samples; startup, module loading, initial module compilation or instantiation, runtime initialization, coverage, memory probes, encoding, file output, and teardown remain outside every timer. This is the same boundary used by the JavaScript and Rust rows. Inspect both raw sample sets and their provenance or run the browser lane here.