π§ͺ Unit Testing and Integration Testing in ASP.NET Core
|
|
API Testing in .NET Core
In .NET Core, testing APIs involves two main approaches: unit testing and integration testing. A common practice is to have more unit tests, a moderate number of integration tests, and very few end-to-end tests β known as the testing pyramid.
Unit Testing
Unit testing focuses on testing individual components of your application in isolation from infrastructure and dependencies.
- Goal: Verify that a single method or unit of work performs the expected business logic.
- Isolation: Dependencies like databases or HTTP context are mocked.
-
Tools: xUnit, NUnit, MSTest, Moq, NSubstitute etc...
π§° Tools Commonly Used
- xUnit: Popular testing framework
- Moq: For mocking dependencies
- TestServer: For in-memory integration testing
- FluentAssertions: For expressive assertions
- Pattern: Arrange - Act - Assert (AAA).
Example: Unit Testing a Controller with Moq
// Service Interface
public interface IProductService
{
Task<Product?> GetProductByIdAsync(int id);
}
// Controller
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService;
}
[HttpGet("{id}")]
public async Task<ActionResult<Product>> Get(int id)
{
var product = await _productService.GetProductByIdAsync(id);
if (product == null)
return NotFound();
return product;
}
}
β
Unit Testing Example
// ProductService.cs
public class ProductService
{
public decimal CalculateDiscount(decimal price) => price * 0.9m;
}
// ProductServiceTests.cs
public class ProductServiceTests
{
[Fact]
public void CalculateDiscount_ReturnsCorrectValue()
{
var service = new ProductService();
var result = service.CalculateDiscount(100);
Assert.Equal(90, result);
}
}
Unit Test another Example
// Unit Test
public class ProductsControllerTests
{
[Fact]
public async Task Get_ProductExists_ReturnsProduct()
{
var mockService = new Mock<IProductService>();
mockService.Setup(s => s.GetProductByIdAsync(1))
.ReturnsAsync(new Product { Id = 1, Name = "Test Product" });
var controller = new ProductsController(mockService.Object);
var result = await controller.Get(1);
var okResult = Assert.IsType<ActionResult<Product>>(result);
var product = Assert.IsType<Product>(okResult.Value);
Assert.Equal(1, product.Id);
}
}
Integration Testing
Integration testing validates that multiple components work together within the full application pipeline.
- Goal: Ensure the full API pipeline works correctly.
- Dependencies: Real or in-memory implementations (like InMemoryDb).
- Tools: xUnit, NUnit, WebApplicationFactory.
β
Integration Testing Example
// ProductsController.cs
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok(new[] { "Laptop", "Phone" });
}
// ProductsIntegrationTests.cs
public class ProductsIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public ProductsIntegrationTests(WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task Get_ReturnsProductList()
{
var response = await _client.GetAsync("/api/products");
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("Laptop", content);
}
}
Another Example: Integration Testing with WebApplicationFactory
// Integration Test
public class ProductsApiIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public ProductsApiIntegrationTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Fact]
public async Task Get_ReturnsSuccessStatusCodeAndCorrectContentType()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/api/products");
response.EnsureSuccessStatusCode();
Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType?.ToString());
}
}
π Whatβs the Difference?
- Unit Testing: Tests individual components (e.g., controllers, services) in isolation using mocks or stubs.
- Integration Testing: Tests the full stack (controllers, middleware, database, etc.) to verify end-to-end behavior.
Key Differences Summary
| Aspect |
Unit Testing |
Integration Testing |
| Scope |
Tests a single component in isolation |
Tests multiple components working together |
| Dependencies |
Mocks and stubs |
Real or in-memory services |
| Speed |
Very fast |
Slower due to setup |
| Confidence |
Logic-level confidence |
System-level confidence |
| Tools |
xUnit, NUnit, Moq |
WebApplicationFactory, xUnit |
β
When to Use Each
- Unit Tests: For fast, isolated logic validation (services, helpers, controllers)
- Integration Tests: For verifying real-world behavior (routing, middleware, DB access)
π« When Not to Use
- Unit Tests: Avoid testing external systems (e.g., databases, APIs)
- Integration Tests: Avoid over-testing trivial logic or duplicating unit tests
π Advantages
- Improves code reliability and maintainability
- Facilitates refactoring with confidence
- Helps catch bugs early in development
β οΈ Disadvantages
- Requires setup and maintenance
- Integration tests can be slower and more brittle
- Over-testing can lead to false confidence or wasted effort
π§― Precautions
- Mock dependencies in unit tests to isolate logic
- Use test databases or in-memory stores for integration tests
- Run tests in CI/CD pipelines to catch regressions
π§ Best Practices
- Follow AAA pattern: Arrange, Act, Assert
- Use meaningful test names and organize by feature
- Keep tests fast and deterministic
- Use coverage tools to identify gaps