Implementing Unit Test in a .Net Core Web API Project with Clean Architecture using N-Unit

Last Update: August 19, 2024
.Net Core Web API Project with Clean Architecture
Table of Contents
Contributors
Picture of Vivasoft Team
Vivasoft Team
Tech Stack
0 +
Want to accelerate your software development company?

It has become a prerequisite for companies to develop custom software products to stay competitive.

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. 

Clean Architecture project with N-Unit

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 must cover all the units inside a project or solution. Hence we will implement unit tests in the Application and Infrastructure layers of the demo project we have created. We skip Domain because it does not contain any logical code. Also we skip WebAPI project because it will be tested while integration test builds.

Unit Test on Application Layer:

In this part we implement unit tests on services of Application layer. We created a project with the N-Unit test project template and installed the Nuget packages named Moq, FluentAssertion, and AutoFixture. We named it, CleanArchitecture.Application.UnitTest project. We added the CleanArchitecture.Application project as a dependency on it.
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<IPostsRepository> _postsRepositoryMock;
        private PostsService _postsService;

        [SetUp]
        public void Setup()
        {
            _fixture = new Fixture();
            _postsRepositoryMock = _fixture.Freeze<Mock<IPostsRepository>>();
            _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<ResponseModel> 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<ResponseModel>();
            Assert.IsAssignableFrom<Post[]>(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<ResponseModel> 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<int>();
            _postsRepositoryMock.Setup(p => p.GetPostByID(It.IsAny<int>())).ReturnsAsync(new Post());

            //Act
            var response = await _postsService.GetPostByID(id);

            //Assert
            response.Should().NotBeNull();
            response.Should().BeOfType<ResponseModel>();
            response.Data.Should().BeOfType<Post>();
            _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<ResponseModel>();
            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<ResponseModel> InsertPost(Post objPost)
        {
            var validationErrors = new Dictionary<string, string>();

            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<ResponseModel>();
            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<ResponseModel>();
            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<ResponseModel> UpdatePost(Post objPost)
        {

            var validationErrors = new Dictionary<string, string>();


            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<int>()))
                .ReturnsAsync(post);
            _postsRepositoryMock.Setup(x => x.UpdatePost(post))
                .ReturnsAsync(true);

            //Act
            var response = await _postsService.UpdatePost(post);

            //Assert
            response.Should().NotBeNull();
            response.Should().BeOfType<ResponseModel>();
            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<ResponseModel>();
            _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<ResponseModel>();
            response.ValidationErrors.Should().HaveCount(1);
            _postsRepositoryMock.Verify(p => p.UpdatePost(post), Times.Never());
            Assert.Pass();
        }

				
			

Delete Post Method

				
					        public async Task<ResponseModel> 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<int>();
            _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<ResponseModel>();
            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<ResponseModel>();
            _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

Unit Test on Application 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<PostContext>().UseInMemoryDatabase(databaseName: "TestPostDB").Options;


            _dbContextMock = new PostContext(options);

            _dbContextMock.Posts.AddRange(
                    new List<Post>()
                    {
                        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<IEnumerable<Post>> 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<IEnumerable<Post>>();

        }

				
			

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<Post> 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<int> 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<Post>();

            //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<bool> 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.

Run All Tests in Text 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.

Potential Developer
Tech Stack
0 +
Accelerate Your Software Development Potential with Us
With our innovative solutions and dedicated expertise, success is a guaranteed outcome. Let's accelerate together towards your goals and beyond.
Blogs You May Love

Don’t let understaffing hold you back. Maximize your team’s performance and reach your business goals with the best IT Staff Augmentation