Introduction: Why Testing in C#?
\nIn modern C# (.NET) development, code quality is not a luxury, but a necessity. Testing not only helps catch bugs before they reach production but also serves as living documentation and accelerates refactoring. There are three main levels of testing that form the "test pyramid": Unit, Integration, and E2E. In this article, we will break down each of them with real C# code examples.
\n\n1. Unit Testing: The Foundation of Quality
\nUnit tests verify an isolated piece of code (method, class) without external dependencies (databases, file systems, APIs). In the .NET ecosystem, three frameworks are popular: xUnit (recommended for new projects), NUnit, and MSTest.
\n\nCore Principles (FIRST)
\n- \n
- Fast — fast (milliseconds). \n
- Isolated — isolated (use mocks). \n
- Repeatable — repeatable. \n
- Self-validating — self-validating (Assert). \n
- Timely — timely. \n
Unit Test Example with xUnit and Moq
\nSuppose we have a service for calculating discounts:
\npublic interface IDiscountService\n{\n decimal CalculateDiscount(decimal price, bool isLoyaltyMember);\n}\n\npublic class DiscountService : IDiscountService\n{\n public decimal CalculateDiscount(decimal price, bool isLoyaltyMember)\n {\n if (isLoyaltyMember) return price * 0.9m; // 10% discount\n return price;\n }\n}\nTest using Moq (for mocking dependencies) and xUnit:
\nusing Xunit;\nusing Moq;\n\npublic class DiscountServiceTests\n{\n [Fact]\n public void CalculateDiscount_WhenLoyaltyMember_ReturnsDiscountedPrice()\n {\n // Arrange\n var service = new DiscountService();\n var price = 100m;\n\n // Act\n var result = service.CalculateDiscount(price, true);\n\n // Assert\n Assert.Equal(90m, result);\n }\n\n [Theory]\n [InlineData(100, false, 100)]\n [InlineData(200, true, 180)]\n public void CalculateDiscount_MultipleCases_ReturnsCorrectValues(decimal price, bool isLoyalty, decimal expected)\n {\n var service = new DiscountService();\n var result = service.CalculateDiscount(price, isLoyalty);\n Assert.Equal(expected, result);\n }\n}\nTip: Use the [Fact] attribute for single tests and [Theory] + [InlineData] for parameterized tests.
2. Integration Testing: Verifying Component Interaction
\nIntegration tests check how multiple modules work together — for example, a controller, a service, and a real database (or its substitute, such as in-memory SQLite). In ASP.NET Core, WebApplicationFactory is used for this purpose.
Setting Up WebApplicationFactory
\nLet's create a test class that spins up a test web host:
\nusing Microsoft.AspNetCore.Mvc.Testing;\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.DependencyInjection;\nusing MyApp.Data;\n\npublic class CustomWebApplicationFactory : WebApplicationFactory<Program>\n{\n protected override void ConfigureWebHost(IWebHostBuilder builder)\n {\n builder.ConfigureServices(services =>\n {\n // Remove the real DbContext\n var descriptor = services.SingleOrDefault(\n d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));\n if (descriptor != null) services.Remove(descriptor);\n\n // Add in-memory database\n services.AddDbContext<AppDbContext>(options =>\n