Blog
Mert Özen's blog posts on software development, backend, frontend, and DevOps.
From xUnit v2 to v3 and 4.0: Reading and Migrating Old Test Projects
Migrating an xUnit v2 project to v3 means collapsing the packages into one, making the project an executable and changing a few signatures; the real risk when moving to 4.0 is extensibility code that compiles but behaves differently. A custom ITestCaseOrderer written for 3.x compiled without warnings on 4.0.2 and silently stopped ordering methods. A step-by-step migration with real build errors, dotnet new xunit still producing v2, xunit-console-tool and a checklist.
Snapshot and Property-Based Testing: Extending Unit Tests with Verify and FsCheck
Property-based testing tries a rule that holds for every input with random inputs, and snapshot testing approves a large output as a whole; both complement example tests. FsCheck found the negative total that three example tests missed on its first try and shrank it to a 0 EUR cart and a 1 EUR coupon; Verify caught a property rename as a contract change. Shrinking, custom arbitraries, scrubbers and Verify 33's maintenance fee check.
Hard Cases in Unit Testing: Async Code, Time, Exceptions, Logging and Background Services
With code tested through FakeTimeProvider, the rule is: start the operation, advance the clock, then await; a retry test that awaited first hit its 2-second timeout because the clock never moved. Verifying backoff as a pure function, the retry flow with a fake clock, exceptions by type and data, the critical log with FakeLogger, cancellation, and a BackgroundService consuming a Channel<T> with deterministic tests.
Keeping Test Code Clean: Test Data Builders, Object Mothers, AutoFixture and Bogus
The rule for test data is simple: the value a test relies on is written explicitly in the test, and the rest is left to meaningful defaults or anonymous data. A shipping test written with unseeded Bogus data failed in 6 of 30 runs. Test Data Builders, the growth problem of Object Mothers, AutoFixture and AutoData, AutoNSubstitute's NU1608 warning with NSubstitute 6, seeded Bogus, and the DRY vs DAMP balance in test code.
Testable Design: Dependency Injection, Seams, Functional Core and Humble Object
What makes code testable is moving decisions into a pure core and leaving time, I/O and framework code in a thin shell; the decisions can then be tested without mocks. A discount test using DateTime.Now depended on the calendar and hid the Istanbul vs UTC gap. Kinds of seams, TimeProvider, functional core / imperative shell, Humble Object with BackgroundService, and characterization tests for legacy code.
When Not to Mock: Over-Mocking, Brittle Tests and "Don't Mock What You Don't Own"
Mocks belong only at the system boundary, for unmanaged dependencies observable from outside; domain objects and the application's own database are not mocked. A mocked exchange rate adapter kept tests green while production showed 100 EUR as 0 TRY; an HttpMessageHandler stub caught the bug on the first run. Output, state and communication test styles and the managed vs unmanaged distinction.
Mocking Libraries in Depth: NSubstitute, Moq and FakeItEasy
NSubstitute, Moq and FakeItEasy share the same Castle DynamicProxy engine, so they can only fake interfaces and virtual members, and the differences between them are mostly syntax and failure messages. The same payment scenarios in all three: argument matching and capture, sequential returns, async exceptions, call count and order, strict mode, the NS1000 analyzer warning and Moq's 2023 SponsorLink incident.
Test Doubles: The Difference Between Dummy, Stub, Spy, Mock and Fake
A test double's kind is decided by its role in the test, not by its library: a dummy fills a slot, a stub supplies data, spies and mocks verify outgoing calls, and a fake is a working shortcut. The Meszaros taxonomy, Fowler's state vs behavior verification, why asserting on stubs hurts, how a fake that does not copy records hid a missing SaveAsync, and FakeTimeProvider and FakeLogger.
The xUnit Test Lifecycle: Constructors, IAsyncLifetime, Fixtures and Full Parallelism in 4.0
xUnit creates a new instance of the test class for every test; expensive, read-only setup is done once in a class, collection or assembly fixture. In 4.0, ParallelMode.All cut an 8-test example from 1.06 to 0.55 seconds, but two tests sharing a static fake failed in 16 of 20 runs. IAsyncLifetime, opt-out levels, ITestOutputHelper and TestContext attachments.
Data-Driven Tests in xUnit v3: Theory, MemberData, TheoryData, TheoryDataRow and MatrixTheoryData
In data-driven tests, which rows you pick matters more than how many: boundary values and equivalence partitioning caught Math.Round's banker's rounding with two rows. In xUnit v3: InlineData, MemberData and type-safe TheoryData, TheoryDataRow with per-row display name, label, trait and skip, MatrixTheoryData, dynamic skip, Explicit tests, and how non-serializable data affects Test Explorer.
Anatomy of a Good Unit Test: AAA, Naming, One Behavior and Readable Assertions
A good unit test runs one behavior through a single Act, contains no branching, and when it turns red its name and failure message explain the problem without opening the code. The limits of AAA, Method_Scenario_Expected vs plain-sentence naming, Assert.Multiple, and Shouldly, AwesomeAssertions and FluentAssertions 8's Xceed license, with real failure messages.
A Test Project with xUnit v3: Microsoft Testing Platform, dotnet test and First Tests
An xUnit.net v3 test project is an executable and, since 4.0, runs on Microsoft Testing Platform v2 by default; once global.json sets the test runner on the .NET 10 SDK, dotnet test rejects VSTest projects. The xunit3 template, the package chain, dotnet test vs dotnet run, Assert.Throws/ThrowsAsync, TestContext.Current.CancellationToken and xUnit1051, filters, TRX reports and exit code 8.