10. Testing (Advanced)

ASP.NET Core Testing - Complete Guide

Practical Guide + Technical Reference


πŸ“‹ Table of Contents

Part 1: Practical Guide (Hands-On)

  1. Testing Fundamentals

  2. Unit Testing Setup (3 Methods)

  3. Integration Testing Setup (3 Methods)

  4. Testing Controllers & APIs

  5. Testing Middleware

  6. Testing with Databases

  7. Common Testing Patterns

  8. Mocking & Test Doubles

  9. Troubleshooting Common Issues

  10. Best Practices

Part 2: Technical Reference (Deep Dive)

  1. Important Testing Interfaces & Classes Reference

  2. Configuration Deep-Dive

  3. Advanced Testing Topics

  4. Performance Testing


PART 1: PRACTICAL GUIDE


1. Testing Fundamentals

Simple Definition: Writing code that automatically verifies your application works correctly.

Think of it like: A quality control inspector checking every part of a car before it leaves the factory.

Testing Pyramid

Key Principles:

  1. Unit Tests - Test one thing in isolation

  2. Integration Tests - Test components working together

  3. End-to-End Tests - Test entire workflows


Types of Tests in ASP.NET Core

Type
What It Tests
Speed
Cost
Quantity

Unit

Single class/method

⚑ Fast (ms)

πŸ’° Cheap

πŸ”’ Many (70%)

Integration

API endpoints + DB

🐒 Medium (100-500ms)

πŸ’°πŸ’° Medium

πŸ”’ Some (20%)

E2E

Full user flow

🐌 Slow (seconds)

πŸ’°πŸ’°πŸ’° Expensive

πŸ”’ Few (10%)


2. Unit Testing Setup (3 Methods)

Method 1: xUnit Only (Simple & Quick)

When to use:

  • βœ… Simple testing needs

  • βœ… Learning

  • βœ… Small projects

  • ❌ Need advanced mocking

  • ❌ Complex assertions

Step 1: Create Test Project

Step 2: Write Your First Test

Step 3: Run Tests


When to use:

  • βœ… Production code

  • βœ… Need to mock dependencies

  • βœ… Readable assertions

  • βœ… Testing services with DI

Step 1: Create Test Project with Packages

Step 2: Test Service with Dependencies


Method 3: xUnit + NSubstitute + Shouldly (Alternative)

When to use:

  • βœ… Prefer fluent mocking syntax

  • βœ… Alternative to Moq

  • βœ… Different assertion style

Step 1: Install Packages

Step 2: Write Tests


3. Integration Testing Setup (3 Methods)

Method 1: WebApplicationFactory (Standard)

When to use:

  • βœ… Testing API endpoints

  • βœ… In-memory database

  • βœ… Full HTTP request/response

  • βœ… Production-like testing

Step 1: Install Package

Step 2: Create Test Class

Step 3: Make Program.cs Accessible


When to use:

  • βœ… Need custom configuration

  • βœ… Test database instead of real one

  • βœ… Replace services for testing

  • βœ… Integration testing in CI/CD

Step 1: Create Custom Factory

Step 2: Use Custom Factory


Method 3: TestServer (Lightweight)

When to use:

  • βœ… Unit testing middleware

  • βœ… Don't need full HTTP client

  • βœ… Faster than WebApplicationFactory

Step 1: Create TestServer


4. Testing Controllers & APIs

Testing GET Endpoints


Testing POST Endpoints


Testing PUT Endpoints


Testing DELETE Endpoints


Testing Controller Actions Directly (Unit Testing)


5. Testing Middleware

Unit Testing Middleware


Integration Testing Middleware


6. Testing with Databases

Method 1: In-Memory Database (Fast, Simple)

When to use:

  • βœ… Fast tests

  • βœ… Don't need specific DB features

  • ❌ Testing SQL queries

  • ❌ Testing transactions


Method 2: SQLite In-Memory (Better for Real SQL)

When to use:

  • βœ… Need real SQL behavior

  • βœ… Test migrations

  • βœ… Fast tests

  • βœ… Production-like queries


Method 3: Test Containers (Real Database) ⭐ Most Realistic

When to use:

  • βœ… CI/CD pipelines

  • βœ… Need exact production DB

  • βœ… Test complex queries

  • ⚠️ Slower than in-memory


7. Common Testing Patterns

AAA Pattern (Arrange-Act-Assert)


Test Fixtures (Shared Setup)


Testing Async Methods


Testing Exceptions


8. Mocking & Test Doubles

Mock Setup Patterns


Argument Matchers


Mock vs Stub vs Fake


9. Troubleshooting Common Issues

Issue 1: Tests Interfering with Each Other

Problem: Tests pass individually but fail when run together

Solution 1: Use unique database names

Solution 2: Cleanup between tests


Issue 2: Async Tests Hanging

Problem: Test never completes


Issue 3: Mock Not Being Called

Problem: mockRepo.Verify() fails


Issue 4: Integration Tests Not Finding Program

Problem: WebApplicationFactory<Program> fails

Solution: Make Program.cs accessible


Issue 5: Database Context Disposed

Problem: "Cannot access a disposed object"


10. Best Practices

Naming Conventions


Test Organization


Test Data Builders


DRY Principle (Don't Repeat Yourself)


Test Categories (Tags)


Code Coverage


Best Practices Checklist


PART 2: TECHNICAL REFERENCE


11. Important Testing Interfaces & Classes Reference

IClassFixture

Namespace: Xunit

Purpose: Share setup/cleanup code across tests in a class

Declaration:

Usage:


IAsyncLifetime

Namespace: Xunit

Purpose: Async initialization and cleanup

Declaration:

Members:

Member
Return Type
Description

InitializeAsync()

Task

Called before each test

DisposeAsync()

Task

Called after each test

Usage:


WebApplicationFactory

Namespace: Microsoft.AspNetCore.Mvc.Testing

Purpose: Create a test server for integration testing

Declaration:

Important Members:

Member
Type
Description

CreateClient()

HttpClient

Create HTTP client for testing

CreateDefaultClient()

HttpClient

Create client with default settings

Server

TestServer

Access underlying test server

Services

IServiceProvider

Access DI container

WithWebHostBuilder()

WebApplicationFactory<T>

Customize configuration

Usage:

Customization:


Mock (Moq Library)

Namespace: Moq

Purpose: Create mock objects for testing

Important Members:

Member
Description

Setup()

Configure method behavior

Returns()

Specify return value

ReturnsAsync()

Specify async return value

Throws()

Specify exception to throw

Callback()

Execute custom logic

Verify()

Verify method was called

VerifyNoOtherCalls()

Ensure no other methods called

Object

Get mocked instance

Setup Methods:

Verify Methods:


It Class (Moq Argument Matching)

Namespace: Moq

Purpose: Match method arguments in mock setups

Static Methods:

Method
Description
Example

It.IsAny<T>()

Any value of type T

It.IsAny<int>()

It.Is<T>(predicate)

Value matching predicate

It.Is<int>(x => x > 0)

It.IsIn<T>(values)

Value in collection

It.IsIn(1, 2, 3)

It.IsInRange<T>(from, to, range)

Value in range

It.IsInRange(1, 100, Range.Inclusive)

It.IsRegex(pattern)

String matching regex

It.IsRegex(@"^\d+$")

Usage Examples:


FluentAssertions Extensions

Namespace: FluentAssertions

Purpose: Readable assertions

Common Assertions:


Times (Moq Verification)

Namespace: Moq

Purpose: Specify expected call count

Static Members:

Member
Description

Times.Never

Should not be called

Times.Once

Should be called exactly once

Times.Exactly(n)

Should be called exactly n times

Times.AtLeastOnce

Should be called at least once

Times.AtLeast(n)

Should be called at least n times

Times.AtMostOnce

Should be called at most once

Times.AtMost(n)

Should be called at most n times

Times.Between(min, max, range)

Should be called between min and max

Usage:


TestServer

Namespace: Microsoft.AspNetCore.TestHost

Purpose: Lightweight in-memory test server

Important Members:

Member
Type
Description

CreateClient()

HttpClient

Create HTTP client

CreateRequest(path)

RequestBuilder

Build custom request

CreateWebSocketClient()

WebSocketClient

Create WebSocket client

Services

IServiceProvider

Access DI container

Host

IHost

Access host

Usage:


12. Configuration Deep-Dive

Pattern 1: In-Memory Database (Hardcoded)

When to use: Simple unit tests, no real DB needed

Pros:

  • βœ… Fast

  • βœ… Simple

  • βœ… No external dependencies

Cons:

  • ❌ Not real SQL

  • ❌ Can't test database-specific features

  • ❌ Potential false positives


Pattern 2: SQLite In-Memory (Code-based)

When to use: Need real SQL behavior

Pros:

  • βœ… Real SQL behavior

  • βœ… Fast

  • βœ… Test migrations

  • βœ… Better than EF In-Memory

Cons:

  • ⚠️ SQLite != SQL Server/PostgreSQL

  • ⚠️ Some features different


Pattern 3: Test Containers (Production-like)

When to use: Integration tests, CI/CD

Pros:

  • βœ… Exact production database

  • βœ… Test real features

  • βœ… Test migrations

  • βœ… Repeatable

Cons:

  • ⚠️ Slower

  • ⚠️ Requires Docker

  • ⚠️ More complex setup


Configuration Comparison

Approach
Speed
Accuracy
Complexity
Use Case

In-Memory

⚑⚑⚑

⚠️ Low

⭐ Simple

Unit tests

SQLite

⚑⚑

⚠️ Medium

⭐⭐ Medium

Unit/Integration

Test Containers

⚑

βœ… High

⭐⭐⭐ Complex

Integration/CI

Real DB

🐌

βœ… Highest

⭐⭐⭐ Complex

Manual testing


13. Advanced Testing Topics

Testing Background Services


Testing SignalR Hubs


Testing gRPC Services


Testing with Time (Fake Clock)


Snapshot Testing


Property-Based Testing


Mutation Testing

What it does: Changes your code slightly (mutates) to verify tests catch the changes


14. Performance Testing

Benchmark Testing with BenchmarkDotNet


Load Testing


Response Time Testing


Summary: Complete Testing Checklist

Unit Testing:

Integration Testing:

Best Practices:

Common Patterns:


This completes the Testing guide combining practical hands-on content with deep technical reference!

Last updated