Before starting the project we need to know about Development tools, The .net version and, Nuget packages which is called unit test.
Topics To know Before: | Clean Architecture, Entity Framework (Code First Approach), Unit Test Basic |
Development Tools: | VS 2022, SQL Server Database (Latest), .Net Version: 8.0 |
Nuget Packages (Unit Test): | Moq, FluentAssertion, AutoFixture, Microsoft.EntityFrameWorkCore.InMemory |
In this blog we will learn how to implement unit tests in a Clean Architecture project with N-Unit. This is part one. We are also covering the second part in the next post. We have built a project with clean architecture already. The project structure is shown in the image below.
Process of .Net Core Web API Project with Clean Architecture using N-Unit
This is a simple crud app with Clean Architecture using Entity Framework Code First Approach. There is an entity class named Post and in this app I just implemented crud on it. The app solution contains three Library projects and one API project.
Domain
- Contains entities, models. view models.
Application
- Contains services with business logic like data validation e.t.c and use repository interfaces to act with database.
- Added Domain project as reference.
Infrastructure
- Contains DBContext class.
- Contains repositories to query, add, update, delete data in the database using DBContext.
- Added Application project as reference.
WebAPI
- Contains API Controllers which use the services of Application project to act with repositories.
- Added Infrastructure project as reference.
About Unit Test:
Unit Test on Application Layer:
In the Application layer there is a PostsService class which will be tested. It has 5 methods which will be tested separately. Hence we created a Services folder in CleanArchitecture.Application.UnitTest project and inside it we created a test class file named PostsServiceUnitTests. We used Fixture and created a mock of our dependencies to act on PostsService inside the Setup method of PostsServiceUnitTests class
public class PostsServiceUnitTests
{
private Fixture _fixture;
private Mock _postsRepositoryMock;
private PostsService _postsService;
[SetUp]
public void Setup()
{
_fixture = new Fixture();
_postsRepositoryMock = _fixture.Freeze>();
_postsService = new PostsService(_postsRepositoryMock.Object);
}
Now the test begins for each method of PostsService class.
Get Posts Method
This method fetches all posts from the database by using dependency IPostRepository.
public class PostsService : IPostsService
{
private readonly IPostsRepository _postsRepository;
public PostsService(IPostsRepository postsRepository)
{
_postsRepository = postsRepository ??
throw new ArgumentNullException(nameof(postsRepository));
}
public async Task GetPosts()
{
return ResponseModel.ok(await _postsRepository.GetPosts());
}
This method is tested using the process below,
Test Case 1: Get List Of Posts As Response On Get Posts Method Call
[Test]
public async Task GetListOfPostsAsResponseOnGetPostsMethodCallAsync()
{
//Arrange
//Act
var response = await _postsService.GetPosts();
//Assert
response.Should().NotBeNull();
response.Should().BeOfType();
Assert.IsAssignableFrom(response.Data);
_postsRepositoryMock.Verify(p => p.GetPosts(), Times.Once());
Assert.Pass();
}
In this step we act the GetPosts method of PostsService. Then we checked the response of the act by fluent assertion that it is not null, is a type of ResponseModel, and is assignable to Post[]. Then we verified the mocked dependency service method named GetPosts is called once. Then if all things ok we Assert the result as passed.
Get Post By ID Method
This method fetches a single post if it exists by it’s Id.
public async Task GetPostByID(int ID)
{
var post = await _postsRepository.GetPostByID(ID);
if(post == null) {
return ResponseModel.customError("No Post Found!");
}
return ResponseModel.ok(post);
}
This method is tested using the process below,
Test Case 1: Get Post As Response On Get Post By ID Method Call By Valid Id
[Test]
public async Task GetPostAsResponseOnGetPostByIDMethodCallByValidIdAsync()
{
//Arrange
int id = _fixture.Create();
_postsRepositoryMock.Setup(p => p.GetPostByID(It.IsAny())).ReturnsAsync(new Post());
//Act
var response = await _postsService.GetPostByID(id);
//Assert
response.Should().NotBeNull();
response.Should().BeOfType();
response.Data.Should().BeOfType();
_postsRepositoryMock.Verify(r => r.GetPostByID(id), Times.Once);
Assert.Pass();
}
In this step we created a random integer by autofixture to use it as a valid id. Then we set up a Post class object as output of the post repository mock’s GetPostById method call. By this we set that when we act PostService’s GetPostById method then the result of its dependency service’s GetPostById method will return a Post Object. Then we act the method GetPostById of PostService. Then we assert the result as the logic we used previously using Fluent Assertion.
Test Case 2: Get Null As Response On Get Post By ID Method Call By Invalid Id
[Test]
public async Task GetNullAsResponseOnGetPostByIDMethodCallByInvalidIdAsync()
{
//Arrange
int id = 0; //invalid id
Post post = null;
_postsRepositoryMock.Setup(p => p.GetPostByID(id)).ReturnsAsync(post);
//Act
var response = await _postsService.GetPostByID(id);
//Assert
response.Should().NotBeNull();
response.Should().BeOfType();
response.Data.Should().BeNull();
_postsRepositoryMock.Verify(r => r.GetPostByID(id), Times.Once);
Assert.Pass();
}
In this step we use zero as invalid id. Setup output of GetpostById method of repository mock as null. Then act the method GetPostById of PostService. Then validate or assert the test as the logic we discussed previously using fluent assertion.
Insert Post Method
This method inserts post into the database using the post repository dependency.
public async Task InsertPost(Post objPost)
{
var validationErrors = new Dictionary();
if (String.IsNullOrEmpty(objPost.Title))
{
validationErrors["Title"] = "Title can not be empty.";
return ResponseModel.validationErrors(validationErrors);
}
int id = await _postsRepository.InsertPost(objPost);
if (id != 0)
{
return ResponseModel.ok(id);
}
else
{
return ResponseModel.customError("Unexpected Error");
}
}
This method is tested using the process below,
Test Case 1: Get True As Response On Add Post Method Call With Valid Data
[Test]
public async Task GetTrueAsResponseOnAddPostMethodCallWithValidDataAsync()
{
//Arrange
Post post = new Post()
{
Title = "Test",
};
_postsRepositoryMock.Setup(p => p.InsertPost(post)).ReturnsAsync(1);
//Act
var response = await _postsService.InsertPost(post);
//Assert
response.Should().NotBeNull();
response.Should().BeOfType();
response.Data.Should().Be(1);
_postsRepositoryMock.Verify(p => p.InsertPost(post), Times.Once());
Assert.Pass();
}
In this step we act on the InsertPost method of PostService with a valid post object and verify that its return true or not. Process is similar as we discussed before.
Test Case 2: Get Validation Error As Response On Add Post Call With Empty Title
[Test]
public async Task GetValidationErrorAsResponseOnAddPostCallWithEmptyTitleAsync()
{
//Arrange
Post post = new Post()
{
Title = "",
};
//Act
var response = await _postsService.InsertPost(post);
//Assert
response.Should().NotBeNull();
response.Should().BeOfType();
response.ValidationErrors.Should().HaveCount(1);
_postsRepositoryMock.Verify(p => p.InsertPost(post), Times.Never());
Assert.Pass();
}
In this step we act InsertPost method with invalid data, like an empty title. Check if it returns a validation error or not.
Update Post Method
This method updates post information based on provided id and data.
public async Task UpdatePost(Post objPost)
{
var validationErrors = new Dictionary();
if (String.IsNullOrEmpty(objPost.Title))
{
validationErrors["Title"] = "Title can not be empty.";
return ResponseModel.validationErrors(validationErrors);
}
var post = await _postsRepository.GetPostByID(objPost.Id);
if (post == null)
{
return ResponseModel.customError("No Post Found!");
}
post.Title = objPost.Title;
post.Description = objPost.Description;
bool result = await _postsRepository.UpdatePost(post);
if (!result)
{
return ResponseModel.customError("Unexpected Error");
}
return ResponseModel.ok(result);
}
This method is tested using the process below,
Test Case 1: Get True As Response On Update Post Method Call With Valid Data
[Test]
public async Task GetTrueAsResponseOnUpdatePostMethodCallWithValidDataAsync()
{
//Arrange
Post post = new Post()
{
Id = 1,
Title = "Test",
};
_postsRepositoryMock.Setup(x => x.GetPostByID(It.IsAny()))
.ReturnsAsync(post);
_postsRepositoryMock.Setup(x => x.UpdatePost(post))
.ReturnsAsync(true);
//Act
var response = await _postsService.UpdatePost(post);
//Assert
response.Should().NotBeNull();
response.Should().BeOfType();
response.Data.Should().Be(true);
_postsRepositoryMock.Verify(p => p.GetPostByID(post.Id), Times.Once());
_postsRepositoryMock.Verify(p => p.UpdatePost(post), Times.Once());
Assert.Pass();
}
Test Case 2: Post Should Not Update On Update Post Method Call With Invalid Id
[Test]
public async Task PostShouldNotUpdateOnUpdatePostMethodCallWithInvalidIdAsync()
{
//Arrange
Post post = new Post()
{
Id = 0, //invalid id
Title = "Test",
};
Post nullPost = null;
_postsRepositoryMock.Setup(x => x.GetPostByID(post.Id)).ReturnsAsync(nullPost);
//Act
var response = await _postsService.UpdatePost(post);
//Assert
response.Should().NotBeNull();
response.Should().BeOfType();
_postsRepositoryMock.Verify(p => p.GetPostByID(post.Id), Times.Once());
_postsRepositoryMock.Verify(p => p.UpdatePost(post), Times.Never());
Assert.Pass();
}
Test Case 2: Post Should Not Update On Update Post Method Call With Invalid Id
[Test]
public async Task GetValidationErrorAsResponseOnUpdatePostCallWithEmptyTitleAsync()
{
//Arrange
Post post = new Post()
{
Title = "",
};
//Act
var response = await _postsService.UpdatePost(post);
//Assert
response.Should().NotBeNull();
response.Should().BeOfType();
response.ValidationErrors.Should().HaveCount(1);
_postsRepositoryMock.Verify(p => p.UpdatePost(post), Times.Never());
Assert.Pass();
}
Delete Post Method
public async Task DeletePost(int ID)
{
var post = await _postsRepository.GetPostByID(ID);
if (post == null)
{
return ResponseModel.customError("No Post Found!");
}
bool response = _postsRepository.DeletePost(ID);
if (!response)
{
return ResponseModel.customError("Unexpected Error");
}
return ResponseModel.ok(response);
}
This method is tested using the process below,
Test Case 1: Get True As Response On Delete Post Method Call With Valid Id.
[Test]
public async Task GetTrueAsResponseOnDeletePostMethodCallWithValidIdAsync()
{
//Arrange
int id = _fixture.Create();
_postsRepositoryMock.Setup(p => p.GetPostByID(id)).ReturnsAsync(new Post());
_postsRepositoryMock.Setup(p => p.DeletePost(id)).Returns(true);
//Act
var response = await _postsService.DeletePost(id);
//Assert
response.Should().NotBeNull();
response.Should().BeOfType();
response.Data.Should().Be(true);
_postsRepositoryMock.Verify(p => p.GetPostByID(id), Times.Once());
_postsRepositoryMock.Verify(p => p.DeletePost(id), Times.Once());
Assert.Pass();
}
Test Case 2: Post Should Not Delete On Delete Post Method Call With Invalid Id
[Test]
public async Task PostShouldNotDeleteOnDeletePostMethodCallWithInvalidIdAsync()
{
//Arrange
int id = 0; //invalid Id
Post nullPost = null;
_postsRepositoryMock.Setup(p => p.GetPostByID(id)).ReturnsAsync(nullPost);
//Act
var response = await _postsService.DeletePost(id);
//Assert
response.Should().NotBeNull();
response.Should().BeOfType();
_postsRepositoryMock.Verify(p => p.GetPostByID(id), Times.Once());
_postsRepositoryMock.Verify(p => p.DeletePost(id), Times.Never);
Assert.Pass();
}
Part Two: Clean Architecture Project Using N-Unit
In this part we implement unit tests on repositories of Infrastructure layer. We created a project with the N-Unit test project template and installed the Nuget packages named Moq, FluentAssertion, AutoFixture, Microsoft.EntityFrameWorkCore.InMemory.
We named it as, CleanArchitecture.Infrastructure.UnitTest project. We added the CleanArchitecture.Infrastructure project as a dependency on it.
Unit Test on Infrastructure Layer
In the Infrastructure layer there is a PostsRepository class which will be tested. It has 5 methods which will be tested separately. Hence we created a Repositories folder in the project CleanArchitecture.Infrastructure.UnitTest and inside it we created a test class file named PostsRepositoryUnitTest.
PostsRepository Unit Tests
We created a mock object of our DbContext to act on PostsRepository inside the Setup method of PostsRepositoryUnitTest class. To test this repository we use in memory database instance through our DbContext object. Also we added some demo data in the in memory database in the Setup method.
public class PostsRepositoryUnitTest
{
private IFixture _fixture;
private PostContext _dbContextMock;
private IPostsRepository _postsRepository;
[SetUp]
public void Setup()
{
_fixture = new Fixture();
var options = new DbContextOptionsBuilder().UseInMemoryDatabase(databaseName: "TestPostDB").Options;
_dbContextMock = new PostContext(options);
_dbContextMock.Posts.AddRange(
new List()
{
new Post { Id= 0,Title = "Title 1", Description = "Description 1" },
new Post { Id= 0,Title = "Title 2", Description = "Description 2" },
new Post { Id= 0,Title = "Title 3", Description = "Description 3" },
}
);
_dbContextMock.SaveChanges();
_postsRepository = new PostsRepository(_dbContextMock);
}
Now the test begins for each method of the PostsRepository class.
Get Posts:
It fetches all posts from the database.
public async Task> GetPosts()
{
return await _postDBContext.Posts.ToListAsync();
}
This method is tested using the process below,
Test Case 1: Get Posts Should Return Data If Data Found Successfully
[Test]
public async Task GetPostsShouldReturnDataIfDataFoundSucessfullyAsync()
{
//Arrange
//Act
var result = await _postsRepository.GetPosts();
//Assert
Assert.NotNull(result);
result.Should().BeAssignableTo>();
}
In this step we act on the GetPosts method of PostsRepository and check it returns not null and the result should be assignable to IEnumerable<Post> .
Get Post By ID Method:
This method returns a single post by it’s Id.
public async Task GetPostByID(int ID)
{
return await _postDBContext.Posts.FindAsync(ID);
}
This method is tested using the process below,
Test Case 1: Get Post By Id Should Return Data If Data Found Successfully
[Test]
public async Task GetPostByIdShouldReturnDataIfDataFoundSucessfullyAsync()
{
//Arrange
int id = _dbContextMock.Posts.FirstOrDefault().Id;
//Act
var data = await _postsRepository.GetPostByID(id);
//Assert
Assert.Multiple(() =>
{
Assert.That(data, Is.Not.Null);
Assert.That(data.Id, Is.EqualTo(id));
});
}
In this step we are taking the first post’s Id from the DbContext object and checking the GetPostById method of PostRepository to return the exact post with the Id we have passed.
Test Case 2: Get Post By Id Should Return Null If Data Not Found
[Test]
public async Task GetPostByIdShouldReturnNullIfDataNotFoundAsync()
{
//Arrange
int id = 0; //invalid id
//Act
var data = await _postsRepository.GetPostByID(id);
//Assert
data.Should().BeNull();
}
In this step we are passing an invalid Id to GetPostById method and checking if it returns null or not.
Insert Post Method:
This method inserts a post into the database.
public async Task InsertPost(Post objPost)
{
try
{
_postDBContext.Posts.Add(objPost);
await _postDBContext.SaveChangesAsync();
return objPost.Id;
}
catch (Exception ex) {
return 0;
}
}
This method is tested using the process below,
Test Case 1: Create Posts Should Return Non Zero Id When Data Inserted Successfully
[Test]
public async Task CreatePostsShouldReturnNonZeroIdWhenDataInsertedSucessfullyAsync()
{
//Arrange
var data = _fixture.Create();
//Act
var result = await _postsRepository.InsertPost(data);
//Assert
Assert.NotNull(result);
result.Should().BeGreaterThanOrEqualTo(1);
}
In this step we are creating a new post using the InsertPost method with random data using AutoFixture and checking it returns non zero Id of the created new Post.
Test Case 2: Create Posts Should Return Zero Id When Invalid Data Provided
[Test]
public async Task CreatePostsShouldReturnZeroIdWhenInvalidDataProvidedAsync()
{
//Arrange
var data = new Post()
{
Title = null //invalid value
};
//Act
var result = await _postsRepository.InsertPost(data);
//Assert
Assert.NotNull(result);
result.Should().Be(0);
}
In this step we are trying to create a new post using the InsertPost method with invalid data and checking it returns zero Id as result.
Insert Post Method:
This method updates the information of a post by it’s Id and other data.
public async Task UpdatePost(Post objPost)
{
try
{
_postDBContext.Entry(objPost).State = EntityState.Modified;
await _postDBContext.SaveChangesAsync();
return true;
}
catch (Exception ex) {
return false;
}
}
This method is tested using the process below,
Test Case 1: Update Posts Should Return True When Data Is Updated Successfully
[Test]
public async Task UpdatePostsShouldReturnTrueWhenDataIsUpdatedSucessfullyAsync()
{
//Arrange
var data = _dbContextMock.Posts.FirstOrDefault();
data.Title = "Name Updated";
//Act
var result = await _postsRepository.UpdatePost(data);
//Assert
Assert.NotNull(result);
result.Should().BeTrue();
Assert.IsTrue(data.Title == _dbContextMock.Posts.FirstOrDefault().Title);
}
In this step we get the first post from the database and then change its Title then try to update it by UpdatePost method and check it returns true and data is updated correctly at the database level.
Test Case 2: Update Posts Should Return False When Wrong Data Provided
[Test]
public async Task UpdatePostsShouldReturnFalseWhenWrongDataProvidedAsync()
{
//Arrange
var data = new Post()
{
Id = 0, //wrong Id
Title = "Valid Title",
};
//Act
var result = await _postsRepository.UpdatePost(data);
//Assert
Assert.NotNull(result);
result.Should().BeFalse();
}
In this step we try to update Post by invalid Id with UpdatePost method and check it returns false.
Delete Post Method:
This method deletes a post by it’s Id.
public bool DeletePost(int ID)
{
try
{
_postDBContext.Posts.Remove(_postDBContext.Posts.Find(ID));
_postDBContext.SaveChanges();
return true;
}
catch (Exception ex)
{
return false;
}
}
This method is tested using the process below,
Test Case 1: Delete Should Return True If Data Deleted Successfully
[Test]
public void DeleteShouldReturnTrueIfDataDeleteSucessfully()
{
//Arrange
int id = _dbContextMock.Posts.FirstOrDefault().Id;
//Act
var result = _postsRepository.DeletePost(id);
//Assert
Assert.IsTrue(result);
Assert.IsTrue(_dbContextMock.Posts.FirstOrDefault().Id != id);
}
In this step we get the first post’s Id from the database then try to delete it using the DeletePost Method and check if it returns true and the data is actually deleted or not.
Test Case 2: Delete Should Return False If Wrong Id Provided
[Test]
public void DeleteShouldReturnFalseIfWrongIdProvided()
{
//Arrange
int id = 0; //Wrong Id
//Act
var result = _postsRepository.DeletePost(id);
//Assert
Assert.IsFalse(result);
}
In this step we are passing invalid id to the DeletePost method and checking if it returns false.
Now the test writing is finished. To run the tests click on the “Test” tab on the top bar of visual studio and select “Run All Tests”. It shows the result of the test in Test Explorer.
Find the code on Git: Visit Here
If you face challenges with unit testing in a Clean Architecture project using NUnit, we’re here to assist. Hire experienced .NET developers from Vivasoft.
Our team will deliver fast solutions, ensuring your project is completed with excellence and efficiency.
Contact us today to discuss your requirements and let us provide the best solution for your project.