{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"},{"name":"Jendrik Brack","url":"https://daily-devops.net/authors/jendrik/"}],"description":"Recent content in TUnit: Modern .NET Test Framework on Daily DevOps \u0026 .NET","favicon":"https://daily-devops.net/images/logo_hu_6465d873dfa490cf.png","feed_url":"https://daily-devops.net/tags/tunit/feed.json","home_page_url":"https://daily-devops.net/tags/tunit/","icon":"https://daily-devops.net/images/logo_hu_5926de77762241ba.png","items":[{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eOpen any .NET test project started in the last two years and the mocking line in the \u003ccode\u003e.csproj\u003c/code\u003e usually reads \u003ccode\u003eNSubstitute\u003c/code\u003e. Three years ago, it read \u003ccode\u003eMoq\u003c/code\u003e. The flip happened fast, and most of the industry has not gone back. What both share, however, is the same architectural foundation: \u003ccode\u003eCastle.DynamicProxy\u003c/code\u003e, runtime IL (Intermediate Language) emission, expression-tree-driven dispatch. The libraries differ in API ergonomics. They are identical underneath.\u003c/p\u003e\n\u003cp\u003eThat foundation is starting to crack.\u003c/p\u003e\n\u003cp\u003eNativeAOT does not allow runtime IL generation. Trimming strips members that Castle later needs. Cold-start benchmarks penalize the first-call reflection cost. And the diagnostics (when a setup matches nothing) surface at the first invocation, deep in test output, not at compile time where they belong.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/thomhurst/TUnit/tree/main/TUnit.Mocks\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eTUnit.Mocks\u003c/a\u003e is the response. It\u0026rsquo;s a \u003cstrong\u003estandalone\u003c/strong\u003e, source-generated mocking library shipping with the TUnit family. It depends on no test framework, runs against xUnit, NUnit, MSTest, or TUnit itself, and answers a question that\u0026rsquo;s been sitting unasked for years: \u003cem\u003ewhat would mocking look like if you started over today, with Roslyn analyzers, source generators, and AOT as the baseline assumption?\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003eThe official documentation is at \u003ca href=\"https://tunit.dev/docs/writing-tests/mocking/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003etunit.dev/docs/writing-tests/mocking/\u003c/a\u003e. Everything that follows is verified against the docs and the source in \u003ca href=\"https://github.com/thomhurst/TUnit/tree/main/TUnit.Mocks\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003ethomhurst/TUnit/TUnit.Mocks\u003c/code\u003e\u003c/a\u003e.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-red-corner\"\u003e\u003ca href=\"/posts/tunit-mocks-source-generated/#the-red-corner\" title=\"The Red Corner\"\u003eThe Red Corner\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMoq is not the comparison axis anymore. In August 2023 it shipped a silent \u003ccode\u003eSponsorLink\u003c/code\u003e dependency that hashed and transmitted developer emails on every build. Trust never recovered. NSubstitute became the default. That\u0026rsquo;s the baseline. Moving on.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-problem-nsubstitute-inherits\"\u003e\u003ca href=\"/posts/tunit-mocks-source-generated/#the-problem-nsubstitute-inherits\" title=\"The Problem NSubstitute Inherits\"\u003eThe Problem NSubstitute Inherits\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eNSubstitute does not have a SponsorLink problem. It has a different problem, and it shares it with every other library that depends on \u003ccode\u003eCastle.DynamicProxy\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAOT (Ahead-of-Time Compilation) compatibility.\u003c/strong\u003e \u003ccode\u003eCastle.DynamicProxy\u003c/code\u003e calls \u003ccode\u003eSystem.Reflection.Emit\u003c/code\u003e to materialize proxy types at runtime. NativeAOT and any sufficiently aggressive trimming pass both refuse. The workaround is to keep the JIT alive, which defeats half the reason to ship AOT in the first place.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eExpression-tree fragility.\u003c/strong\u003e NSubstitute parses \u003ccode\u003esub.GetUser(Arg.Any\u0026lt;int\u0026gt;()).Returns(user)\u003c/code\u003e using \u003ccode\u003eCastle\u003c/code\u003e-generated proxies that intercept the method call and walk the call site. When the method involves generic constraints, ref structs, in-parameters, or default interface methods, the interception either fails outright or silently produces a setup that matches nothing. The error, if it appears at all, surfaces during the first invocation.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eCold-start cost.\u003c/strong\u003e Each first substitute of a type pays a one-time IL emission cost, and every \u003ccode\u003e.Returns()\u003c/code\u003e allocates and resolves call specifications at runtime. On a CI cold cache, with thousands of tests, this adds real wall time. Not catastrophic, but visible.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eStatic abstract members.\u003c/strong\u003e Interfaces with static abstract members (an increasingly common .NET pattern since C# 11) trigger \u003ccode\u003eCS8920\u003c/code\u003e when passed as a generic type parameter. \u003ccode\u003eSubstitute.For\u0026lt;IFoo\u0026gt;()\u003c/code\u003e refuses to compile if \u003ccode\u003eIFoo\u003c/code\u003e has any. The library has no escape hatch.\u003c/p\u003e\n\u003cp\u003eNone of these are dealbreakers individually. Stacked together, they are the reason \u003ccode\u003eNSubstitute\u003c/code\u003e keeps showing up on issue trackers as a candidate for the next step.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"one-caveat-before-we-start-c-14--net-10\"\u003e\u003ca href=\"/posts/tunit-mocks-source-generated/#one-caveat-before-we-start-c-14--net-10\" title=\"One Caveat Before We Start: C# 14 / .NET 10\"\u003eOne Caveat Before We Start: C# 14 / .NET 10\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eTUnit.Mocks\u003c/code\u003e requires \u003cstrong\u003eC# 14 or later\u003c/strong\u003e (\u003ccode\u003eLangVersion\u003c/code\u003e set to \u003ccode\u003e14\u003c/code\u003e or \u003ccode\u003epreview\u003c/code\u003e). The source generator emits \u003ccode\u003eTM004\u003c/code\u003e at compile time if it doesn\u0026rsquo;t see it. The reason is the most ergonomic entry point uses C# 14 \u003cem\u003estatic extension members\u003c/em\u003e. If you\u0026rsquo;re stuck on C# 12 or 13, you can still use the library, but you\u0026rsquo;ll use the \u003ccode\u003eMock.Of\u0026lt;T\u0026gt;()\u003c/code\u003e factory throughout instead of the \u003ccode\u003eT.Mock()\u003c/code\u003e syntax shown below.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-tunitmocks-actually-ships\"\u003e\u003ca href=\"/posts/tunit-mocks-source-generated/#what-tunitmocks-actually-ships\" title=\"What TUnit.Mocks Actually Ships\"\u003eWhat TUnit.Mocks Actually Ships\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe API has the shape of NSubstitute, deliberately. Migration is meant to be possible. The internals are completely different.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"creating-mocks-iservicemock-and-mockoft\"\u003e\u003ca href=\"/posts/tunit-mocks-source-generated/#creating-mocks-iservicemock-and-mockoft\" title=\"Creating Mocks: IService.Mock() (and Mock.Of\u0026lt;T\u0026gt;())\"\u003eCreating Mocks: \u003ccode\u003eIService.Mock()\u003c/code\u003e (and \u003ccode\u003eMock.Of\u0026lt;T\u0026gt;()\u003c/code\u003e)\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe recommended entry point is a static extension method on the type itself:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003emock\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eIUserService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMock\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis is real C# 14 syntax — a static extension member generated by the source generator. For interfaces, it returns a typed wrapper that \u003cstrong\u003eimplements the interface directly\u003c/strong\u003e. You can assign it to the interface variable without unwrapping, pass it as a constructor argument, store it in collections — anywhere \u003ccode\u003eIUserService\u003c/code\u003e is expected:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003emock\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eIUserService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMock\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eIUserService\u003c/span\u003e \u003cspan class=\"n\"\u003esvc\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e                  \u003cspan class=\"c1\"\u003e// implicit cast, no .Object\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eList\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIUserService\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eall\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e];\u003c/span\u003e          \u003cspan class=\"c1\"\u003e// works in collections\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eprocessor\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eOrderProcessor\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// passes as parameter\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eFor older language versions, multi-interface mocks, wrap mocks, delegate mocks, or interfaces with static abstract members, the factory variants on the \u003ccode\u003eMock\u003c/code\u003e static class remain:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ealt\u003c/span\u003e    \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eMock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOf\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIUserService\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e            \u003cspan class=\"c1\"\u003e// equivalent to IUserService.Mock()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003emulti\u003c/span\u003e  \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eMock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOf\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eILogger\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eIDisposable\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e    \u003cspan class=\"c1\"\u003e// multi-interface (up to 4)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003edel\u003c/span\u003e    \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eMock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOfDelegate\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eFunc\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003estring\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"kd\"\u003epartial\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eMock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eWrap\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erealService\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e            \u003cspan class=\"c1\"\u003e// wrap a real instance\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eSource: \u003ca href=\"https://github.com/thomhurst/TUnit/blob/main/TUnit.Mocks/Mock.cs\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eMock.cs\u003c/code\u003e\u003c/a\u003e, \u003ca href=\"https://github.com/thomhurst/TUnit/blob/main/TUnit.Mocks/MockOfT.cs\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eMockOfT.cs\u003c/code\u003e\u003c/a\u003e.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"matchers-no-arg-prefix\"\u003e\u003ca href=\"/posts/tunit-mocks-source-generated/#matchers-no-arg-prefix\" title=\"Matchers: No Arg. Prefix\"\u003eMatchers: No \u003ccode\u003eArg.\u003c/code\u003e Prefix\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003eTUnit.Mocks\u003c/code\u003e ships a \u003ccode\u003eglobal using static TUnit.Mocks.Arguments.Arg;\u003c/code\u003e — every matcher is in scope by name, no prefix needed. The everyday matchers (full set at \u003ca href=\"https://tunit.dev/docs/writing-tests/mocking/argument-matchers/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eargument-matchers\u003c/code\u003e\u003c/a\u003e):\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e               \u003cspan class=\"c1\"\u003e// matches everything, type inferred from position\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;()\u003c/span\u003e            \u003cspan class=\"c1\"\u003e// typed any-value\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eIs\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"k\"\u003evalue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e        \u003cspan class=\"c1\"\u003e// exact equality\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eIs\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003epredicate\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e    \u003cspan class=\"c1\"\u003e// predicate match\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eIsNull\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eIsNotNull\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eMatches\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eregex\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e      \u003cspan class=\"c1\"\u003e// string regex\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eIsInRange\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003emin\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003emax\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eNot\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eT\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;(\u003c/span\u003e\u003cspan class=\"n\"\u003einner\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eAnyArgs\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e           \u003cspan class=\"c1\"\u003e// expands to one Any\u0026lt;T\u0026gt;() per parameter\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThe clever part: \u003cstrong\u003eraw values and inline lambdas work directly\u003c/strong\u003e. No wrapper needed:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUser\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e42\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eReturns\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ealice\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e           \u003cspan class=\"c1\"\u003e// raw int → exact match on 42\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUser\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eid\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003eid\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eReturns\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003euser\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// inline lambda → predicate match\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSearch\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ename\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003ename\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLength\u003c/span\u003e \u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e \u003cspan class=\"m\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e()).\u003c/span\u003e\u003cspan class=\"n\"\u003eReturns\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eresults\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e \u003cspan class=\"c1\"\u003e// mix freely\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eSo in idiomatic test code, you almost never type \u003ccode\u003eIs(...)\u003c/code\u003e explicitly. Compared to NSubstitute\u0026rsquo;s \u003ccode\u003eArg.Any\u0026lt;int\u0026gt;()\u003c/code\u003e, \u003ccode\u003eArg.Is\u0026lt;int\u0026gt;(x =\u0026gt; x \u0026gt; 0)\u003c/code\u003e, it\u0026rsquo;s a real reduction in noise.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"setup-returns-throws-callback-then\"\u003e\u003ca href=\"/posts/tunit-mocks-source-generated/#setup-returns-throws-callback-then\" title=\"Setup: .Returns(), .Throws(), .Callback(), .Then()\"\u003eSetup: \u003ccode\u003e.Returns()\u003c/code\u003e, \u003ccode\u003e.Throws()\u003c/code\u003e, \u003ccode\u003e.Callback()\u003c/code\u003e, \u003ccode\u003e.Then()\u003c/code\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eThe chain method after the call decides whether you\u0026rsquo;re stubbing or verifying. From \u003ca href=\"https://tunit.dev/docs/writing-tests/mocking/setup/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003esetup\u003c/code\u003e\u003c/a\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Return values: async methods need no special API\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUser\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e()).\u003c/span\u003e\u003cspan class=\"n\"\u003eReturns\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Alice\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUserAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e()).\u003c/span\u003e\u003cspan class=\"n\"\u003eReturns\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Alice\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e   \u003cspan class=\"c1\"\u003e// auto-wrapped in Task\u0026lt;T\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Exceptions\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDelete\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e()).\u003c/span\u003e\u003cspan class=\"n\"\u003eThrows\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eInvalidOperationException\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDelete\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e()).\u003c/span\u003e\u003cspan class=\"n\"\u003eThrows\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eArgumentException\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;bad id\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Callbacks\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eProcess\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e()).\u003c/span\u003e\u003cspan class=\"n\"\u003eCallback\u003c/span\u003e\u003cspan class=\"p\"\u003e((\u003c/span\u003e\u003cspan class=\"kt\"\u003eobject?\u003c/span\u003e\u003cspan class=\"p\"\u003e[]\u003c/span\u003e \u003cspan class=\"n\"\u003eargs\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003elog\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eargs\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"m\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e]!));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Sequential behaviors: each .Then() advances the call counter\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetValue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThrows\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eInvalidOperationException\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;()\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// 1st call\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThen\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eReturns\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;retry-ok\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e           \u003cspan class=\"c1\"\u003e// 2nd call\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThen\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eReturns\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;cached\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e            \u003cspan class=\"c1\"\u003e// 3rd+ calls\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Shorthand\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetValue\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e()).\u003c/span\u003e\u003cspan class=\"n\"\u003eReturnsSequentially\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;first\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;second\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;third\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eVoid methods support \u003ccode\u003eCallback\u003c/code\u003e and \u003ccode\u003eThrows\u003c/code\u003e (not \u003ccode\u003eReturns\u003c/code\u003e) and are eagerly registered — calling \u003ccode\u003emock.Log(Any())\u003c/code\u003e without a chain is enough to allow the call in strict mode.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"properties-via-c-14-extension-properties\"\u003e\u003ca href=\"/posts/tunit-mocks-source-generated/#properties-via-c-14-extension-properties\" title=\"Properties via C# 14 Extension Properties\"\u003eProperties via C# 14 Extension Properties\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eProperty mocking gets first-class syntax:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Getter (default)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eReturns\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Alice\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThrows\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eInvalidOperationException\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Setter: any value\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCount\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSetter\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCallback\u003c/span\u003e\u003cspan class=\"p\"\u003e(()\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003elog\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;set\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Setter — specific value\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eCount\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSet\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eIs\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e42\u003c/span\u003e\u003cspan class=\"p\"\u003e)).\u003c/span\u003e\u003cspan class=\"n\"\u003eCallback\u003c/span\u003e\u003cspan class=\"p\"\u003e(()\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e \u003cspan class=\"n\"\u003elog\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAdd\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;set to 42\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// Auto-tracking — setters store, getters return\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSetupAllProperties\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eObject\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eName\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;Alice\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ename\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eObject\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eName\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// \u0026#34;Alice\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eNo NSubstitute equivalent ships at this fluency level.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"verification-wascalled-wasnevercalled-verifyinorder\"\u003e\u003ca href=\"/posts/tunit-mocks-source-generated/#verification-wascalled-wasnevercalled-verifyinorder\" title=\"Verification: .WasCalled(), .WasNeverCalled(), VerifyInOrder\"\u003eVerification: \u003ccode\u003e.WasCalled()\u003c/code\u003e, \u003ccode\u003e.WasNeverCalled()\u003c/code\u003e, \u003ccode\u003eVerifyInOrder\u003c/code\u003e\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003eSame call site as setup, different chain method. From \u003ca href=\"https://tunit.dev/docs/writing-tests/mocking/verification/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003everification\u003c/code\u003e\u003c/a\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUser\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e42\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eWasCalled\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e                            \u003cspan class=\"c1\"\u003e// at least once\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUser\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e42\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eWasCalled\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTimes\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOnce\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUser\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e()).\u003c/span\u003e\u003cspan class=\"n\"\u003eWasCalled\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTimes\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eExactly\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDelete\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e()).\u003c/span\u003e\u003cspan class=\"n\"\u003eWasNeverCalled\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUser\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e42\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eWasCalled\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTimes\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOnce\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;must run at startup\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e  \u003cspan class=\"c1\"\u003e// custom message\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003eTimes\u003c/code\u003e carries the standard vocabulary (\u003ccode\u003eOnce\u003c/code\u003e, \u003ccode\u003eNever\u003c/code\u003e, \u003ccode\u003eAtLeastOnce\u003c/code\u003e, \u003ccode\u003eExactly(n)\u003c/code\u003e, \u003ccode\u003eAtLeast(n)\u003c/code\u003e, \u003ccode\u003eAtMost(n)\u003c/code\u003e, \u003ccode\u003eBetween(min, max)\u003c/code\u003e).\u003c/p\u003e\n\u003cp\u003eCross-mock ordered verification is something I\u0026rsquo;ve wanted in NSubstitute for years and never had cleanly:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eMock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eVerifyInOrder\u003c/span\u003e\u003cspan class=\"p\"\u003e(()\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003emockLogger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Starting\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eWasCalled\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003emockRepo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSaveAsync\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e()).\u003c/span\u003e\u003cspan class=\"n\"\u003eWasCalled\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003emockLogger\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLog\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;Done\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eWasCalled\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eOut of order, it throws with the actual observed sequence. Works across independent mock instances, not just within one.\u003c/p\u003e\n\u003cp\u003eThere\u0026rsquo;s also TUnit-assertion integration if you prefer the \u003ccode\u003eAssert.That\u003c/code\u003e pipeline:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTUnit.Mocks.Assertions\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eawait\u003c/span\u003e \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUser\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e42\u003c/span\u003e\u003cspan class=\"p\"\u003e)).\u003c/span\u003e\u003cspan class=\"n\"\u003eWasCalled\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTimes\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOnce\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"strict-mode-repository-state-auto-mocking\"\u003e\u003ca href=\"/posts/tunit-mocks-source-generated/#strict-mode-repository-state-auto-mocking\" title=\"Strict Mode, Repository, State, Auto-Mocking\"\u003eStrict Mode, Repository, State, Auto-Mocking\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ccode\u003eMockBehavior.Strict\u003c/code\u003e.\u003c/strong\u003e Unconfigured calls throw \u003ccode\u003eMockStrictBehaviorException\u003c/code\u003e. NSubstitute has no native strict mode — you simulate it via \u003ccode\u003e.Throws()\u003c/code\u003e defaults.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003emock\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eIUserService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMock\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eMockBehavior\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStrict\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e\u003ccode\u003eMockRepository\u003c/code\u003e.\u003c/strong\u003e When a test creates five mocks, NSubstitute leaves you to verify each one. The repository groups them — aggregates failures across the batch:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003erepo\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eMockRepository\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eMockBehavior\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eStrict\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eusers\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003erepo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOf\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIUserService\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eorders\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003erepo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOf\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIOrderService\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003erepo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eVerifyAll\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e           \u003cspan class=\"c1\"\u003e// throws AggregateException if any setup unmet\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003erepo\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eVerifyNoOtherCalls\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eSource: \u003ca href=\"https://github.com/thomhurst/TUnit/blob/main/TUnit.Mocks/MockRepository.cs\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eMockRepository.cs\u003c/code\u003e\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eAuto-mocking (recursive).\u003c/strong\u003e In loose mode, methods returning interface types auto-return functional mocks. Reach them via \u003ccode\u003eMock.Get(obj)\u003c/code\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003emock\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eIServiceA\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMock\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eserviceB\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eObject\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetServiceB\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e   \u003cspan class=\"c1\"\u003e// not null — an auto-mock\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eMock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGet\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eserviceB\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eGetValue\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eReturns\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e42\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eserviceB\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetValue\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e                        \u003cspan class=\"c1\"\u003e// 42\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eState machines.\u003c/strong\u003e Setups scoped to named states; transitions on a chain method. From \u003ca href=\"https://tunit.dev/docs/writing-tests/mocking/advanced/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eadvanced\u003c/code\u003e\u003c/a\u003e:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003emock\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eIConnection\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMock\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSetState\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;disconnected\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInState\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;disconnected\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003em\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eReturns\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;OFFLINE\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eConnect\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eTransitionsTo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;connected\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eInState\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;connected\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003em\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eReturns\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;ONLINE\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003em\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDisconnect\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eTransitionsTo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;disconnected\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e});\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eObject\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e   \u003cspan class=\"c1\"\u003e// \u0026#34;OFFLINE\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eObject\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eConnect\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eObject\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetStatus\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e   \u003cspan class=\"c1\"\u003e// \u0026#34;ONLINE\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eFor protocols, connection lifecycles, or anything whose response depends on prior calls, this is dramatically cleaner than the \u003ccode\u003eReturns(_ =\u0026gt; ...)\u003c/code\u003e callback workarounds typical in NSubstitute.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eEvents.\u003c/strong\u003e Auto-generated \u003ccode\u003eRaise{EventName}()\u003c/code\u003e extension methods plus chain-attached \u003ccode\u003e.Raises{EventName}()\u003c/code\u003e for auto-raise on call:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRaiseOnMessage\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;hello\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e           \u003cspan class=\"c1\"\u003e// raise an event manually\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSendMessage\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e())\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eReturns\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eRaisesOnMessage\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;echo\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e           \u003cspan class=\"c1\"\u003e// fire OnMessage every time SendMessage is called\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThere\u0026rsquo;s also a \u003ccode\u003emock.Events\u003c/code\u003e surface for subscription tracking (\u003ccode\u003eSubscriberCount\u003c/code\u003e, \u003ccode\u003eOnSubscribe\u003c/code\u003e callbacks).\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eOut / ref parameters.\u003c/strong\u003e Strongly typed by parameter name — \u003ccode\u003eout string value\u003c/code\u003e produces \u003ccode\u003e.SetsOutValue(...)\u003c/code\u003e, \u003ccode\u003eref int count\u003c/code\u003e produces \u003ccode\u003e.SetsRefCount(...)\u003c/code\u003e. No untyped \u003ccode\u003e(index, value)\u003c/code\u003e tuples in normal code.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"sister-packages\"\u003e\u003ca href=\"/posts/tunit-mocks-source-generated/#sister-packages\" title=\"Sister Packages\"\u003eSister Packages\u003c/a\u003e\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003eTUnit.Mocks.Http\u003c/code\u003e and \u003ccode\u003eTUnit.Mocks.Logging\u003c/code\u003e provide first-class helpers for two cases that everyone reinvents:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// TUnit.Mocks.Http\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ehttpHandler\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eMock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHttpHandler\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003ehttpClient\u003c/span\u003e  \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eMock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eHttpClient\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ebaseAddress\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;https://api/\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// TUnit.Mocks.Logging\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003elogger\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eMock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eLogger\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eMyService\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e   \u003cspan class=\"c1\"\u003e// implements ILogger\u0026lt;MyService\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eNSubstitute users typically have hand-rolled \u003ccode\u003eMockHttpHandler\u003c/code\u003e and \u003ccode\u003eTestLogger\u0026lt;T\u0026gt;\u003c/code\u003e in every project. Replacing those is a quiet win.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"what-generatemock-is-actually-for\"\u003e\u003ca href=\"/posts/tunit-mocks-source-generated/#what-generatemock-is-actually-for\" title=\"What [GenerateMock] Is Actually For\"\u003eWhat \u003ccode\u003e[GenerateMock]\u003c/code\u003e Is Actually For\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe \u003ccode\u003e[assembly: GenerateMock(typeof(IFoo))]\u003c/code\u003e attribute is \u003cem\u003enot\u003c/em\u003e the normal entry point. Most types are picked up automatically when you call \u003ccode\u003eT.Mock()\u003c/code\u003e or \u003ccode\u003eMock.Of\u0026lt;T\u0026gt;()\u003c/code\u003e. The attribute exists for one specific case: types with \u003cstrong\u003estatic abstract members\u003c/strong\u003e. Those trigger \u003ccode\u003eCS8920\u003c/code\u003e (\u0026ldquo;the interface has unresolved static abstract members\u0026rdquo;) when used as a generic type parameter, and \u003ccode\u003eMock.Of\u0026lt;IMyParseable\u0026gt;()\u003c/code\u003e refuses to compile — same wall NSubstitute hits.\u003c/p\u003e\n\u003cp\u003eTUnit.Mocks\u0026rsquo; workaround is to emit a \u003cem\u003ebridge interface\u003c/em\u003e (suffixed \u003ccode\u003e_Mockable\u003c/code\u003e) with default implementations for the static abstract surface. You mock the bridge instead:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTUnit.Mocks\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[assembly: GenerateMock(typeof(IMyParseable))]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003einterface\u003c/span\u003e \u003cspan class=\"nc\"\u003eIMyParseable\u003c/span\u003e \u003cspan class=\"p\"\u003e:\u003c/span\u003e \u003cspan class=\"n\"\u003eIParsable\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIMyParseable\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003estring\u003c/span\u003e \u003cspan class=\"n\"\u003eFormat\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// In your test:\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003emock\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eMock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOf\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eTUnit_Mocks_Tests_IMyParseable_Mockable\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFormat\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eReturns\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;formatted\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eThis is one of the very few places in the API where the source-generation model leaks into the test code. For 99% of types, you never see the attribute at all. NSubstitute has no equivalent. If your interface uses static abstract members, you\u0026rsquo;re either rewriting the interface or wrapping it.\u003c/p\u003e\n\u003cp\u003eSource: \u003ca href=\"https://github.com/thomhurst/TUnit/blob/main/TUnit.Mocks/GenerateMockAttribute.cs\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003e\u003ccode\u003eGenerateMockAttribute.cs\u003c/code\u003e\u003c/a\u003e.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"nsubstitute--tunitmocks-side-by-side\"\u003e\u003ca href=\"/posts/tunit-mocks-source-generated/#nsubstitute--tunitmocks-side-by-side\" title=\"NSubstitute → TUnit.Mocks Side-by-Side\"\u003eNSubstitute → TUnit.Mocks Side-by-Side\u003c/a\u003e\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// NSubstitute\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003esub\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eSubstitute\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eFor\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eIUserService\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003esub\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUser\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eArg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;()).\u003c/span\u003e\u003cspan class=\"n\"\u003eReturns\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e42\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eresult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003esub\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUser\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e42\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003esub\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eReceived\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUser\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e42\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003esub\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eDidNotReceive\u003c/span\u003e\u003cspan class=\"p\"\u003e().\u003c/span\u003e\u003cspan class=\"n\"\u003eSave\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eArg\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026lt;\u003c/span\u003e\u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e\u0026gt;());\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// TUnit.Mocks (idiomatic — C# 14)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003emock\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eIUserService\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMock\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUser\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e()).\u003c/span\u003e\u003cspan class=\"n\"\u003eReturns\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003enew\u003c/span\u003e \u003cspan class=\"n\"\u003eUser\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e42\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eIUserService\u003c/span\u003e \u003cspan class=\"n\"\u003esvc\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kt\"\u003evar\u003c/span\u003e \u003cspan class=\"n\"\u003eresult\u003c/span\u003e \u003cspan class=\"p\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003esvc\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUser\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e42\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eGetUser\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e42\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eWasCalled\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eTimes\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eOnce\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003emock\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSave\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eAny\u003c/span\u003e\u003cspan class=\"p\"\u003e()).\u003c/span\u003e\u003cspan class=\"n\"\u003eWasNeverCalled\u003c/span\u003e\u003cspan class=\"p\"\u003e();\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eRoughly half the syntactic noise vanishes — partly because the \u003ccode\u003eArg.\u003c/code\u003e prefix is gone, partly because raw values match exactly without \u003ccode\u003eArg.Is(...)\u003c/code\u003e. Where the two diverge in capability (and where TUnit.Mocks pulls ahead) is the framework surface: strict mode, repositories, state machines, wrap mocks, \u003ccode\u003eVerifyInOrder\u003c/code\u003e, property mocking. Those translate as feature \u003cem\u003eadditions\u003c/em\u003e, not API changes.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"where-it-still-hurts\"\u003e\u003ca href=\"/posts/tunit-mocks-source-generated/#where-it-still-hurts\" title=\"Where It Still Hurts\"\u003eWhere It Still Hurts\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eI want to be honest about where this is not yet drop-in.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eC# 14 requirement.\u003c/strong\u003e If you\u0026rsquo;re stuck on C# 12 or 13, \u003ccode\u003eT.Mock()\u003c/code\u003e and extension-property setup are off the table. You can still use \u003ccode\u003eMock.Of\u0026lt;T\u0026gt;()\u003c/code\u003e and most features, but the ergonomic gap closes.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eMaturity gap.\u003c/strong\u003e NSubstitute has a decade of edge cases catalogued in issues and Stack Overflow answers. \u003ccode\u003eTUnit.Mocks\u003c/code\u003e is much newer. When a setup behaves unexpectedly, the search results are sparse. This will close with time.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eGenerator diagnostics.\u003c/strong\u003e The source generator surfaces some errors cleanly (missing factory, wrong type, language version). Others — especially around complex generic constraints — show up as compile errors on the \u003cem\u003egenerated\u003c/em\u003e code. Read the generated file in \u003ccode\u003eobj/Generated/\u003c/code\u003e; it\u0026rsquo;s the fastest way to understand what the generator decided to emit.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eWrap mocks need virtual members.\u003c/strong\u003e \u003ccode\u003eMock.Wrap\u0026lt;T\u0026gt;(instance)\u003c/code\u003e requires \u003ccode\u003eT\u003c/code\u003e to be non-sealed with virtual members the generator can override. Sealed classes (including most records with bodies) can\u0026rsquo;t be wrapped. NSubstitute hits the same wall.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eEcosystem.\u003c/strong\u003e Libraries that ship \u003ccode\u003eNSubstitute.Extensions\u003c/code\u003e or test helpers built on its argument matchers won\u0026rsquo;t transparently work. Migration is per-test, not per-project.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"a-realistic-migration-path\"\u003e\u003ca href=\"/posts/tunit-mocks-source-generated/#a-realistic-migration-path\" title=\"A Realistic Migration Path\"\u003eA Realistic Migration Path\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThis is what worked on the project I tried it on.\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003eBump \u003ccode\u003eLangVersion\u003c/code\u003e to \u003ccode\u003e14\u003c/code\u003e first.\u003c/strong\u003e Without it, you get TM004 and the most useful syntax is locked.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAdd \u003ccode\u003eTUnit.Mocks\u003c/code\u003e alongside \u003ccode\u003eNSubstitute\u003c/code\u003e.\u003c/strong\u003e Both coexist across fixtures. Don\u0026rsquo;t mix them within a single test file — the cognitive cost is real even though the technical conflict is small.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMigrate new tests first.\u003c/strong\u003e Anything written from scratch uses \u003ccode\u003eT.Mock()\u003c/code\u003e. Old tests stay on NSubstitute until they break.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eMigrate by fixture, not by file.\u003c/strong\u003e One library per test class.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eAdopt \u003ccode\u003eMockBehavior.Strict\u003c/code\u003e deliberately.\u003c/strong\u003e Both libraries default to loose. Strict-by-default is tempting, but it\u0026rsquo;s a cultural choice — make it explicitly.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDelete \u003ccode\u003eNSubstitute\u003c/code\u003e only when the verification semantics are confirmed.\u003c/strong\u003e \u003ccode\u003eVerifyNoOtherCalls\u003c/code\u003e interacts differently in edge cases compared to NSubstitute\u0026rsquo;s \u0026ldquo;what wasn\u0026rsquo;t \u003ccode\u003eReceived()\u003c/code\u003e is fine\u0026rdquo; model. Write a few high-coverage tests in parallel before committing.\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003eThe fastest wins I observed: AOT-failing tests compiled without changes, setup-matched-nothing errors surfaced at build time rather than buried in test output, and cold-start time dropped roughly 15% on a 3,000-test suite. Property mocking and \u003ccode\u003eVerifyInOrder\u003c/code\u003e immediately replaced hand-rolled helpers I\u0026rsquo;d been carrying for years.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"when-to-stay-on-nsubstitute\"\u003e\u003ca href=\"/posts/tunit-mocks-source-generated/#when-to-stay-on-nsubstitute\" title=\"When to Stay on NSubstitute\"\u003eWhen to Stay on NSubstitute\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eDon\u0026rsquo;t migrate just because something is new. If the test suite is in maintenance mode and nobody\u0026rsquo;s touching it, the cost outweighs the benefit. Same if you can\u0026rsquo;t move to C# 14 yet: the ergonomic gap closes hard without static extension members and extension properties. And if CI depends heavily on \u003ccode\u003eNSubstitute.Analyzers\u003c/code\u003e rules, verify the TUnit.Mocks analyzer coverage first. The rule sets differ, and losing existing guardrails by accident is a bad trade.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"verdict\"\u003e\u003ca href=\"/posts/tunit-mocks-source-generated/#verdict\" title=\"Verdict\"\u003eVerdict\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eTUnit.Mocks\u003c/code\u003e is not a curiosity. It\u0026rsquo;s a serious answer to a problem the .NET mocking ecosystem has been quietly accumulating since \u003ccode\u003eCastle.DynamicProxy\u003c/code\u003e became the foundation everyone built on. NSubstitute is excellent — and stuck on the same ceiling. If you\u0026rsquo;re standing up a new test project in 2026 on .NET 10 / C# 14, especially one targeting NativeAOT, this is now my default recommendation.\u003c/p\u003e\n\u003cp\u003eThe API is close enough to NSubstitute that the muscle memory transfers. The internals are different enough that the AOT, trimming, and cold-start problems disappear. The diagnostics surface at compile time instead of test run time. And the strict-mode, repository, state-machine, \u003ccode\u003eVerifyInOrder\u003c/code\u003e, and property-mocking features are genuine improvements I didn\u0026rsquo;t realize I was missing.\u003c/p\u003e\n\u003cp\u003eThe honest summary: NSubstitute is not dead, and Moq is not coming back. But for the first time in a decade, the foundation underneath all of them has a credible challenger — a Roslyn source generator, not a smarter proxy.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eA mocking library that compiles your setup expressions instead of evaluating them at runtime is not a small change. It\u0026rsquo;s a different category of tool.\u003c/p\u003e\n\u003c/blockquote\u003e\n","date_modified":"2026-06-11T19:05:42+02:00","date_published":"2026-06-11T17:00:00+02:00","id":"https://daily-devops.net/posts/tunit-mocks-source-generated/","language":"en","summary":"TUnit.Mocks brings source-generated, AOT-compatible mocks to .NET. No Castle.DynamicProxy, no runtime reflection. A practical comparison with NSubstitute.","tags":["dotnet","csharp","testing","tunit","mocking","sourcegenerators"],"title":"TUnit.Mocks: No Castle, No Reflection, No Drama\n","url":"https://daily-devops.net/posts/tunit-mocks-source-generated/"},{"authors":[{"name":"Martin Stühmer","url":"https://daily-devops.net/authors/martin/"}],"content_html":"\u003cp\u003eIn the .NET ecosystem, few things have remained as stable as the unit testing landscape.\nFor years, \u003cstrong\u003exUnit\u003c/strong\u003e, \u003cstrong\u003eNUnit\u003c/strong\u003e, and \u003cstrong\u003eMSTest\u003c/strong\u003e have been the go-to frameworks — dependable, predictable, and well-integrated.\nNow, \u003cstrong\u003eTUnit\u003c/strong\u003e, a new open-source project from the community (not Microsoft), is challenging the status quo with a modern design built on source generation, concurrency, and native AOT support.\u003c/p\u003e\n\u003cp\u003eThe question isn’t whether it’s new — it’s whether it’s worth adopting.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-testing-landscape-stability-meets-disruption\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#the-testing-landscape-stability-meets-disruption\" title=\"The Testing Landscape: Stability Meets Disruption\"\u003eThe Testing Landscape: Stability Meets Disruption\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eMost enterprise .NET teams rely on mature testing stacks that have proven themselves through countless CI/CD cycles.\nEach of the established frameworks has its place:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eMSTest\u003c/strong\u003e – The traditional, Microsoft-endorsed option, tightly integrated into Visual Studio and Azure DevOps; predictable and enterprise-friendly, though somewhat dated in syntax and extensibility.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNUnit\u003c/strong\u003e – Feature-rich and stable, ideal for complex testing scenarios and broad legacy support.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003exUnit\u003c/strong\u003e – Modern conventions, parallelization by default, and a cleaner programming model for test organization.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTUnit\u003c/strong\u003e – The newcomer, built with Roslyn source generators and a modern runtime model (using \u003cem\u003eMicrosoft.Testing.Platform\u003c/em\u003e) focused on speed, determinism, and native AOT compatibility.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe innovation TUnit offers is architectural — not syntactical. It moves responsibility from runtime to build-time, changing how tests are discovered and executed.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"familiar-syntax-subtle-evolution\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#familiar-syntax-subtle-evolution\" title=\"Familiar Syntax, Subtle Evolution\"\u003eFamiliar Syntax, Subtle Evolution\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eOne of TUnit’s most compelling strengths is that it feels instantly familiar to developers.\nThe syntax closely mirrors that of xUnit, minimizing friction while adding small but meaningful improvements.\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"example--tunit\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#example--tunit\" title=\"Example — TUnit\"\u003eExample — TUnit\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eTUnit\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eArithmeticTests\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Test]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eAdd_ShouldReturnSum\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e2\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"m\"\u003e3\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eIsEqualTo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e5\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Test]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Arguments(2, 3, 5)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Arguments(10, 20, 30)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eParameterized_Add\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eIsEqualTo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Test]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [DependsOn(nameof(Add_ShouldReturnSum))]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eDependentTest\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e1\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"m\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e).\u003c/span\u003e\u003cspan class=\"n\"\u003eIsEqualTo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"m\"\u003e2\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eCompare that to \u003cstrong\u003eMSTest\u003c/strong\u003e, \u003cstrong\u003exUnit\u003c/strong\u003e, and \u003cstrong\u003eNUnit\u003c/strong\u003e:\u003c/p\u003e\n\n\n\n\n\u003ch3 id=\"mstest\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#mstest\" title=\"MSTest\"\u003eMSTest\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eMicrosoft.VisualStudio.TestTools.UnitTesting\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e[TestClass]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eArithmeticTests\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [TestMethod]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [DataRow(2, 3, 5)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [DataRow(10, 20, 30)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eAdd_ShouldReturnSum\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eAreEqual\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"xunit\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#xunit\" title=\"xUnit\"\u003exUnit\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eXunit\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eArithmeticTests\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [Theory]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [InlineData(2, 3, 5)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [InlineData(10, 20, 30)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eAdd_ShouldReturnSum\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEqual\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\n\n\u003ch3 id=\"nunit\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#nunit\" title=\"NUnit\"\u003eNUnit\u003c/a\u003e\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-csharp\" data-lang=\"csharp\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003eusing\u003c/span\u003e \u003cspan class=\"nn\"\u003eNUnit.Framework\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003eclass\u003c/span\u003e \u003cspan class=\"nc\"\u003eArithmeticTests\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [TestCase(2, 3, 5)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"na\"\u003e    [TestCase(10, 20, 30)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003epublic\u003c/span\u003e \u003cspan class=\"k\"\u003evoid\u003c/span\u003e \u003cspan class=\"n\"\u003eAdd_ShouldReturnSum\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003ea\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eAssert\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eThat\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003ea\u003c/span\u003e \u003cspan class=\"p\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003eb\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003eIs\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eEqualTo\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eexpected\u003c/span\u003e\u003cspan class=\"p\"\u003e));\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAcross these examples, the differences are subtle — but TUnit introduces compile-time discovery, dependency control, and async-aware assertions without abandoning the simplicity that makes xUnit and MSTest approachable.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"performance-and-discovery-the-compile-time-advantage\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#performance-and-discovery-the-compile-time-advantage\" title=\"Performance and Discovery: The Compile-Time Advantage\"\u003ePerformance and Discovery: The Compile-Time Advantage\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eThe real technical distinction lies under the surface.\nWhile MSTest, xUnit, and NUnit rely on \u003cstrong\u003ereflection\u003c/strong\u003e to discover and run tests, TUnit shifts this process to \u003cstrong\u003ecompile time\u003c/strong\u003e via Roslyn source generators.\nThat change has measurable consequences:\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eFramework\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eDiscovery Model\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eAvg. Startup Time\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eParallel Execution\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eAOT Compatible\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eEcosystem Maturity\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eMSTest\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eReflection\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e~1.6s\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eLimited\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eVery High\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eNUnit\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eReflection\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e~1.8s\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eOptional\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eNo\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eVery High\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003exUnit\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eReflection\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e~1.4s\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDefault\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePartial\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eExcellent\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eTUnit\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSource Generation\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e~0.9s\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eBuilt-in\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eYes\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eEmerging\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eEarly benchmarks (from \u003ca href=\"https://andrewlock.net/converting-an-xunit-project-to-tunit/\" target=\"_blank\" rel=\"noopener external noreferrer\"\u003eAndrew Lock, 2024\u003c/a\u003e) show discovery and execution overhead reduced by \u003cstrong\u003e15–25%\u003c/strong\u003e in mid-sized suites.\nThat’s not academic — in enterprise CI pipelines, small savings compound fast.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eExample:\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e10,000 builds per week × 15 seconds saved per run → \u003cstrong\u003e41 hours saved weekly\u003c/strong\u003e.\u003cbr/\u003e\nAt $50/hour in build infrastructure costs, that’s roughly \u003cstrong\u003e$2,000 per month\u003c/strong\u003e in real value.\u003c/p\u003e\n\u003cp\u003eThis is where TUnit begins to show \u003cstrong\u003eeconomic relevance\u003c/strong\u003e — not just theoretical efficiency.\u003c/p\u003e\n\u003c/blockquote\u003e\n\n\n\n\n\u003ch2 id=\"tooling-and-ecosystem-integration\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#tooling-and-ecosystem-integration\" title=\"Tooling and Ecosystem Integration\"\u003eTooling and Ecosystem Integration\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTooling maturity remains TUnit’s biggest hurdle.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eMSTest\u003c/strong\u003e integrates seamlessly with Visual Studio, Azure DevOps, and corporate reporting pipelines — it’s stable, predictable, and requires zero friction.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003exUnit\u003c/strong\u003e and \u003cstrong\u003eNUnit\u003c/strong\u003e enjoy broad support across IDEs, build systems, and test runners; they’re the de facto standards for mature teams.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTUnit\u003c/strong\u003e works seamlessly through the \u003ccode\u003eMicrosoft.Testing.Platform\u003c/code\u003e layer, so it integrates well with existing tools and workflows. It works in Visual Studio, other IDEs and the CLI.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eFor greenfield projects, this is acceptable. For enterprise ecosystems with thousands of tests, it\u0026rsquo;s currently a deal-breaker, though automatic migration tools are emerging to address this limitation.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"maintainability-and-lifecycle-considerations\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#maintainability-and-lifecycle-considerations\" title=\"Maintainability and Lifecycle Considerations\"\u003eMaintainability and Lifecycle Considerations\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTUnit’s design aligns well with modern .NET runtime evolution — it’s built for SDK-level integration and AOT compatibility.\nHowever, unlike MSTest, it doesn’t follow Microsoft’s LTS cadence, which means \u003cstrong\u003efaster iteration\u003c/strong\u003e but \u003cstrong\u003eless predictable stability\u003c/strong\u003e.\u003c/p\u003e\n\u003cp\u003eThat’s both opportunity and risk:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eMSTest\u003c/strong\u003e is safe but slow-moving.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003exUnit/NUnit\u003c/strong\u003e are stable and predictable.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTUnit\u003c/strong\u003e evolves rapidly, reflecting the latest language and SDK advances.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eFor teams comfortable with early adoption, that’s an advantage. For conservative enterprise stacks, it introduces change management overhead.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"adoption-guidance\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#adoption-guidance\" title=\"Adoption Guidance\"\u003eAdoption Guidance\u003c/a\u003e\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eScenario\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eRecommendation\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eNew .NET 10+ projects\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e✅ Worth adopting; future-ready and performance-efficient\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003ePerformance-critical CI pipelines\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e✅ Pilot candidate\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eExisting MSTest/xUnit/NUnit suites\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e⚠️ Defer migration until ecosystem matures\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eLong-term enterprise projects (LTS)\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e❌ Too early; lifecycle alignment uncertain\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003eA reasonable approach is hybrid adoption: start with new modules or performance-sensitive components, measure, and expand only if the ROI is tangible.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"the-business-view-value-cost-risk\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#the-business-view-value-cost-risk\" title=\"The Business View: Value, Cost, Risk\"\u003eThe Business View: Value, Cost, Risk\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eAt its core, the choice of testing framework is not a technical one — it’s architectural.\nThe framework defines reliability, maintainability, and operational efficiency for years.\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eMSTest\u003c/strong\u003e guarantees continuity and corporate integration — ideal where risk avoidance trumps innovation.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003exUnit\u003c/strong\u003e offers balance — modern yet stable, performant yet well-supported.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eNUnit\u003c/strong\u003e remains feature-rich but leans toward legacy or test-heavy applications.\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eTUnit\u003c/strong\u003e pushes testing forward — faster discovery, AOT readiness, smarter concurrency — but its youth carries risk.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eThe decision is ultimately about \u003cem\u003etiming\u003c/em\u003e: adopting too early adds cost; adopting too late loses competitive edge.\u003c/p\u003e\n\n\n\n\n\u003ch2 id=\"final-thoughts\"\u003e\u003ca href=\"/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/#final-thoughts\" title=\"Final Thoughts\"\u003eFinal Thoughts\u003c/a\u003e\u003c/h2\u003e\n\u003cp\u003eTUnit represents the direction .NET testing is headed — toward compile-time determinism, deeper runtime integration, and minimal overhead.\nIt’s technically elegant and forward-looking, but still maturing.\u003c/p\u003e\n\u003cp\u003eFor most organizations today, the pragmatic answer is balance:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eKeep \u003cstrong\u003eMSTest\u003c/strong\u003e, \u003cstrong\u003exUnit\u003c/strong\u003e, and \u003cstrong\u003eNUnit\u003c/strong\u003e where stability matters.\u003c/li\u003e\n\u003cli\u003ePilot \u003cstrong\u003eTUnit\u003c/strong\u003e where innovation pays off.\u003c/li\u003e\n\u003cli\u003eMeasure, not assume.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eIn short: \u003cstrong\u003eTUnit is not a replacement (yet) for all teams, but a glimpse of the future.\u003c/strong\u003e\nAnd as always in architecture, progress is best managed, not rushed.\u003c/p\u003e\n","date_modified":"2026-06-11T17:00:40+02:00","date_published":"2025-10-09T11:30:00+02:00","id":"https://daily-devops.net/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/","language":"en","summary":"A pragmatic TUnit evaluation for .NET teams - comparing performance, maintainability, and ecosystem readiness against MSTest, xUnit, and NUnit frameworks.","tags":["architecture","bestpractices","dotnet","performance","rcda","softwareengineering","testing","tunit"],"title":"TUnit — A Pragmatic Evaluation for .NET Teams\n","url":"https://daily-devops.net/posts/tunit-a-pragmatic-evaluation-for-dotnet-teams/"}],"language":"en","title":"TUnit: Modern .NET Test Framework on Daily DevOps \u0026 .NET","version":"https://jsonfeed.org/version/1.1"}