In this post, I share the three libraries I find useful for unit testing a .NET project: EF Core SQLite Database Provider, Bogus and Moq.
Before I learned about and started using Entity Framework in projects, I was reluctant to test database related codes because I thought the tests would have to connect to an external database, which could be a problem especially when using a CI process to build the app. However, Entity Framework SQLite provider makes it much straightforward to setup an in memory database and a DBContext wrapping around it. I did not run into much troubles when following the document here.
When using Entity Framework, you need to create a class that extends from DBContext. If you use Code First Approach, you also model the database schema by defining the entities and relationships between them in this class. You can then add the class to the dependency container such that you can use it in services that need to interact with the database.
The DBContext has an underlying connection to a database. For my unit tests, I create a base class in which I setup an in-memory database, as shown in the below snippets.
[Collection("Database query tests")] public class BaseInfrastructureTest: IDisposable { protected DbContextOptions < ApplicationContext > ContextOptions { get; } private const string ConnectionString = "DataSource = myshareddb; mode = memory; cache = shared"; private DbConnection _connection; protected BaseInfrastructureTest() { ContextOptions = new DbContextOptionsBuilder < ApplicationContext > ().UseSqlite(CreateInMemoryDatabase()).Options; } protected ApplicationContext GetSeededDBContext() { var dbContext = new ApplicationContext(ContextOptions); dbContext.Database.EnsureDeleted(); dbContext.Database.EnsureCreated(); PopulateStatusTable(dbContext); PopulateRoleTable(dbContext); PopulateMediaTypeTable(dbContext); dbContext.SaveChanges(); return dbContext; } private void PopulateMediaTypeTable(ApplicationContext dbContext) { foreach(var id in new MediaType[] { MediaType.Internet, MediaType.Magazines, MediaType.Movies, MediaType.Music, MediaType.Newspapers, MediaType.PublicBroadcasting, MediaType.Radio, MediaType.Television }) { var mediaType = new Domain.Entities.MediaType() { Id = (int) id, Description = id.ToString() }; dbContext.Add(mediaType); dbContext.SaveChanges(); } } private void PopulateRoleTable(ApplicationContext dbContext) { // codes omitted for brevity } private void PopulateStatusTable(ApplicationContext dbContext) { // codes omitted for brevity } public void Dispose() { _connection.Close(); } private DbConnection CreateInMemoryDatabase() { if (_connection == null) { _connection = new SqliteConnection(ConnectionString); _connection.Open(); } return _connection; } }
You may have noticed the annotation [Collection(“Database query tests”)]. This is to prevent parallel execution of tests under the same collection. When my tests ran in parallel, I got failures when one test changed the database state which affected the other tests. Adding the annotation resolves this issue for me. For more info, checkout the document here.
I use this library to generate fake entities and data transfer objects on demand. The library is awesome and straightforward to use. Below is an example of how I use the library to generate a fake User object on demand.
public User GetNewTransion() { // some codes were omitted for brevity return new Faker < User > () .RuleFor(m => m.CreatedDate, f => f.Date.Past()) .RuleFor(m => m.LastUpdateDate, f => f.Date.Past()) .RuleFor(m => m.StatusId, f => f.Random.Number(1, 4)) .RuleFor(m => m.RoleId, f => f.Random.Number(1, 3)) .RuleFor(m => m.Email, f => f.Lorem.Word()).Generate(); }
public class User { public int Id { get; set; } public int StatusId { get; set; } public int RoleId { get; set; } public String Email { get; set; } public DateTime CreatedDate { get; set; } public DateTime LastUpdateDate { get; set; } }
For more info on adding and setting up Bogus, checkout the document here.
Moq is an intuitive and straightforward to use library for creating a mock instance of a class that implement an interface.
As an example, I have a service that retrieves user’s info from both SQL database as well as from an azure ADB2C tenant. Below I show the codes that extract the info from Microsoft.Graph.User instance and convert into a data transfer object.
private B2CUserAdditionalInfo ExtractAdditionalInfoFromGraphUser(User user) { B2CUserAdditionalInfo addInfo = new B2CUserAdditionalInfo(); addInfo.Id = user.Id; addInfo.MobileNumber = user.MobilePhone; addInfo.Name = user.DisplayName; if (string.IsNullOrEmpty(addInfo.MobileNumber)) { var keys = user.AdditionalData?.Keys.ToList(); if (keys != null) { var mobilekey = keys.Where(m => m.Contains("MobileNumber")).FirstOrDefault(); addInfo.MobileNumber = user.AdditionalData[mobilekey].ToString(); } } return addInfo; }
During QA, the app crashed when ExtractAdditionalInfoFromGraphUser get called. The exception was at the line addInfo.MobileNumber = user.AdditionalData[mobilekey].ToString();
. Can you notice what is wrong at that line? If the user.AdditionalData dictionary does not contain the key “MobileNumber”, the code would cause a NullReferenceException. The user.AdditionalData dictionary would not contain this key if the user did not provide a phone number during registration.
Following test driven design, I first need to write a test case that would reproduce the issue. My test would need to call ExtractAdditionalInfoFromGraphUser with a fake user instance of which AdditionalData dictionary does not contain the key “MobileNumber”. The test would fail. I then need to fix the code to make the test pass. Because B2CUserAdditionalInfo is a private method, I cannot test the method directly. Instead, I can test the public method that eventually calls ExtractAdditionalInfoFromGraphUser. The public method is part of the service which utilizes Microsoft.Graph.IGraphServiceClient to interact with Microsoft Graph API. Below I show the service and the relevant methods.
public class GraphHelper: IGraphHelper { private readonly ILogger < GraphHelper > _logger; private readonly IGraphServiceClient _graphClient; private readonly string _defaultUserSelectAttributes; private readonly AzureADB2COptions _azureADB2COptions; public GraphHelper(ILogger < GraphHelper > logger, IGraphServiceClient graphServiceClient, IOptions < AzureADB2COptions > azureADB2COptions) { _logger = logger; _graphClient = graphServiceClient; _azureADB2COptions = azureADB2COptions.Value; _defaultUserSelectAttributes = $ "displayName,givenName,surName,mobilePhone,userPrincipalName,id, {_azureADB2COptions.ExtensionAttributeMobileNumberFullName}"; } public async Task < IDictionary < string, B2CUserAdditionalInfo >> GetAdditionalUsersInfoByEmails(IList < string > emails) { if (emails == null) { throw new ArgumentException(string.Format("Param '{0}' cannot be null", "emails")); } B2CUserAdditionalInfo[] usersAdditionalInfo = await RetrieveAdditionalInfoForUsers(emails); IDictionary < string, B2CUserAdditionalInfo > result = new Dictionary < string, B2CUserAdditionalInfo > (); foreach(var additionalInfo in usersAdditionalInfo) { result.Add(additionalInfo.Email, additionalInfo); } return result; } private async Task < B2CUserAdditionalInfo[] > RetrieveAdditionalInfoForUsers(IList < string > emails) { IList < Task < B2CUserAdditionalInfo >> userRetrievalTasks = emails.Select(email => RetrieveAdditionalInfoForUser(email)).ToList(); return await Task.WhenAll(userRetrievalTasks); } private async Task < B2CUserAdditionalInfo > RetrieveAdditionalInfoForUser(string email) { var filter = $ "identities / any(id: id / issuer eq '{_azureADB2COptions.Issuer}' and id / issuerAssignedId eq '{WebUtility.UrlEncode(email)}')"; var user = await ExpectExactUser(filter); var additionalUserInfo = ExtractAdditionalInfoFromGraphUser(user); additionalUserInfo.Email = email; return additionalUserInfo; } private async Task < User > ExpectExactUser(string filter) { var users = await _graphClient.Users.Request().Filter(filter).Select(_defaultUserSelectAttributes).GetAsync(); if (users.Count == 0) { throw new KeyNotFoundException($"Not able to find user with given filter: {filter}"); } else if (users.Count > 1) { throw new ArgumentException($"More than one users found with given filter: {filter}"); } return users[0]; } private B2CUserAdditionalInfo ExtractAdditionalInfoFromGraphUser(User user) { B2CUserAdditionalInfo addInfo = new B2CUserAdditionalInfo(); addInfo.Id = user.Id; addInfo.MobileNumber = user.MobilePhone; addInfo.Name = user.DisplayName; if (string.IsNullOrEmpty(addInfo.MobileNumber)) { var keys = user.AdditionalData?.Keys.ToList(); if (keys != null) { var mobilekey = keys.Where(m => m.Contains("MobileNumber")).FirstOrDefault(); addInfo.MobileNumber = user.AdditionalData[mobilekey].ToString(); } } return addInfo; } }
I cannot directly call ExtractAdditionalInfoFromGraphUser since it’s a private method. But I can test the public method GetAdditionalUsersInfoByEmails. However, I need to create an instance of GraphHelper. The GraphHelper class requires a few dependencies, one of which is the
Microsoft.Graph.IGraphServiceClient. The graph service client is for calling Microsoft Graph API, as shown by the code below:
var users = await _graphClient.Users.Request().Filter(filter).Select(_defaultUserSelectAttributes).GetAsync();
It may be daunting if I have to manually construct the dependencies when creating the GraphHelper class in my test. Fortunately, Moq makes this task much simpler. Below snippets show how I use Moq to construct a Microsoft.Graph.IGraphServiceClient that would return a list which contains a fake Microsoft.Graph.User object.
// arrange var fakeEmail = "test123@email.com"; var fakeAdditionalDataWithNoPhoneNumber = new Dictionary < string, object > (); var fakeGraphUser = new User() { Id = "123", AdditionalData = fakeAdditionalDataWithNoPhoneNumber }; var fakeGraphUserCollectionPage = new GraphServiceUsersCollectionPage(); fakeGraphUserCollectionPage.Add(fakeGraphUser); var mockGraphClient = new Mock < IGraphServiceClient > (); mockGraphClient.Setup(m => m.Users.Request().Filter(It.IsAny < string > ()).Select(It.IsAny < string > ()).GetAsync()).ReturnsAsync(fakeGraphUserCollectionPage);
Below snippets show the full test class.
public class GraphHelperTests { private readonly IGraphHelper _graphHelper; public GraphHelperTests() { } [Fact] public void GetAdditionalUsersInfoByEmails_NoPhoneNumber() { // arrange var fakeEmail = "test123@email.com"; var fakeAdditionalDataWithNoPhoneNumber = new Dictionary < string, object > (); var fakeGraphUser = new User() { Id = "123", AdditionalData = fakeAdditionalDataWithNoPhoneNumber }; var fakeGraphUserCollectionPage = new GraphServiceUsersCollectionPage(); fakeGraphUserCollectionPage.Add(fakeGraphUser); var mockGraphClient = new Mock < IGraphServiceClient > (); mockGraphClient.Setup(m => m.Users.Request().Filter(It.IsAny < string > ()).Select(It.IsAny < string > ()).GetAsync()).ReturnsAsync(fakeGraphUserCollectionPage); var graphHelper = BuildGraphHelper(mockGraphClient.Object); // act var b2cAdditionalInfoDict = graphHelper.GetAdditionalUsersInfoByEmails(new string[] { fakeEmail }).Result; // assert Assert.NotNull(b2cAdditionalInfoDict); Assert.Single(b2cAdditionalInfoDict); var b2cAdditionalInfo = b2cAdditionalInfoDict[fakeEmail]; Assert.NotNull(b2cAdditionalInfo); Assert.Equal(fakeGraphUser.Id, b2cAdditionalInfo.Id); } private IGraphHelper BuildGraphHelper(IGraphServiceClient graphServiceClient) { var mockLogger = Mock.Of < ILogger < GraphHelper >> (); var azureADB2COptions = Options.Create(new AzureADB2COptions() { ExtensionAttributeMobileNumber = "MobileNumber", B2CExtensionsAppClientId = "Test123" }); return new GraphHelper(mockLogger, graphServiceClient, azureADB2COptions); } }
Without modifying the method in GraphHelper, I ran the test and it failed as expected. After adding the conditional statement to guard against referencing a null object, I was able to get the test to pass. Below snippets show the ExtractAdditionalInfoFromGraphUser method with the check in place.
private B2CUserAdditionalInfo ExtractAdditionalInfoFromGraphUser(User user) { B2CUserAdditionalInfo addInfo = new B2CUserAdditionalInfo(); addInfo.Id = user.Id; addInfo.MobileNumber = user.MobilePhone; addInfo.Name = user.DisplayName; if (string.IsNullOrEmpty(addInfo.MobileNumber)) { var keys = user.AdditionalData?.Keys.ToList(); if (keys != null) { var mobilekey = keys.Where(m => m.Contains("MobileNumber")).FirstOrDefault(); if (mobilekey != null) { addInfo.MobileNumber = user.AdditionalData[mobilekey].ToString(); } } } return addInfo; }
I have a service which retrieves the list of users’ requests that need approval. The service retrieves the users’ info from both SQL database using Entity Framework and Azure ADB2C using Microsoft Graph API / MSAL library. Below shows the codes in the service.
public class GetPendingUsersHandler: IRequestHandler < GetPendingRequestsRequest, IList < PendingUserDTO >> { private readonly ApplicationContext _dbContext; private readonly IGraphHelper _graphHelper; public GetPendingUsersHandler(ApplicationContext dbContext, IGraphHelper graphHelper) { _dbContext = dbContext; _graphHelper = graphHelper; } public async Task < IList < PendingUserDTO >> Handle(GetPendingRequestsRequest request, CancellationToken cancellationToken) { var pendingRequests = await _dbContext.AccessRequest .Include(mt => mt.MediaType) .Include(u => u.User) .Where(u => u.User.StatusId == (int) AccessRequestStatus.pending) .Select(pendingRequest => new PendingUserDTO() { Email = pendingRequest.User.Email, MediaType = pendingRequest.MediaType.Description, MediaName = pendingRequest.MediaName, RequestDate = pendingRequest.CreatedDate, Id = pendingRequest.Id, UserId = pendingRequest.User.Id }) .ToListAsync(); IDictionary < string, B2CUserAdditionalInfo > b2cUsersInfo = await _graphHelper.GetAdditionalUsersInfoByEmails(pendingRequests.Select(pendingRequest => pendingRequest.Email).ToList()); UpdatePendingRequestsWithAdditionalInfo(pendingRequests, b2cUsersInfo); return pendingRequests; } private void UpdatePendingRequestsWithAdditionalInfo(List < PendingUserDTO > pendingRequests, IDictionary < string, B2CUserAdditionalInfo > b2cUsersInfo) { foreach(var pendingRequest in pendingRequests) { B2CUserAdditionalInfo b2cAdditionalInfo = null; if (b2cUsersInfo.TryGetValue(pendingRequest.Email, out b2cAdditionalInfo)) { pendingRequest.PhoneNum = b2cAdditionalInfo.MobileNumber; pendingRequest.Name = b2cAdditionalInfo.Name; } } } }
As you can see, the codes above use Entity Framework to interact with the database, and the IGraphHelper service which in turns uses Microsoft.Graph.IGraphServiceClient to interact with Microsoft Graph API. To construct GetPendingUsersHandler class for testing, I use all the three libraries. Below is the codes for the test class.
public class GetPendingUsersHandlerTests: BaseInfrastructureTest { public GetPendingUsersHandlerTests(): base() {} [Fact] public void GetPendingUsers() { using(var dbContext = GetSeededDBContext()) { // Arrange var accessRequestDataOnDemand = new AccessRequestDataOnDemand(dbContext); var accessRequest = accessRequestDataOnDemand.GetNewPersisted((int) Core.Enums.Role.User, (int) AccessRequestStatus.pending); var user = accessRequest.User; IDictionary < string, B2CUserAdditionalInfo > fakeB2CAdditionalInfo = new Dictionary < string, B2CUserAdditionalInfo > (); fakeB2CAdditionalInfo.Add(user.Email, new B2CUserAdditionalInfo() { Email = user.Email, MobileNumber = "123", Name = "ABC" }); var mockGraphHelper = new Mock < IGraphHelper > (); mockGraphHelper.Setup(m => m.GetAdditionalUsersInfoByEmails(It.IsAny < IList < string >> ())).ReturnsAsync(fakeB2CAdditionalInfo); var handler = new GetPendingUsersHandler(dbContext, mockGraphHelper.Object); // act var pendingRequests = handler.Handle(new GetPendingRequestsRequest(), cancellationToken: CancellationToken.None)?.Result; // assert Assert.NotNull(pendingRequests); Assert.Single(pendingRequests); var pendingRequest = pendingRequests[0]; Assert.Equal(user.Email, pendingRequest.Email); Assert.Equal(accessRequest.MediaName, pendingRequest.MediaName); Assert.Equal(fakeB2CAdditionalInfo[user.Email].MobileNumber, pendingRequest.PhoneNum); Assert.Equal(fakeB2CAdditionalInfo[user.Email].Name, pendingRequest.Name); Assert.Equal(user.Id, pendingRequest.UserId); Assert.Equal(accessRequest.Id, pendingRequest.Id); } } }
In the above snippets, GetSeededDBContext is in the BaseInfrastructureTest class which uses SQLite to construct an in-memory database as shown at the beginning of the post.
The AccessRequestDataOnDemand is a helper class which uses the Bogus library to instantiate fake instances of AccessRequest entity class, as shown in the snippets below.
public override AccessRequest GetNewTransion() { return GetNewTransion((int)Core.Enums.Role.User, (int)AccessRequestStatus.pending); } public AccessRequest GetNewTransion(int roleId, int statusId) { var mediaType = _appContext.MediaType.Find((int)Core.Enums.MediaType.Internet); var user = _userDataOnDemand.GetNewTransion(roleId, statusId); return new Faker<AccessRequest>() .RuleFor(m => m.MediaName, f => f.Lorem.Word()) .RuleFor(m => m.MediaTypeId, mediaType.Id) .RuleFor(m => m.MediaType, mediaType) .RuleFor(m => m.MediaDomain, f => f.Lorem.Word()) .RuleFor(m => m.CreatedDate, f => f.Date.Past()) .RuleFor(m => m.User, user).Generate(); }
Finally, the test method use Moq to quickly setup an instance of IGraphHelper.
In conclusion, I would like to use a paragraph about unit tests in the the book “Clean Code” by Robert Martin.
Tests are as important to the health of a project as the production code is. Perhaps they are even more important, because tests preserve and enhance the flexibility, maintainability, and reusability of the production code. So keep your tests constantly clean. Work to make them expressive and succinct. Invent testing APIs that act as domain-specific language that helps you write the tests.
Martin, Robert. “Unit Tests.” Clean Code, e-book, Pearson Education, Inc, 2009, p. 133.
Beside inventing testing APIs, you can also use the ones other people have built, such as the three libraries I share in this post to make your testing easier.
Building a fully multitenant system using Microsoft Identity Framework and SQL Row Level Security
Analyzing a rental property through automation
Web scraping in C# using HtmlAgilityPack
Building multitenant application – Part 2: Storing value into database session context from ASP.NET core web API
Build and deploy a WebJob alongside web app using azure pipelines
Authenticate against azure ad using certificate in a client credentials flow
Notes on The Clean Architecture
How to retrieve connection strings in azure key vault from ASP.NET using configuration builders, XML transformation and azure devops.