.NET Development

Unit Testing in .NET: Where to Start

A practical starting guide to unit testing in .NET with xUnit, NUnit or MSTest, what to test first and how to keep tests useful on .NET Core 3.1 and .NET Framework.

Quick answer: Start .NET unit testing by picking one framework (xUnit, NUnit or MSTest), adding a test project beside your app, and covering pure business logic before controllers and UI. Write small tests that arrange data, act on one method and assert one outcome. Use a mocking library such as Moq when you need to isolate dependencies. On April 2020 stacks that usually means .NET Core 3.1 or .NET Framework with a clear separation between testable domain code and infrastructure.

Why unit tests earn their keep

Unit tests catch regressions while the change is still cheap. They also force clearer design: methods that need twelve services to construct are hard to test and hard to maintain. You do not need full coverage charts on day one. You need a habit of protecting the rules that lose money when they break: pricing, permissions, tax, inventory math, validation.

If your team is weighing runtimes, our comparison of .NET vs Node.js stays relevant for architecture talks. Testing strategy still matters on either side, but this guide stays with .NET tooling.

Choose a test framework (and stop shopping)

All three common options work:

  • xUnit: popular in .NET Core samples, clean attribute model, parallel by default
  • NUnit: long history, flexible assertions, familiar to many Framework teams
  • MSTest: ships comfortably in Visual Studio-centric shops

Pick one per solution. Mixing frameworks in a single repo without a reason creates noise. For new .NET Core 3.1 work, xUnit is a solid default because templates and docs assume it. For older Framework monoliths already on MSTest, stay there until you migrate.

Solution layout that scales

A typical layout:

  • src/MyApp.Domain (entities, domain services, pure logic)
  • src/MyApp.Infrastructure (EF, file system, email, HTTP)
  • src/MyApp.Web (UI or API)
  • tests/MyApp.Domain.Tests

Reference only what you must. Domain tests should not spin up IIS Express or a full database to check a discount calculation. If they do, the logic lives in the wrong project.

When you migrate classic apps, keep testing in mind during the move. Our Classic ASP migration guide is about platform change, but the same lesson applies: peel logic into places you can test without the old runtime.

Your first useful tests

Start with code that is:

  • Pure or nearly pure (inputs in, outputs out)
  • Responsible for money, access or compliance rules
  • Already buggy once in production memory

Example targets: coupon stacking rules, password policy helpers, date window calculations, mapping from DTOs to domain objects.

Avoid starting with:

  • Private methods reached only by reflection
  • Thin wrappers around Entity Framework
  • Razor view rendering

Those belong later, often as integration tests.

Arrange, act, assert

Keep a consistent shape:

  1. Arrange: build inputs and dependencies
  2. Act: call the method under test
  3. Assert: check one meaningful outcome (or a tight cluster)

Name tests by behaviour: CalculateTotal_AppliesMemberDiscount_WhenUserIsMember. Cute names that joke about the bug waste time in CI logs.

Mocking with Moq (and when not to)

Moq is widely used in 2020 .NET teams to substitute interfaces. Mock:

  • Email gateways
  • Clock/time providers when time matters
  • Remote HTTP APIs
  • Repositories when you are testing domain services, not SQL

Do not mock everything. If a class is only a pass-through to a mock, the test asserts nothing real. Prefer fakes for in-memory repositories when the behaviour under test is how your code uses data, not how Moq was configured.

.NET Core 3.1 vs .NET Framework notes

.NET Core 3.1: dotnet new xunit, dotnet test, and SDK-style projects make CI straightforward on Windows and Linux agents.

.NET Framework: you can still use xUnit or NUnit with packages, but build agents and path assumptions differ. Keep test projects on a language version your CI image supports.

Either way, run tests on every pull request. Tests that only run on one developer’s laptop are documentation, not safety.

Testing ASP.NET behaviour without boiling the ocean

For API controllers:

  • Unit test filters and services heavily
  • Use integration tests (WebApplicationFactory on Core, or self-host patterns on older stacks) for routing and auth wiring
  • Keep controller bodies thin so unit tests stay rare there

For SQL-heavy features, pair a few integration tests against a disposable database with many unit tests around query shaping and business rules. Query performance issues are a different discipline; see our SQL Server performance guide when slow queries, not logic bugs, are the pain.

API contract checks with tools like Postman complement unit tests; they answer different questions about HTTP surfaces. Keep them in a separate suite from pure unit tests.

Coverage without theatre

Coverage percentage is a flashlight, not a goal. One hundred percent coverage with weak assertions still ships bugs. Prefer:

  • Critical domain modules above a high bar
  • New code reviewed with tests attached
  • Bug fixes that arrive with a regression test

Delete tests that only mirror implementation detail and break on every rename. They slow teams down and train people to ignore red builds.

CI checklist

  • Restore and build
  • dotnet test (or VSTest) with failures blocking merge
  • Optional: category attributes so smoke tests run first on huge suites
  • Publish test results as artifacts for triage

Flaky tests need quarantine or repair the same week. A noisy suite is worse than a small strict one.

Common beginner mistakes

  • Testing private state instead of behaviour
  • One giant test project with no namespaces
  • Sharing mutable static state between tests
  • Hitting real third-party APIs in unit tests
  • Asserting on exact exception messages that change often
  • Copy-pasting huge arrange blocks instead of builders or fixtures

A practical first week plan

Day 1: add test project, wire CI, write three tests around one pure service.

Day 2 to 3: extract hard-to-test logic out of controllers into services; cover the new services.

Day 4: add Moq for one external gateway; cover failure paths (timeout, 404, invalid payload).

Day 5: document how to run tests in the README; agree that bug fixes need a test unless the team explicitly waives it.

Working with legacy code

You will not wrap an old Framework monolith in perfect tests overnight. Characterise the module you are about to change: write tests that lock current behaviour, change the code, then refactor with a safety net. Seam introduction (extract interface, move pure functions) beats rewriting everything to chase coverage numbers.

FAQ

xUnit, NUnit or MSTest: which is “best”?

The best one is the one your team will run daily. Feature differences are smaller than consistency.

Do we need unit tests if we have QA?

Yes. QA finds gaps in flows. Unit tests protect rules at the speed of compile. They complement, not replace, exploratory testing.

Should we test Entity Framework mappings?

Prefer a few integration tests for mappings and many unit tests for rules that use the data. EF internals are not your product.

Is TDD required?

No. Writing tests with the change, or immediately after for tricky logic, still delivers value. Strict TDD is optional; regression safety is not.

Build the habit before the cathedral

Unit testing in .NET starts small: one framework, one project, the rules that hurt when they break. Expand as the design allows seams. If you need help structuring .NET applications so logic is testable and deployable, see our Custom development service.

About the author

Imran M

Leave a Reply

Your email address will not be published. Required fields are marked *

Ready to improve your website?

Let's build a faster, higher performing website.