Even if an API passes every test in its own project, it might still cause problems for the apps that rely on it. An endpoint may begin delivering a new status code, a property name may change, or a response field may become nullable. During development, these modifications can seem insignificant, but they can lead to problems with frontend, mobile, and third-party integrations.
Because of this, evaluating an API entails more than just seeing if its endpoints provide the desired information.
You also need to verify that the API continues to follow the contract consumers rely on.
In ASP.NET Core, you can combine integration tests, response validation, OpenAPI document checks, and contract testing to catch breaking changes before release. This article explains how to build that testing approach using practical C# examples.
What Is an API Breaking Change?
An API breaking change occurs when an existing consumer can no longer use an API in the way it previously could.
For example, imagine an endpoint returns this JSON response:
{
"id": 101,
"name": "Laptop",
"price": 799.99
}A frontend application expects the name property to display the product title.
Now suppose a developer renames the property to productName:
{
"id": 101,
"productName": "Laptop",
"price": 799.99
}The API might still return HTTP 200, and its internal tests might pass. However, a consumer expecting name may stop displaying the product correctly.
Breaking changes can affect several parts of an API contract:
- Routes, HTTP methods, and query parameters.
- Request and response property names.
- Data types and nullability.
- HTTP status codes.
- Authentication and authorization requirements.
- Validation rules and error response formats.
- Pagination behavior and default values.
A good API testing strategy checks these behaviors, not just whether the application starts successfully.
Create an ASP.NET Core API Test Project
For this example, assume an ASP.NET Core application exposes a product endpoint.
A typical solution structure might look like this:
ProductApi/
Controllers/
Models/
Program.cs
ProductApi.Tests/
ProductApi.Tests.csproj
ProductEndpointTests.csThe test project should reference the API project and include a compatible version of Microsoft.AspNetCore.Mvc.Testing, along with a test framework such as xUnit.
The ASP.NET Core integration testing infrastructure provides WebApplicationFactory<TEntryPoint>, which allows tests to start the application in a test host and send HTTP requests without deploying it to a real server.
For applications that use top-level statements, you may need to expose the entry point to the test project. One common approach is to add the following declaration at the end of Program.cs:
public partial class Program
{
}This makes the generated Program type accessible to the test project when configured appropriately.
Write an Integration Test for an API Endpoint
Integration tests verify how the API behaves when the application processes an actual HTTP request. Unlike isolated unit tests, they can exercise routing, middleware, model binding, serialization, filters, and endpoint logic together.
Consider this response model:
public sealed record ProductResponse(
int Id,
string Name,
decimal Price);The API exposes a route such as GET /api/products/101.
A basic integration test can verify the status code and response data.
using System.Net;
using System.Net.Http.Json;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;
public sealed class ProductEndpointTests
: IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public ProductEndpointTests(
WebApplicationFactory<Program> factory)
{
_client = factory.CreateClient();
}
[Fact]
public async Task GetProduct_ReturnsExpectedResponse()
{
var response = await _client.GetAsync(
"/api/products/101");
Assert.Equal(
HttpStatusCode.OK,
response.StatusCode);
var product =
await response.Content
.ReadFromJsonAsync<ProductResponse>();
Assert.NotNull(product);
Assert.Equal(101, product.Id);
Assert.Equal("Laptop", product.Name);
Assert.Equal(799.99m, product.Price);
}
}This test assumes that the test environment provides a product with ID 101 and that the API returns the stated values. In a real project, arrange the required test data or replace the production data source with a test implementation.
The test verifies more than whether the endpoint returns HTTP 200. It also checks whether the response contains the expected values and can be deserialized into the expected model.
However, deserialization alone is not always enough. Depending on serializer settings and model definitions, an unexpected JSON property may be ignored, or a missing property may receive a default value. You should also test the response contract directly when the exact JSON shape matters.
Verify the JSON Response Contract
Suppose a consumer requires the name property to remain part of the response. You can inspect the raw JSON to catch accidental changes.
using System.Net;
using System.Text.Json;
using Xunit;
[Fact]
public async Task GetProduct_PreservesRequiredJsonProperties()
{
var response = await _client.GetAsync(
"/api/products/101");
Assert.Equal(
HttpStatusCode.OK,
response.StatusCode);
using var document = JsonDocument.Parse(
await response.Content.ReadAsStringAsync());
var root = document.RootElement;
Assert.True(
root.TryGetProperty("id", out var id));
Assert.True(
root.TryGetProperty("name", out var name));
Assert.True(
root.TryGetProperty("price", out var price));
Assert.Equal(101, id.GetInt32());
Assert.Equal("Laptop", name.GetString());
Assert.Equal(799.99m, price.GetDecimal());
}This test fails if the name property is renamed or removed. It also verifies the values and expected JSON types.
The JSON property names used by the test must match the API's actual serialization contract. ASP.NET Core applications commonly use camel-case JSON names, but serializer configuration can change that behavior.
When Should You Compare the Entire JSON Response?
Checking individual properties is useful when consumers depend on a small, stable set of fields.
For APIs with strict response contracts, you may also compare the complete JSON structure against an approved fixture or snapshot. This can detect changes to nested objects, property names, and response shapes.
Full-response comparisons need careful maintenance. A harmless addition may fail a strict comparison even when existing consumers remain compatible. Decide whether additional fields are allowed before choosing the test approach.
Test HTTP Status Codes and Error Responses
An endpoint's contract includes its failure behavior. Consumers may depend on receiving 404 Not Found for a missing resource or 400 Bad Request for invalid input.
For example:
[Fact]
public async Task GetUnknownProduct_ReturnsNotFound()
{
var response = await _client.GetAsync(
"/api/products/999999");
Assert.Equal(
HttpStatusCode.NotFound,
response.StatusCode);
}This test assumes the API returns 404 when the requested product does not exist.
You should also test invalid request data. For an endpoint that creates a product, useful scenarios include a missing required name, a negative price, malformed JSON, and an unauthenticated request.
The expected status code depends on the API's documented behavior. For example, an API might use 400 for validation errors and 401 when authentication is missing. Tests should reflect the actual contract rather than impose arbitrary expectations.
For applications that return structured validation errors, verify important fields in the error response as well. Otherwise, a change to the error format may break frontend validation messages even when the status code remains unchanged.
Detect Changes in the OpenAPI Document
Integration tests cover specific scenarios. An OpenAPI document provides a broader description of an API's public contract, including paths, methods, parameters, request bodies, response schemas, and security requirements.
Checking the OpenAPI document can help detect changes that individual endpoint tests do not cover.
For example, suppose an API previously exposed this operation:
paths:
/api/products/{id}:
get:
parameters:
- name: id
in: path
required: true
schema:
type: integerA later change might remove the endpoint, rename its route, or change the parameter type. A contract comparison can identify these differences before release.
Use OpenAPI Contract Checks
A practical approach is to generate the OpenAPI document during the build or from a controlled test environment, then compare it with the version approved for the current API release.
The comparison should identify changes such as:
- Removed paths or HTTP methods.
- Removed request or response properties.
- Changed property types.
- New required request fields.
- Changes to authentication requirements.
- Removed response status codes.
Not every difference is necessarily breaking. Adding an optional response property is often compatible with existing clients, while removing a required field may not be. The comparison tool and review process should account for the rules your API follows.
Also, OpenAPI checks cannot replace runtime tests. A document may describe the correct response schema even when the endpoint returns different data due to a serialization or business-logic problem.
Add Consumer-Driven Contract Testing
Integration tests usually focus on the API provider's expected behavior. Consumer-driven contract testing adds another perspective: it verifies the interactions that actual consumers rely on.
Imagine a product API used by a web application and a mobile application. The web application may depend on the product name and price, while the mobile application may also depend on an image URL.
A consumer-driven contract records the expected request and response for a specific interaction. The provider can then verify that its implementation still satisfies those expectations.
This approach is especially useful when multiple teams maintain separate services and deploy them independently.
| Testing approach | Main purpose | Best use |
|---|---|---|
| Unit tests | Verify isolated application logic | Business rules and validation |
| Integration tests | Verify real application request handling | Routing, middleware, serialization, and status codes |
| JSON contract tests | Verify response fields and values | Stable API response shapes |
| OpenAPI comparison | Identify changes to the documented API | Release reviews and compatibility checks |
| Consumer-driven contract tests | Verify interactions required by consumers | Microservices and independently deployed applications |
Contract testing does require coordination. Teams need to maintain consumer expectations and decide when a contract change is intentional. Without that process, contract tests can become outdated and block legitimate improvements.
Run API Tests in CI/CD Before Release
Automated tests provide the most value when they run before changes reach production.
Add API tests to your continuous integration pipeline so that a pull request or release candidate cannot proceed when required contract checks fail.
A typical pipeline might follow this sequence:
- Restore dependencies and compile the solution.
- Run unit tests.
- Run integration tests against the test configuration.
- Generate or validate the OpenAPI document.
- Compare the API contract with the approved baseline.
- Publish test results and block the release when required checks fail.
For example, a pipeline can execute the test project with the .NET CLI:
dotnet restore
dotnet build --no-restore
dotnet test --no-buildThese commands assume the solution has already been configured to include the relevant test project. The OpenAPI comparison and contract-testing steps must be added separately using the tools selected by your team.
Keep test configuration isolated from production. Use test databases, controlled test data, and test credentials. Avoid making destructive requests against production services as part of automated compatibility checks.
Common Mistakes to Avoid
Testing Only the HTTP Status Code
A successful status code does not guarantee that the response contains the expected properties or values. Validate the response body whenever consumers depend on its structure.
Tests that depend on changing production records can become unreliable. Create predictable test data or use a controlled test data source.
Clients often rely on validation errors, authentication failures, and missing-resource responses. Test these cases alongside successful requests.
Treating Every OpenAPI Difference as Breaking
A new optional property may be compatible with existing clients. Review schema changes based on consumer expectations instead of blocking every difference automatically.
Running Contract Tests Only Before Deployment
Finding a breaking change during a release is more expensive than detecting it during a pull request. Run the essential checks early and repeat them at release time when appropriate.
Best Practices for Reliable API Testing
A reliable strategy combines several testing levels instead of relying on one tool.
- Define the contract: Document important routes, status codes, request fields, response fields, and authentication requirements.
- Test consumer-critical behavior: Focus on the fields and interactions that existing applications actually use.
- Keep tests deterministic: Control test data, environment variables, and external dependencies.
- Validate error behavior: Treat failure responses as part of the API contract.
- Review schema changes: Use OpenAPI comparison to identify changes that deserve compatibility review.
- Test backward compatibility: When supporting existing clients, verify that new versions preserve the interactions those clients require.
- Make failures actionable: Include the endpoint, expected behavior, and actual result in test failure messages.
Advantages and Limitations
| Advantages | Limitations |
|---|---|
| Detects incompatible changes before release | Requires maintenance of tests and contract baselines |
| Protects frontend and mobile consumers | Cannot automatically represent every consumer's behavior |
| Improves confidence in API versioning | Integration tests may need database and service setup |
| Makes release reviews more consistent | Strict snapshots can flag harmless changes |
| Helps multiple teams coordinate API changes | Consumer-driven contracts require team coordination |
The right balance depends on the application. A small internal API may need integration tests and a few contract checks. A public API with many independent consumers may benefit from a formal compatibility policy, OpenAPI diffing, and consumer-driven contract testing.
Summary
Breaking API changes often involve small differences that are easy to miss during development. A renamed JSON property, a changed status code, or a new required request field can disrupt applications even when the API still builds and starts correctly.
ASP.NET Core integration tests help verify real HTTP behavior. JSON contract tests protect important response fields, while OpenAPI comparison identifies changes to the documented interface. Consumer-driven contract tests add another layer of protection when multiple applications depend on the same API.
Run these checks in CI/CD, use predictable test data, and review compatibility changes before release. This approach helps teams identify API problems earlier and release changes with greater confidence.


0 comments:
Post a Comment