Introduction
Developer testing is an essential part of software development that developers must pay attention to. It is a crucial aspect of software development that improves our codebase’s reliability, maintainability, and overall quality. Developer testing typically focuses on testing individual units or components of code to detect defects early in the development process. Despite its importance, some developers find it tiresome to set up mock classes repeatedly or maintain the service class when the constructor has changed. Not to mention if a scenario involves many model classes requiring complex data mocking.
In this post, we will share some ways to simplify the test setup and learn how to create tests that can handle multi-tenancy and multiple databases using an actual service provider in the test project. Let’s begin!
Prerequisites
This tutorial delves into developer testing using `xUnit`, `EF Core`, `SQL Server`, and `Dependency Injection`. It’s important to familiarize yourself with the prerequisites that will form the foundation of our testing journey.
Testing Strategy
In C#, several testing strategies are available and choosing the right one can significantly impact the effectiveness and efficiency of your testing efforts. We should make the right choice in selecting the correct testing strategies. A fundamental decision when testing EF Core applications is to use a Production database just as the application does (by replicating it in the test environment) or to use a test double (https://martinfowler.com/bliki/TestDouble.html) that replaces your production database system. Two main examples of test doubles exist in the EF Core context: `SQLite in-memory mode` and `in-memory provider`.
It’s important to note that passing tests using a test double, doesn’t necessarily mean your program will function properly when interacting with the actual external resource. For instance, a database test double may use case-sensitive string comparisons, while the real production database system may use case-insensitive comparisons. These discrepancies can only be identified by running tests against the production database, so including such tests in your overall testing strategy is crucial.
Testing against the database
In this section, we will construct a test against a real database, as the recommendation from Microsoft for developers (https://learn.microsoft.com/en-us/ef/core/testing/choosing-a-testing-strategy#testing-against-the-database-may-be-easier-than-it-seems).
Testing against the database offers several benefits to your software development process’s overall quality, reliability, and effectiveness. Here are the key advantages of testing against the database:
- Realistic Environment
Testing against the production database provides a highly realistic environment. It ensures that the tests closely resemble the conditions under which your application will operate in the real world. This helps identify potential issues arising from differences between test and production environments.
- Reduced Risk of Regressions
Since production database testing reflects actual usage, it significantly reduces the risk of regressions. It provides an early indication of changes negatively impacting existing functionality or data.
- Concurrency and Transaction Testing
Production database testing allows you to evaluate the behavior of your application in scenarios involving concurrent transactions and interactions. This is crucial for identifying race conditions and ensuring the integrity of your data.
- Enhanced Confidence
Successfully testing against the production database instils greater confidence in your application’s reliability. It assures stakeholders and team members that the application is thoroughly vetted against the same system it will eventually run on.
- Effective Troubleshooting
Testing against the production database allows for more effective troubleshooting in case of test failures. You can directly correlate test failures with your live system’s data and conditions.
Setting up a Sample Project in .NET Core Using Visual Studio 2022
We’ll utilize an example project that’s available on GitHub. These steps will give you a hands-on understanding of creating and running .NET Core applications.
- Clone the Project Repository
git clone https://github.com/soetedja/awesome.git
- Open the Project in Visual Studio
Browse to the directory where you cloned the example repository, andrepository and select the appropriate solution file (Awesome.sln).
- Build and Run the Example Project
Run the project, the Swagger Page will be displayed in the browser.
- Check the SQL Database and Initial Data
This project involves database creation and initialization; you should see the expected tables and data records within the connected database. Verify with SSMS whether the database named `AwesomeDB` has been successfully created.
Explanation of Project Structure

Figure 1. Project Structure
Here’s an explanation of the solution’s project structure, which consists of seven projects.
- API
The API project serves as the entry point for your application and acts as an interface for external clients to interact with the system.
- Business Service
The `BusinessService` project encapsulates the core business logic of the application. It contains services, classes, and components responsible for implementing the business rules and workflows. This layer interacts with the data access layer (`Repository`) to retrieve and persist data while applying the business rules and transformations.
- Common
The Common project houses utilities, helper classes, extensions, and functionalities commonly used across various application parts. These utilities include error handling, logging, validation helpers, authentication mechanisms, and more.
- Domain
The Domain project defines the core domain model of the application. It includes classes representing your business domain’s entities. This layer focuses on modelling real-world concepts and relationships.
- Model
The Model project primarily focuses on DTOs (Data Transfer Objects)), and view models used for communication between different layers of your application. DTOs help transfer data between the API, `BusinessService`, and Repository layers without exposing unnecessary details of the domain model.
- Repository
The Repository project manages the data access of the application. It provides an abstraction layer that shields the rest of the application from the underlying data storage technology (e.g., databases). This layer includes classes and methods to perform CRUD (Create, Read, Update, Delete) operations on the data entities.
- Test
The Test project contains your tests, essential for verifying that individual units of your application (such as methods or classes) work correctly in isolation. Each component in your solution, such as services or utilities, should have corresponding tests to ensure proper functioning. These tests help catch bugs early, support refactoring, and improve code quality.
How to Setup a Test Project
Add a new Test Project
- Open your .NET solution in Visual Studio.
- Right-click on the solution in the Solution Explorer and select Add > New Project.
- Select C# language, Test project type.
- Choose xUnit Test Project from the list of templates.
- Give your project a name and choose a location.
- Click Next to create the project.
Figure 2. Create a test project.
The test project has already been created in the repository. Our focus will be on explaining the structure of the project.
Test Project Structure
The test project is divided into three main folders:
- API: This folder is used for testing through the controller API.
- BusinessService: Here, we conduct testing for the Business Service class.
- Infrastructures: This folder is dedicated to setting up the testing environment.
Let’s focus on the `LocationControllerTest` class inside the API folder, for our starting point to give an overview of how the test is running.
Figure 3. Sample Controller Test
This class is responsible for testing the `LocationController` class, which is part of the API project.
The class is annotated with `[Collection(“TransactionalTests”)]`, indicating that it belongs to a collection of tests that require a transactional test database.
The `TransactionalTestDatabaseFixture` is injected into the class’s constructor, which provides the necessary setup and teardown of the test database. Here is the reference link for shared object instances across multiple test classes using Text Fixture on xUnit (https://xunit.net/docs/shared-context#class-fixture)
Inside the constructor, the class initializes the necessary dependencies for the test. It creates an instance of `DataContext` using `Fixture`, and then uses a `ServiceProviderTestHelper` to create a service provider with the `DataContext` and the mock dependencies.
Here we use an actual Service Provider to run the test without mocking it. This allows us to test the functionality of the `LocationController` class with a real implementation of the `ICountryService` and `ICityService` dependency.
The service provider is used to resolve the `ICountryService` and `ICityService` dependency, which is then passed to the `LocationController` instance.
Here’s a diagram to show you the difference between the Dependency Injection from the API and the Test.

Figure 4. Test Class Diagram
As you can see, fromin the diagram above, we have two different instances of `ServiceCollection`. One `ServiceCollection` is from the API, and the other one is from the Test. The latter utilises a different Database as we use different connection strings. TransactionalTestDatabaseFixture Class. The `TransactionalTestDatabaseFixture` class is a utility class commonly used for integration tests involving databases.
Figure 5. Test Database Fixture Class
Let’s break down its key components:
The `CreateContext` method initializes a new instance of a `DataContext`, which represents the database context for the application. It configures the context to use a SQL Server database connection defined in the `SQLDBConnections` class.
The constructor of `TransactionalTestDatabaseFixture` is responsible for initializing the test fixture. It calls the `Cleanup` method to ensure a clean state before each test method runs. This cleanup frees any residual data or schema changes from the previous test.
The `Cleanup` method ensures that the database is reset to a clean state before each test method executes. It achieves this by deleting and recreating the database schema using the `EnsureDeleted` and `EnsureCreated` methods provided by Entity Framework Core. Additionally, it invokes the `InitializeDataGlobally` method to populate the database with predefined data such as countries and cities.
The `InitializeDataGlobally` method streamlines the setup of test data by populating the database with sample data representing countries and their respective cities. It creates instances of Country and City entities, assigns them appropriate names and codes, and establishes the relationships between countries and cities. Finally, it saves the changes to the database using Entity Framework Core’s `SaveChanges` method.
ServiceProviderTestHelper Class
Let’s look at the `ServiceProviderTestHelper` class.
Figure 6. Service Provider Test Helper Class
In the test project, we will use an actual `ServiceProvider` to avoid mocking the `Services` class. We use this `ServiceProviderTestHelper` class to create the Service Provider instance for the Test project.
The `ServiceProviderTestHelper` class is a utility class to setup the service provider for testing scenarios, particularly when testing services that rely on external dependencies. Let’s dissect its main functionalities:
- Service Provider Creation:
The `CreateServiceProvider` method constructs a service provider by configuring a `ServiceCollection`. It begins by creating an in-memory configuration containing a mock API key replacing the Web.config. Then, it registers the application’s internal dependencies using `RegisterInternalServiceDependencies`. Additionally, it registers dependencies related to database access by configuring a scoped instance of the `DataContext`.
- Mocked External Service Registration:
The `RegisterMockedExternalServiceDependencies` method is responsible for registering mocked dependencies related to external services. In this case, it adds a scoped instance of a mocked HTTP client service using the `MockHelper` class, which is commonly used in tests to simulate interactions with external APIs.
Sample Test Method
The test class can now utilize methods and assertions to test the functionality of the `LocationController` class. Let’s check one of the sample test methods below.
Figure 7. Sample Test Method
The code above tests the behavior of the `GetCountry` method in the `LocationController`. Here’s a brief explanation of each section:
- Fact Attribute
This indicates that the following method is a test case. In xUnit, test methods are marked with attributes like [Fact] to denote that they are runnable tests.
- Arrange
In this section, the necessary preconditions for the test are set up. Here, `_dataContextTestHelper` is used to add a new country to the data context with the name “Singapore2”.
- Act
This is where the actual action being tested is performed. In this case, it is called the `GetCountry` method on the `_controller`.
- Assert
Here, the outcomes of the action are verified against expected results. The test asserts that the returned list of countries contains four elements and that the names of these countries match the expected values at specific positions in the list.
This test ensures that the `GetCountry` method returns a list of countries that includes the newly added “Singapore2” country, alongside the existing countries “Australia”, “New Zealand”, and “Indonesia”. If any of the assertions fail, it indicates a discrepancy between the expected and actual behavior of the method, prompting further investigation or code correction.
We are not only limited to testing the Controller, but we can also set up the Test for the Service Classes, and Repository Level if we need to. You can check the sample of the test from the Service level inside the `BusinessService` folder in the Test Project.
Potential Drawbacks and Risks
Running tests with an actual database may have some potential drawbacks and risks:
- Dependency on External Systems
Tests that rely on an actual database introduce a dependency on external systems if the database is in a different environment/server, making tests less reliable and potentially impacting their portability across different environments.
To prevent any issue in running the DB, we must ensure that the Testing Environment has access to the DB Server. When managing multiple development stages, each test project should be equipped with its unique database instance. This approach minimizes conflicts and ensures that tests remain isolated and independent of one another. By adhering to these practices, you can mitigate potential issues associated with database access during testing.
- Performance Impact
Tests might execute queries that could degrade the performance of the database server, affecting the overall system’s performance.
To mitigate this risk, it is recommended to perform testing using a separate database server that is different from the production environment. By using a dedicated test database server, you can isolate the impact of tests on system performance. This ensures that any performance issues or database load generated during testing do not impact the stability or responsiveness of the production environment.
- Resource Consumption
Maintaining a separate test database requires additional resources, including storage and computational power. This can increase infrastructure costs.
Keep monitoring and optimizing resource usage for the test database to minimize costs and ensure efficient utilization of infrastructure resources.
- Test Isolation
While using a separate test database reduces the risk of impact on production data, it is important to ensure that tests are isolated and do not impact other tests or environments that share the same test database.
Maintain separate test environments for each project to prevent interference between tests and ensure isolation.
Summary
In conclusion, developer testing plays a crucial role in ensuring the reliability and correctness of our codebase. Through the example test case we’ve explored, we’ve demonstrated how to effectively test a controller method in a C# application using xUnit.
One notable aspect of our approach is the utilization of a real service provider instead of relying on mocking frameworks. By leveraging a real service provider, we streamline the setup process for our tests, leading to faster test execution and more robust validations. This strategy allows us to maintain the integrity of our tests while avoiding the complexity and overhead often associated with mocking dependencies.
However, by adopting strategies such as providing each test project with its unique database instance, utilizing separate test environments, and monitoring resource usage, these risks can be effectively mitigated.
As you continue to refine your testing practices, consider the balance between using real dependencies and leveraging mocks based on the specific requirements of your project. By prioritizing simplicity, readability, and thoroughness in your tests, you can enhance the reliability and maintainability of your codebase, delivering high-quality software solutions.