Introduction
Building robust and reliable ASP.NET Core 8.0 APIs requires precision and strategic planning. This article explores essential practices that form the foundation of a well-tested API, navigating through the intricacies of Unit, Integration, and Functional Testing.
Mastering the Art of Unit Testing
Unit testing involves testing a single class by mocking out its dependencies. This is the most basic form of testing, and we will cover it quickly. The fewer mocks used, the better, as excessive mocking can complicate the tests. If you find yourself using a lot of mocks and verifications to isolate the item being tested, it might be better to create an integration test. After all, you need to test the integration between parts, right?
In ASP.NET Core 8.0, unit testing is similar to other .NET applications. Depending on your architecture, you’ll likely test domain entities or related domain elements, avoiding excessive mocking and focusing on the application’s business logic. Avoid unit testing queries or services that heavily depend on the database. In such cases, integration testing is preferable.
Sample Case:
Here we have a method that checks whether a misspelt city name or a variant city name is the same city or not.
Test:
This is a simple case and test, no mocks are needed – an ideal case for unit testing.
Cultivating Seamless Integration:
Integration testing involves testing the interaction between two or more parts of the application, such as a CRUD service without mocking the database or the repository class. This stage examines how components interact and collaborate, revealing their true potential.
Using tools like WebApplicationFactory and meticulously planned collection fixtures in xUnit, we create an environment that simulates real-world integration challenges, fostering a robust API capable of seamless operation.
The API will run in-memory using WebApplicationFactory, configured and run once for all tests. Some tests may require special implementations, like changing the API environment, services, or configuration. For this article, we’ll use the same API for all tests, achieved using the collection fixture feature in xUnit. Let’s look at some implementations:
ApiCollection.cs → Marks the tests using the API.
ApiClientTests.cs → Demonstrates the use of the `ApiCollection´ fixture in a test (attribute).
WebApplicationFactoryFixture.cs → Implements the fixture, initialising the API and related services once and disposing of them after all test run. This includes setting up the database.
Database.cs → Uses Testcontainers to deploy the database using DACPAC to an SQL Server container, logging the process in the xUnit output. The DACPAC comes from a NuGet package.
API logging is redirected to the test output, which can also be asserted by tests.
Property injection is used to avoid redundant code in each test constructor.
Test.cs → A base class for each test, using property injection to avoid passing the fixture and ‘ITestOutputHelper’ in each test via constructor. This is achieved using the ‘Xunit.DependencyInjection’ NuGet package.
There are options for deeper investigation or to decrease test execution time:
TestOptions.cs
What are we testing?
Primarily the API endpoints and responses. However, we can also test the services and other components more thoroughly if needed. It’s recommended to test the whole stack whenever possible (API endpoints). For example, testing the correct upload of an image and validating that we get the same image afterward.
Ensuring Functional testing
In-memory integration testing is great, but what about when the API is deployed and running? This is where functional testing comes in.
Functional testing validates that the API and its services work as expected when deployed and running. IT’s useful for testing continuous deployment (CD) processes. In production, we might only test GET requests to avoid modifying anything, but this is sufficient to validate that the API has been deployed successfully and responds as expected. Think of it like testing with Postman but within a .NET test project (which I prefer).
These kinds of tests that cover the whole API (from endpoint to database) are invaluable for ensuring that we haven’t broken anything and are resilient to major refactors. It is more effective to have a robust set of these tests than a large number of unit tests with excessive mocking.
In conclusion, testing is a critical aspect of API development, ensuring that your ASP.NET Core 8.0 applications are robust, reliable, and resilient to changes. By mastering unit, integration, and functional testing, you can deliver high-quality software that meets users’ needs and withstands the demands of real-world usage.
For further resources and community support, don’t hesitate to explore the provided source code or reach out with any questions.