Browse Source

Merge pull request #23752 from abpframework/yagmur-article

Add article on integration testing best practices
pull/23774/head
Alper Ebiçoğlu 1 year ago
committed by GitHub
parent
commit
ae7b7a9987
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 257
      docs/en/Community-Articles/2025-09-17-Integration-Testing-Best-Practices-for-Building-a-Robust-Application-Layer/article.md
  2. BIN
      docs/en/Community-Articles/2025-09-17-Integration-Testing-Best-Practices-for-Building-a-Robust-Application-Layer/cover.png

257
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<Startup>();
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<Order>();
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<TaskCanceledException>(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<IPaymentService>();
paymentServiceMock.Setup(x => x.ProcessPayment(It.IsAny<decimal>(), It.IsAny<string>()))
.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.

BIN
docs/en/Community-Articles/2025-09-17-Integration-Testing-Best-Practices-for-Building-a-Robust-Application-Layer/cover.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

Loading…
Cancel
Save