diff --git a/docs/en/Community-Articles/2025-09-17-Integration-Testing-Best-Practices-for-Building-a-Robust-Application-Layer/article.md b/docs/en/Community-Articles/2025-09-17-Integration-Testing-Best-Practices-for-Building-a-Robust-Application-Layer/article.md new file mode 100644 index 0000000000..f5e02c6ff3 --- /dev/null +++ b/docs/en/Community-Articles/2025-09-17-Integration-Testing-Best-Practices-for-Building-a-Robust-Application-Layer/article.md @@ -0,0 +1,257 @@ +# Integration Testing Best Practices for Building a Robust Application Layer + +## 1. Introduction to Integration Testing + +For software development purposes, it is not sufficient to validate individual components individually in a vacuum. In practical usage, programs consist of holistic systems comprising a myriad of components such as **databases, services, APIs, and existing tools**, which **must work together**. An application would fail testing parameters for an individual component sufficiently but fail as a whole if such components fail to interact appropriately. + +Whereas unit tests verify individual parts one by one, **integration tests confirm software reliability with integrated parts**. Integration testing guarantees that the overall system performs as per design parameters whenever parts of the system are interfederated. + +**Why Integration Testing Is Important:** + +* **Reliability:** Checks the system works as it should, even with network problems, service stops, or incorrect data. +* **Easy to Update:** Makes sure adding or changing parts doesn't break what already works. +* **Good Quality:** Finds problems before users do. +* **Strong Application Layer:** Checks that database actions, service handling, and API communications all work together correctly. + +**Example: Detailed Integration Test with Dependency Injection** + +```csharp +[Fact] +public async Task OrderCreation_WithValidInput_ShouldPersistAndReturnSuccess() +{ + // Set up: Use a complete Test Server + var factory = new WebApplicationFactory(); + var client = factory.CreateClient(); + + // Do: + var request = new CreateOrderRequest { ProductId = 1, Quantity = 2 }; + var response = await client.PostAsJsonAsync("/api/orders", request); + + // Check + response.EnsureSuccessStatusCode(); // Stops if the code isn't successful + var order = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(order); + Assert.Equal(2, order.Quantity); +} +``` + +This example uses a memory `TestServer` to act like the full HTTP process, from the controller to the database, for a more real test. + +## 2. Setting Up an Isolated Integration Test Environment + +Integration tests should happen in their own area to prevent: + +* **Slow tests** because of network issues or big data amounts +* **Unreliable results** from live data changes +* **Tests messing with each other** + +**Tips for Keeping Tests Separate:** + +* **Memory databases:** Fast and easy to reset for simple data tasks. +* **Container-based areas (Docker/TestContainers):** Copy real areas safely for complex setups (like PostgreSQL, Redis). +* **Database Actions:** Undo changes after each test to keep things separate and fast. + +**Example: Using Actions to Keep Things Separate** + +```csharp +[Fact] +public async Task OrderCreation_RollsBack_AfterTest() +{ + // Set up: Start an action that will be undone later + await using var transaction = await _dbContext.Database.BeginTransactionAsync(); + + var service = new OrderService(_dbContext); + var result = service.CreateOrder(1, 2); + Assert.True(result.IsSuccess); + + // Do & Check + var orderCount = await _dbContext.Orders.CountAsync(); + Assert.Equal(1, orderCount); + + // Clean up: Undo the action to reset all changes + await transaction.RollbackAsync(); +} +``` + +Using actions makes sure tests stay separate and can run at the same time safely. + +## 3. Seeding Test Data for Consistent Integration Results + +Start data gives a steady base for tests. Include complex links for real situations. + +```csharp +// Use a special way to get the data started +private static async Task SeedData(AppDbContext dbContext) +{ + await dbContext.Products.AddRangeAsync( + new Product { Id = 1, Name = "Coffee", Price = 5, Stock = 50 }, + new Product { Id = 2, Name = "Tea", Price = 3, Stock = 20 } + ); + await dbContext.Users.AddAsync(new User { Id = 1, Name = "Alice", IsActive = true }); + await dbContext.Orders.AddAsync(new Order { Id = 1, UserId = 1, ProductId = 1, Quantity = 2 }); + + await dbContext.SaveChangesAsync(); +} +``` + +**Tip:** Use the Builder style or a special `TestSeeder` class to make detailed, reusable data setups. + +## 4. Validating API and Service Layer Interactions + +Integration tests should copy what real users do to check the whole request-response process. + +```csharp +// Detailed API Test with CancellationToken and Custom Headers +var request = new CreateOrderRequest { ProductId = 1, Quantity = 2 }; +var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/api/orders") +{ + Content = JsonContent.Create(request) +}; +requestMessage.Headers.Add("X-Request-Id", Guid.NewGuid().ToString()); + +// Act like a timeout is happening +var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50)); +await Assert.ThrowsAsync(async () => +{ + await _client.SendAsync(requestMessage, cts.Token); +}); +``` + +Test both normal uses and unusual ones like timeouts, bad requests, and too many users at once for a truly reliable API. + +## 5. Mocking External Dependencies in Integration Tests + +Use copies or fake versions for services like payment systems, emails, or other APIs to keep your application logic separate. + +```csharp +// Set up: Copy an outside payment service +var paymentServiceMock = new Mock(); +paymentServiceMock.Setup(x => x.ProcessPayment(It.IsAny(), It.IsAny())) + .ThrowsAsync(new TimeoutException("Payment gateway timeout")); + +// Do: Put the copy into the test area +services.AddScoped(_ => paymentServiceMock.Object); + +// Check: See how the system handles the timeout +var response = await _client.PostAsJsonAsync("/api/payments", new { Amount = 100 }); +Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); +Assert.Contains("Payment service unavailable", await response.Content.ReadAsStringAsync()); +``` + +Mimicking lets you act like things are failing (like APIs timing out or services being down) and check that your application handles these issues well without using real network calls. + +## 6. Covering Success and Failure Scenarios in Tests + +A good test set has full coverage of both successful actions and error handling. + +```csharp +// Normal use: A successful order creation +var validOrder = new CreateOrderRequest { ProductId = 1, Quantity = 1 }; +var successResponse = await _client.PostAsJsonAsync("/api/orders", validOrder); +Assert.Equal(HttpStatusCode.Created, successResponse.StatusCode); + +// Bad request: Wrong product ID +var invalidProductOrder = new CreateOrderRequest { ProductId = 999, Quantity = 1 }; +var badProductResponse = await _client.PostAsJsonAsync("/api/orders", invalidProductOrder); +Assert.Equal(HttpStatusCode.NotFound, badProductResponse.StatusCode); + +// Conflict: Not enough stock +var largeOrder = new CreateOrderRequest { ProductId = 1, Quantity = 1000 }; +var stockResponse = await _client.PostAsJsonAsync("/api/orders", largeOrder); +Assert.Equal(HttpStatusCode.Conflict, stockResponse.StatusCode); +``` + +Testing everything makes sure your application layer gives helpful and correct error messages to users. + +## 7. Ensuring Cleanup and Test Isolation + +Keep test results steady by making sure each test is totally separate from others. + +```csharp +// Use a special cleanup method or a test setup +public class OrderTests : IDisposable +{ + private readonly AppDbContext _dbContext; + + public OrderTests() + { + _dbContext = new AppDbContext(GetDbContextOptions()); + // Setup logic here + } + + // Cleanup logic + public void Dispose() + { + _dbContext.Database.EnsureDeleted(); + _dbContext.Dispose(); + } +} +``` + +Clean up automatically for tests that run at the same time using `IDisposable` or by putting each test in a database action. + +## 8. Automating Integration Tests with CI/CD Pipelines + +Automate tests in CI/CD lines for regular, consistent checking. + +```yaml +jobs: + test-and-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 7.0.x + - name: Build + run: dotnet build --configuration Release + - name: Run Integration Tests + run: dotnet test --filter Category=Integration --logger trx;LogFileName=testresults.trx +``` + +Regular testing finds integration problems early, saving effort over time. + +## 9. Complete Integration Test Example with a Basic Comparison + +```csharp +[Fact] +public async Task CompleteOrderWorkflow_ShouldPersistAllSteps() +{ + // Set up: Start complex data for a full action + await SeedData(_dbContext); + + // Do: Act like a real user request + var request = new CreateOrderRequest { ProductId = 1, Quantity = 2, UserId = 1 }; + var response = await _client.PostAsJsonAsync("/api/orders", request); + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + + // Check 1: See the database state + var order = await _dbContext.Orders.FirstOrDefaultAsync(o => o.UserId == 1 && o.ProductId == 1); + Assert.NotNull(order); + Assert.Equal(2, order.Quantity); + + // Check 2: See side effects (like stock going down) + var product = await _dbContext.Products.FirstOrDefaultAsync(p => p.Id == 1); + Assert.Equal(48, product.Stock); +} +``` + +🍪 **Cookie Comparison** + +* **Starting products** are like the dough you have ready. +* **The new order** is like the new stuff you mix in. +* **The oven (application)** mixes both, baking (business logic). +* **The cookie** is the right outcome, using the old dough and new stuff well. + +This shows how a good application layer uses new and old data well together. + +--- + +### 10. Summary: + +* **Separate Area:** Ensures tests run on their own. +* **Start Data:** Gives results that are consistent. +* **Mimicking Outside Parts:** Keeps tests stable. +* **Testing Both Good and Bad:** Makes the situation solid. +* **CI/CD Automation:** Keeps the system stable. diff --git a/docs/en/Community-Articles/2025-09-17-Integration-Testing-Best-Practices-for-Building-a-Robust-Application-Layer/cover.png b/docs/en/Community-Articles/2025-09-17-Integration-Testing-Best-Practices-for-Building-a-Robust-Application-Layer/cover.png new file mode 100644 index 0000000000..9ff6df4764 Binary files /dev/null and b/docs/en/Community-Articles/2025-09-17-Integration-Testing-Best-Practices-for-Building-a-Robust-Application-Layer/cover.png differ