Writing Unit Tests For Our Applications Via xUnit, FakeItEasy And Shouldly

Background

 
Usually, we write some unit tests ourselves to cover the code we wrote because unit tests can verify that the component we wrote works fine when we ran it independently.
 
Sometimes, software developers attempt to save time by doing minimal unit testing or not writing a line of test code. This is a myth that it saves time because skipping on unit testing leads to higher defect fixing costs during Integration Testing, System Testing, and even Acceptance Testing after the application is completed.
 
Proper unit testing done during the development stage saves both, time and money, in the end.
 
Based on my limited experience, there are some reasons that make it hard to write unit tests.
  1. Untestable Code.
  2. No idea of unit tests.
  3. Companies don't care about unit tests.
  4. One-time project
The first two are the key reasons. 
 
In this article, I will share how I write unit tests step by step with a sample.
 
Let's take a look at the components we will use at first.
 

Components

 
There are many components that we can use in .NET Core. In this article, I will demonstrate with components that I use often.
 
xUnit.net
 
 
xUnit.net is a free, open source, community-focused unit testing tool for the .NET Framework. Written by the original inventor of NUnit v2, xUnit.net is the latest technology for unit testing C#, F#, VB.NET, and other .NET languages. xUnit.net works with ReSharper, CodeRush, TestDriven.NET, and Xamarin. It is part of the .NET Foundation and operates under its code of conduct. It is licensed under Apache 2 (an OSI approved license).
 
FakeItEasy
 
 
FakeItEasy is a .NET dynamic fake framework for creating all types of fake objects, mocks, stubs, etc.
 
Shouldly
 
 
Shouldly is an assertion framework which focuses on giving great error messages when the assertion fails while being simple and terse.
 
Example
 
Suppose we now have several simple scenes.
  1. Create a new user which should have a strong password.
  2. Modify user's password
  3. Delete a user via his/her name but if the name does not exist, we should warn the operator. 
Note 
In this sample, strong password means that it's not empty and its length is greater than 4.
 
If we get the requirement for the above scene, how do we start to write our code?
 

Domains

 
Before we define a domain class, we should consider what behaviors it should have!
 
UserInfo must have a method for checking the password whether it is strong or not, so we use it for an example.
 
The following test methods show us how many test cases we should cover when we write code. As you can see, we just have identified a number of situations that may arise.
  1. public class UserInfoTests  
  2. {  
  3.     [Theory]  
  4.     [InlineData("")]  
  5.     [InlineData(" ")]  
  6.     [InlineData(null)]  
  7.     public void CheckIsStrongPassword_Should_Return_False_When_Password_Is_Empty(string pwd)  
  8.     {  
  9.   
  10.     }  
  11.   
  12.     [Theory]  
  13.     [InlineData("1234")]  
  14.     [InlineData("123")]  
  15.     [InlineData("12")]  
  16.     [InlineData("1")]  
  17.     public void CheckIsStrongPassword_Should_Return_False_When_Password_Length_LessThan_Five(string pwd)  
  18.     {  
  19.   
  20.     }  
  21.   
  22.     [Theory]  
  23.     [InlineData("12345")]  
  24.     [InlineData("123456")]  
  25.     public void CheckIsStrongPassword_Should_Return_True_When_Password_Length_GreatOrEqual_Five(string pwd)  
  26.     {  
  27.   
  28.     }  
  29.       
  30.     // other behaviors...  
  31. }  
Note 
We also can make the test methods fail to substitute for empty test methods.
 
Next time, we will create a UserInfo class. It contains some import properties and behaviors.
  1. public class UserInfo  
  2. {  
  3.     public int Id { getset; }  
  4.   
  5.     public string UserName { getset; }  
  6.   
  7.     public string Email { getset; }  
  8.   
  9.     public string Password { getset; }  
  10.   
  11.     public bool IsDel { getset; }        
  12.       
  13.     public bool CheckIsStrongPassword()  
  14.     {  
  15.         return !(string.IsNullOrWhiteSpace(Password) || Password.Length < 5);  
  16.     }  
  17.       
  18.     // others...  
  19. }  
Then, we will fill the test methods for the domain class.
  1. [Theory]  
  2. [InlineData("")]  
  3. [InlineData(" ")]  
  4. [InlineData(null)]  
  5. public void CheckIsStrongPassword_Should_Return_False_When_Password_Is_Empty(string pwd)  
  6. {  
  7.     var user = new UserInfo { Password = pwd };  
  8.   
  9.     var flag = user.CheckIsStrongPassword();  
  10.   
  11.     flag.ShouldBe(false);  
  12. }  
  13.   
  14. [Theory]  
  15. [InlineData("1234")]  
  16. [InlineData("123")]  
  17. [InlineData("12")]  
  18. [InlineData("1")]  
  19. public void CheckIsStrongPassword_Should_Return_False_When_Password_Length_LessThan_Five(string pwd)  
  20. {  
  21.     var user = new UserInfo { Password = pwd };  
  22.   
  23.     var flag = user.CheckIsStrongPassword();  
  24.   
  25.     flag.ShouldBe(false);  
  26. }  
  27.   
  28. [Theory]  
  29. [InlineData("12345")]  
  30. [InlineData("123456")]  
  31. public void CheckIsStrongPassword_Should_Return_True_When_Password_Length_GreatOrEqual_Five(string pwd)  
  32. {  
  33.     var user = new UserInfo { Password = pwd };  
  34.   
  35.     var flag = user.CheckIsStrongPassword();  
  36.   
  37.     flag.ShouldBe(true);  
  38. }  
The following screenshot shows the test result.
 
 
It's really not hard to write unit tests for our application.
 
The next section will introduce some more complex tests for our business.
 

Business Layer

 
Business layer plays a very important role in this article. It contains how to mock up an operation of the database and other related services.
 
We can write some test cases based on the above requirement.
  1. public class UserInfoBizTests  
  2. {  
  3.     [Fact]  
  4.     public async Task CreateUser_Should_Return_1001_When_Password_Is_Weak()  
  5.     {           
  6.     }  
  7.   
  8.     [Fact]  
  9.     public async Task CreateUser_Should_Return_9000_When_Add_Failed()  
  10.     {            
  11.     }  
  12.   
  13.     [Fact]  
  14.     public async Task CreateUser_Should_Return_0_When_Add_Succeed()  
  15.     {           
  16.     }  
  17.   
  18.     [Fact]  
  19.     public async Task CreateUser_Should_Trigger_SendEmail_When_Add_Succeed()  
  20.     {            
  21.     }  
  22.   
  23.     [Fact]  
  24.     public async Task CreateUser_Should_Not_Trigger_SendEmail_When_Add_Failed()  
  25.     {           
  26.     }  
  27.   
  28.     [Fact]  
  29.     public async Task DeleteUser_Should_Return_2001_When_User_Is_Not_Exist()  
  30.     {           
  31.     }  
  32.   
  33.     [Fact]  
  34.     public async Task DeleteUser_Should_Return_2002_When_User_Is_Deleted()  
  35.     {           
  36.     }  
  37.   
  38.     [Fact]  
  39.     public async Task DeleteUser_Should_Return_0_When_Delete_Succeed()  
  40.     {            
  41.     }  
  42.   
  43.     [Fact]  
  44.     public async Task DeleteUser_Should_Return_9000_When_Delete_Failed()  
  45.     {           
  46.     }  
  47.   
  48.     [Fact]  
  49.     public async Task ModifyPassword_Should_Return_2001_When_User_Is_Not_Exist()  
  50.     {            
  51.     }  
  52.   
  53.     [Fact]  
  54.     public async Task ModifyPassword_Should_Return_1001_When_Password_Is_Weak()  
  55.     {            
  56.     }  
  57.   
  58.     [Fact]  
  59.     public async Task ModifyPassword_Should_Return_0_When_Modify_Succeed()  
  60.     {           
  61.     }  
  62.   
  63.     [Fact]  
  64.     public async Task ModifyPassword_Should_Return_9000_When_Modify_Failed()  
  65.     {          
  66.     }  
  67. }  
We will define three methods in the IUserInfoBiz interface. Why do we not only add a UserInfoBiz? Because other services may depend on it.
  1. public interface IUserInfoBiz  
  2. {  
  3.     Task<(int code, string msg)> CreateUserAsync(CreateUserDto dto);  
  4.   
  5.     Task<(int code, string msg)> ModifyPasswordAsync(ModifyPasswordDto dto);  
  6.   
  7.     Task<(int code, string msg)> DeleteUserAsync(DeleteUserDto dto);  
  8. }  
Also, we should add the Implementclass for it. However, we should add some dependencies at first.
 
Add a repository named IUserInfoRepository to operate the data but we will not add an Implement class for it, because we don't care how it implements!!
  1. public interface IUserInfoRepository  
  2. {  
  3.     Task<bool> CreateUserAsync(UserInfo userInfo);  
  4.   
  5.     Task<bool> ModifyUserAsync(UserInfo userInfo);  
  6.   
  7.     Task<bool> DeleteUserAsync(UserInfo userInfo);  
  8.   
  9.     Task<UserInfo> GetUserInfoByUserNameAsync(string userName);  
  10. }  
It's the same as INotifyBiz which contains a method to send email to the user.
  1. public interface INotifyBiz  
  2. {  
  3.     Task<bool> SendEmail(string email);  
  4. }  
After finishing the definition of dependencies, we can implement the business logic of UserInfoBiz.
  1. public class UserInfoBiz : IUserInfoBiz  
  2. {  
  3.     private readonly ILogger _logger;  
  4.     private readonly IUserInfoRepository _repo;  
  5.     private readonly INotifyBiz _notifyBiz;  
  6.   
  7.     public UserInfoBiz(ILoggerFactory loggerFactory, IUserInfoRepository repo, INotifyBiz notifyBiz)  
  8.     {  
  9.         _logger = loggerFactory.CreateLogger<UserInfoBiz>();  
  10.         _repo = repo;  
  11.         _notifyBiz = notifyBiz;  
  12.     }  
  13.   
  14.     public async Task<(int code, string msg)> CreateUserAsync(CreateUserDto dto)  
  15.     {  
  16.         // ignore some params check...  
  17.   
  18.         // here can use AutoMapper to impore  
  19.         var userInfo = dto.GetUserInfo();  
  20.   
  21.         var isStrongPassword = userInfo.CheckIsStrongPassword();  
  22.   
  23.         if (!isStrongPassword) return (1001, "password is too weak");  
  24.   
  25.         var isSucc = await _repo.CreateUserAsync(userInfo);  
  26.   
  27.         if (isSucc)  
  28.         {  
  29.             await _notifyBiz.SendEmail(userInfo.Email);  
  30.             _logger.LogInformation("create userinfo succeed..");  
  31.             return (0, "ok");  
  32.         }  
  33.         else  
  34.         {  
  35.             _logger.LogWarning("create userinfo fail..");  
  36.             return (9000, "error");  
  37.         }  
  38.     }  
  39.   
  40.     public async Task<(int code, string msg)> DeleteUserAsync(DeleteUserDto dto)  
  41.     {  
  42.         // ignore some params check...  
  43.   
  44.         var userInfo = await _repo.GetUserInfoByUserNameAsync(dto.UserName);  
  45.   
  46.         if(userInfo == nullreturn (2001, "can not find user");  
  47.   
  48.         var status = userInfo.CheckUserStatus();  
  49.   
  50.         if(status) return (2002, "user is already been deleted");            
  51.   
  52.         var isSucc = await _repo.DeleteUserAsync(userInfo);  
  53.   
  54.         if (isSucc)  
  55.         {  
  56.             _logger.LogInformation($"delete {dto.UserName} succeed..");  
  57.             return (0, "ok");  
  58.         }  
  59.         else  
  60.         {  
  61.             _logger.LogWarning($"delete {dto.UserName} fail..");  
  62.             return (9000, "error");  
  63.         }  
  64.     }  
  65.   
  66.     public async Task<(int code, string msg)> ModifyPasswordAsync(ModifyPasswordDto dto)  
  67.     {  
  68.         // ignore some params check...  
  69.   
  70.         var userInfo = await _repo.GetUserInfoByUserNameAsync(dto.UserName);  
  71.   
  72.         if (userInfo == nullreturn (2001, "can not find user");  
  73.   
  74.         userInfo.ModifyPassword(dto.Password);  
  75.   
  76.         var isStrongPassword = userInfo.CheckIsStrongPassword();  
  77.   
  78.         if (!isStrongPassword) return (1001, "password is too weak");  
  79.   
  80.         var isSucc = await _repo.ModifyUserAsync(userInfo);  
  81.   
  82.         if (isSucc)  
  83.         {  
  84.             _logger.LogInformation($"modify password of {dto.UserName} succeed..");  
  85.             return (0, "ok");  
  86.         }  
  87.         else  
  88.         {  
  89.             _logger.LogWarning($"modify password of {dto.UserName} fail..");  
  90.             return (9000, "error");  
  91.         }  
  92.     }  
  93. }  
Now, we should fill the test methods!
 
Due to the fact that UserInfoBiz depends on IUserInfoRepository and INotifyBiz, both of them are not implemented, and we should not be affected by them when we test UserInfoBiz. So, here, we will use FakeItEasy to mock them.
 
The following code demonstrates how to mock dependent interface and create the test object of UserInfoBiz.
  1. private IUserInfoRepository _repo;  
  2. private INotifyBiz _notifyBiz;  
  3. private UserInfoBiz _biz;  
  4.   
  5. public UserInfoBizTests()  
  6. {  
  7.     // mock IUserInfoRepository  
  8.     _repo = A.Fake<IUserInfoRepository>();  
  9.     // mock INotifyBiz  
  10.     _notifyBiz = A.Fake<INotifyBiz>();  
  11.     var loggerFactory = Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory.Instance;  
  12.   
  13.     _biz = new UserInfoBiz(loggerFactory, _repo, _notifyBiz);  
  14. }  
What we should do in the next step is to write the test code.
 
Here, I choose CreateUser_Should_Return_0_When_Add_Succeed and CreateUser_Should_Trigger_SendEmail_When_Add_Succeed as the two test cases to show.
 
When CreateUser returns 0, it means the database operation has succeeded.
  1. private void FakeCreateUserAsyncReturnTrue()  
  2. {  
  3.     A.CallTo(() => _repo.CreateUserAsync(A<CoreLayer.Domains.UserInfo>._)).Returns(Task.FromResult(true));  
  4. }  
The above code tells us that when the method named CreateUserAsync with any parameters was called, it will return Task.FromResult(true).
 
It's really a good way to avoid the triggering operation of the database when we test our code.
 
The whole test method of CreateUser_Should_Return_0_When_Add_Succeed is as follows.
  1. [Fact]  
  2. public async Task CreateUser_Should_Return_0_When_Add_Succeed()  
  3. {  
  4.     FakeCreateUserAsyncReturnTrue();  
  5.       
  6.     var dto = new CreateUserDto { UserName = "catcher", Password = "123456", Email = "[email protected]" };  
  7.       
  8.     var (code, msg) = await _biz.CreateUserAsync(dto);  
  9.       
  10.     code.ShouldBe(0);  
  11.     msg.ShouldBe("ok");  
  12. }  
Now, when we call the CreateUserAsync method of UserInfoBiz, it will always create a new user successfully, no matter if the database is down or there's another infrastructure accident.
 
Turning to CreateUser_Should_Trigger_SendEmail_When_Add_Succeed--, this test case means that when creating a user successfully, our code should call the SendEmail method of INotifyBiz.
 
It also means that our code just ensures to call this method, but in UserInfoBiz, we don't care to send email to a user unsuccessfully or successfully, because the result of sending email should consider by INotifyBiz.
 
So, our test code will look like this.
  1. [Fact]  
  2. public async Task CreateUser_Should_Trigger_SendEmail_When_Add_Succeed()  
  3. {  
  4.     FakeCreateUserAsyncReturnTrue();  
  5.   
  6.     var dto = new CreateUserDto { UserName = "catcher", Password = "123456", Email = "[email protected]" };  
  7.   
  8.     var (code, msg) = await _biz.CreateUserAsync(dto);  
  9.   
  10.     A.CallTo(() => _notifyBiz.SendEmail(dto.Email)).MustHaveHappened();  
  11. }  

Here is the result when we run the test methods.

 
Here is the source code you can find in my GitHub page.

Summary

 
This article showed you an easy sample to demonstrate how to write unit tests for our application via xUnit, FakeItEasy, and Shouldly. Also, it suggested that we all should write some unit tests for our applications. Because it has the following advantages.
  1. Reduces Defects in the Newly developed features or reduces bugs when changing the existing functionality.
  2. Reduces Cost of Testing as defects are captured in a very early phase.
  3. Improves the design and allows better refactoring of code.
  4. Unit Tests, when integrated with build gives the quality of the build as well......
I hope this article can help you!