The guardian angels of your code

Image not Found

Target Audience: Developers

Introduction

In any software development life cycle, software testing plays a major role in ensuring the given application meets the expected business objects as defined. This can be in the form of UI/UX, API, Business/Functionality, Security or Performance aspects. Moreover, this can be further divided into automation and manual testing. Usually, functional, smoke, regression and release testing are performed by field expertise in QA or Staging environments and there is a controversy on feasibility of conducting end-to-end testing each and every release to ensure nothing is broken from previous to current release.

I strongly believe as developers, it is our responsibility to release a fully tested stable (not only just the feature but also the existing functionality) source code to QA environments to minimize the back and forth iterations. Eventually, it will increase the throughput of the project development life cycle.

Unit Testing plays a major role in ensuring individual modules/business rules are tested and validated in all aspects by the developer to determine whether they are fit for use prior to any releasing activities.

Vision & Goal

In this series of articles on Unit Testing (using xUnit Framework), I will be covering the following sections as in multiple articles to minimize the reading overhead and build up as in a step by step guide.

1. Unit Testing [Part 1]: Use of In-Memory database & mock with

Builder Pattern - Service Layer Testing

2. Unit Testing [Part 2]: Testing API Endpoints & contracts

3. Unit Testing [Part 3]: Generate Code Coverage Reports

4. Unit Testing [Part 4]: Integrate Code Coverage into

Azure Build Pipeline.

Ultimate goal of the above articles is to increase the quality of the code and practice writing unit tests in all the projects regardless of the scale of the software.

Expected Ratio

  • Service Layer: 100%

  • API Layer : at least 80%

  • Integration Endpoints: at least 80%

  • Front end: above 60%

For illustration purposes, I have created the following class libraries/Api projects along with the below npm packages.

DemoApp.Services.Tests

  • FluentAssertions

  • Microsoft.EntityFrameworkCore.InMemory

  • Moq

  • xunit

  • Xunit.runner.visualstudio

DemoApp.Services

  • Microsoft.EntityFrameworkCore

DemoApp.Domain.Models / DemoApp.Domain.Entities

  • N/A

DemoApp.API

  • Swashbuckle.AspNetCore

Quick look on Builder Pattern

GOF (Gang of Four) says:

“Separate the construction of a complex object from its representation so that the same construction process can create different representations”

The Builder Pattern is a creational design pattern which is used to build a complex object by using a step by step approach. Moreover, extract the object construction code out of its own class and put it in a separate object which we call a builder and this class is responsible for creating an instance of a specific object.

What is a Fixture

When executing a set of Test Cases, we will need to set up a common data set throughout the test execution depending on the scope of the Tests. Sometimes, we might need to set up fresh data in each and every test case, sometimes just one single shared context per test class. Concept of Test Fixture comes in handy to streamline the test data preparation. xUnit has provided following:

1. Constructor and Dispose: Use case of cleaning the test context for each and every test without sharing the object instance. Also known as

Test Class as Context

2. Class Fixture: Use case of sharing the same object instance across tests within a single class.

3. Collection Fixture: Use case of sharing the same instance across multiple test classes.


Let’s go through the code snippet and understand the what we are going to do here:

Service Layer Unit Testing: Use of In-Memory databases & mock with Builder Pattern

Note: You will notice a few additional code snippets on Soft Deletion, use of auditable entities and accessing request context values to make a demo application almost similar to production scenario.

If you don’t want a hard deletion, you may introduce the ISoftDelete interface and implement it in relevant entities.

Purpose of an Auditable Entity is to track who has executed the action against the data record. We can set all the relevant values inside the DataContext. I have injected IUserContext to extract user information from the HttpContext.

I will discuss the implementation of IUserContext in the API testing section.

Now, let’s have a look at User Entity which has implemented ISoftDelete and IAuditableEnttity. I have also added a parameterized constructor to support UserBuilder Class in UnitTesting.

One of the most important parts of Application setup is Creating the Datacontext. I have used EF Core DbContext default behavior and have overridden the SaveChanagesAsync() method. All the entities which have implemented IAuditableEntity, can set values as follows. The key benefit of this approach is to reduce the duplication of handling auditable fields in all the entities. In this way, you can centralize the behavior of the Auditable in a more robust way.

Let’s use the builder pattern to create a UserBuilder class. This will contain all or few properties that we are gonna use for Unit Testing, basically the fields that are required for business flow verifications.

Following the naming convention as I have used in the below class which will provide a concise use of the fields in Test Classes.

One last thing before jumping to Unit Test Project is to set up a class Fixture. Ultimate goal of this class is to share the TestDataContext throughout the same class test execution.

We have come to the last part of this article on writing Unit Tests for User Class. All the set up activities are supportive classes/methods to write more meaningful cases to cover the business logic aspects of the service layer.

Note: Readup more on xUnit and understand Fact, InlineData, Theory concepts and apply it depending on your testing scenario.

Here are some test cases that I have picked randomly to showcase Unit Testing scenarios. There are dozens of naming conventions out there, however I’d prefer following

1. MethodName_StateUnderTest_ExpectedBehavior

2. Should_ExpectedBehavior_When_StateUnderTest

Test Case 01: Application should not allow Email duplicates

Test Case 02: Application should allow inserting a user with a new email address and return the inserted Id.

Test Case 03: Deleting an existing user with a given email should return true or false

Test Case 04: Deleting a non-existing user with a given email should throw

Test Case 05: Should return User details for the given existing User Id

Test class constructor

Testing an Extension method with Theory and InlineData

I have created a simple extension method to validate the email address using Regex.

Test Results


This is the end of Unit Testing Part 1


You May Also Like