Back to: ASP.NET Core Web API Tutorials
Unit Testing Repositories in ASP.NET Core Web API
Unit testing repositories is a fundamental step in ensuring the reliability and correctness of the data access layer in an ASP.NET Core Web API application. Repositories serve as the gateway between the application and its underlying data sources, encapsulating all CRUD operations and abstracting the database complexities from other parts of the system. Properly testing repositories helps validate that data retrieval, insertion, updates, and deletions behave as expected.
In this post, we will discuss how to Unit Test Repositories in an ASP.NET Core Web API application. We will continue to work with the same example that we have been using so far. Please read the posts below before this post.
- Part 1: Real-time ASP.NET Core Project setup and architecture overview, including repositories, services, controllers, DTOs, and dependency injection, focusing on establishing a clean project structure for testability.
- Par 2: Unit testing the Service Layer, detailing approaches with EF Core InMemory database and Moq mocking framework, plus comprehensive sample tests for happy paths, exceptions, and edge cases.
- Part 3: Unit testing the Controller Layer, emphasizing mocking the service layer, testing various HTTP response scenarios, and ensuring input validation and error handling correctness.
What are Repositories?
Repositories are the Data Access Layer (DAL) responsible for interacting with the database or data sources. They encapsulate CRUD (Create, Read, Update, Delete) operations and abstract the underlying data source from the rest of the application. For a better understanding, please refer to the following diagram.
Abstraction of Data Access
- The repository pattern abstracts the complexities of querying and manipulating the data.
- Consumers of the repository (like services or controllers) don’t need to know how the data is stored or retrieved.
- This abstraction allows developers to change the underlying data source or database technology without impacting other layers.
Encapsulation of CRUD Operations
Repositories centralize basic data operations:
- Create: Add new records.
- Read: Retrieve records by different criteria.
- Update: Modify existing records.
- Delete: Remove records.
By encapsulating these operations, the repository provides a clean API for data access.
Separation of Concerns
- Separates the data access code from business logic.
- Business logic is placed in service layers, while repositories focus only on interacting with data.
- This improves code maintainability, testability, and scalability.
Testability
- Since repositories use interfaces, they can be easily mocked during unit testing.
- This allows testing the business logic in isolation without depending on an actual database.
How Repositories Fit in ASP.NET Core Web API Architecture
To understand how Repositories fit into our ASP.NET Core Web API Architecture, please refer to the following diagram.
Here is the step-by-step process:
- Controllers handle HTTP requests and responses but don’t interact with the database directly.
- Services implement business logic and call repositories to fetch or persist data.
- Repositories communicate with the database using Entity Framework Core or other Object-Relational Mapping (ORM) tools or raw queries.
- DbContext is usually injected into repositories to handle database sessions and transactions.
Why Unit Test Repositories in ASP.NET Core Web API?
Testing repositories ensures that your data operations (CRUD) behave as expected, your Entity Framework Core mappings are correct, and your queries return correct results. For a better understanding, please refer to the following image.
So, unit testing repository methods is essential because of the following:
- Validate Data Access Logic: Confirm that your LINQ queries, including eager loading (using Include), filtering, and data manipulation, behave as expected.
- Catch Mapping and Model Issues Early: Detect model-property misconfigurations or DbContext issues.
- Ensure Repository Contracts: Verify that repository methods adhere to expected contracts (e.g., return correct data, handle null values properly).
- Prevent Regressions: Catch accidental breaks after refactoring or database schema changes.
Well-tested repositories help ensure data integrity and prevent bugs that could lead to data loss, corruption, or incorrect business behaviour.
Approaches to Repository Testing in ASP.NET Core Web API
Since repositories directly depend on the database, unit testing them requires strategies to avoid hitting a real database during test execution for speed, reliability, and repeatability.
- Using EF Core InMemory Provider
- Mocking DbContext and DbSets
Note: Do not use Moq for testing actual repository logic; instead, use EF Core InMemory for that purpose. Moq is for unit testing the layers that depend on the repository, not the repository’s own EF/SQL logic.
Using EF Core InMemory Provider for Unit Testing Repositories
For a better understanding, please refer to the following image.
Here:
- We need to use Microsoft.EntityFrameworkCore.InMemory package.
- We need to run tests against an in-memory database that mimics EF Core behaviour.
- This allows testing repository methods realistically (LINQ queries, Includes, Add/Update/Delete).
- This approach does not enforce relational constraints; transactions behave differently from real SQL Server.
Please ensure your test project has the EF Core InMemory package installed:
Install-Package Microsoft.EntityFrameworkCore.InMemory
Example to Test Product Repository using EF Core InMemory Provider:
We have already created the following Product Repository Interface and its Implementation classes.
IProductRepository Interface:
The following is our IProductRepository Interface.
using ProductCatalog.API.Models; namespace ProductCatalog.API.Repositories { public interface IProductRepository { Task<Product?> GetByIdAsync(int id); Task AddAsync(Product product); Task SaveChangesAsync(); } }
ProductRepository Concrete Class:
The following is our ProductRepository implementation.
using ProductCatalog.API.Data; using ProductCatalog.API.Models; namespace ProductCatalog.API.Repositories { public class ProductRepository : IProductRepository { private readonly ApplicationDbContext _dbContext; public ProductRepository(ApplicationDbContext dbContext) { _dbContext = dbContext; } public async Task<Product?> GetByIdAsync(int id) { return await _dbContext.Products.FindAsync(id); } public async Task AddAsync(Product product) { await _dbContext.Products.AddAsync(product); } public async Task SaveChangesAsync() { await _dbContext.SaveChangesAsync(); } } }
ProductRepository Unit Test Class using EF Core InMemory
In the ProductCatalog.Tests project, create a folder named Repositories in the project root directory. Then, add a class file named ProductRepositoryTests.cs within the Repositories folder and copy and paste the following code. We need to use a fresh in-memory database for each test to ensure isolation.
using Microsoft.EntityFrameworkCore; using ProductCatalog.API.Data; using ProductCatalog.API.Models; using ProductCatalog.API.Repositories; namespace ProductCatalog.Tests.Repositories { public class ProductRepositoryTests { // Helper method that creates a fresh, isolated ApplicationDbContext using EF Core InMemory provider private ApplicationDbContext GetInMemoryDbContext() { // Configure DbContextOptions to use an InMemory database with a unique name // Guid.NewGuid().ToString() ensures a new separate database per test, avoiding data conflicts var options = new DbContextOptionsBuilder<ApplicationDbContext>() .UseInMemoryDatabase(databaseName: System.Guid.NewGuid().ToString()) .Options; // Create a new ApplicationDbContext instance with the above options var context = new ApplicationDbContext(options); // Seed initial product data into the in-memory database for testing context.Products.AddRange( new Product { Id = 1, Name = "Test Laptop", Price = 60000m, Stock = 20 }, // Product 1 new Product { Id = 2, Name = "Test Smartphone", Price = 25000m, Stock = 50 } // Product 2 ); // Persist seeded data immediately to the in-memory store context.SaveChanges(); // Return the prepared in-memory context for use in tests return context; } // Test method to verify GetByIdAsync returns a product when the product exists in the database [Fact] public async Task GetByIdAsync_ProductExists_ReturnsProduct() { // Arrange: create a fresh in-memory DbContext and instantiate ProductRepository var context = GetInMemoryDbContext(); var repository = new ProductRepository(context); // Act: attempt to retrieve product with ID=1 (exists in seeded data) var product = await repository.GetByIdAsync(1); // Assert: product should not be null Assert.NotNull(product); // Assert: product's Name matches the seeded value "Test Laptop" Assert.Equal("Test Laptop", product.Name); // Assert: product's Price matches the seeded value 60000m Assert.Equal(60000m, product.Price); } // Test method to verify GetByIdAsync returns null when the product does not exist [Fact] public async Task GetByIdAsync_ProductDoesNotExist_ReturnsNull() { // Arrange: setup fresh context and repository var context = GetInMemoryDbContext(); var repository = new ProductRepository(context); // Act: attempt to fetch a product with ID=999 which does not exist in seeded data var product = await repository.GetByIdAsync(999); // Assert: product should be null since it does not exist Assert.Null(product); } // Test method to verify AddAsync successfully adds a product to the database [Fact] public async Task AddAsync_ProductIsAdded_ProductExistsInDb() { // Arrange: setup fresh context and repository var context = GetInMemoryDbContext(); var repository = new ProductRepository(context); // Create a new product instance to be added var newProduct = new Product { Id = 3, Name = "Test Tablet", Price = 15000m, Stock = 30 }; // Act: add new product asynchronously using repository method await repository.AddAsync(newProduct); // Save changes to persist new product in in-memory database await repository.SaveChangesAsync(); // Assert: retrieve the newly added product by ID var addedProduct = await repository.GetByIdAsync(3); // Assert: product should not be null (exists) Assert.NotNull(addedProduct); // Assert: product Name should be "Test Tablet" Assert.Equal("Test Tablet", addedProduct.Name); } // Test method to verify changes to a product are persisted correctly after SaveChangesAsync [Fact] public async Task SaveChangesAsync_ModifiesData_DataIsPersisted() { // Arrange: setup fresh context and repository var context = GetInMemoryDbContext(); var repository = new ProductRepository(context); // Act: fetch an existing product by ID=1 var product = await repository.GetByIdAsync(1); // Assert: ensure product is not null before modifying it Assert.NotNull(product); // Modify the Stock property of the fetched product product!.Stock = 15; // Save changes to persist the modification in in-memory database await repository.SaveChangesAsync(); // Assert: fetch the product again to verify changes persisted var updatedProduct = await repository.GetByIdAsync(1); // Assert: product still exists Assert.NotNull(updatedProduct); // Assert: Stock property reflects the updated value 15 Assert.Equal(15, updatedProduct!.Stock); } } }
When to Use Moq for Repository Interfaces?
- We typically need to mock repositories when testing layers above repositories, such as Services or Controllers, to isolate the tested class from real data access.
- Mocking repositories means simulating the repository interface methods to return controlled data or behaviors without hitting any real or in-memory database.
- Mocking the repository directly for repository tests is less common because repositories are typically thin wrappers around DbContext and EF Core, which are better tested using the EF Core InMemory provider.
So, unit testing repositories is crucial for building robust and maintainable ASP.NET Core Web API applications. While the EF Core InMemory provider is ideal for testing repository implementations in isolation, mocking repository interfaces using frameworks like Moq is more suitable when unit testing service or controller layers. Combining these approaches enables developers to write fast, reliable, and comprehensive tests that detect data access issues early, ultimately leading to improved application quality and easier maintenance.
In the next article, I will discuss Integration Testing in an ASP.NET Core Web API application. In this article, I explain Unit Testing the Repositories of our ASP.NET Core Web API. I hope you enjoy this article on Unit Testing Repositories in ASP.NET Core Web API.