Tuesday, August 8, 2023

Creating Unit Tests and Maintainable Code with Moq and NUnit in 5 Minutes



What is a Unit Test?

If you have been coding for more than 5 minutes that you have heard of the term Unit Test. You have also heard that you need to have unit tests in your code. But why do I need this test when you can attach the code to a debugger and step through all the code and verify it works. It seems like a lot of unnecessary steps and time that could be spent improving the project. While in small projects this may be true, when working with large enterprise projects with thousands of lines of code the need for unit testing becomes obvious.

As a project grows so does our dependencies on the code. For example, an email service may be referenced throughout the project. Meaning if you update the email service all the references need to be verified that they are in a working state after the change. This can add hours if stepping through each line is necessary. If an update to the code that depends on the email service changes, then all the lines of code with this dependency need to be tested as well. As you can see the chain of testing can get quite extensive.

Unit tests will ensure our code is tested and that any dependencies are still working after an update. Unit testing also allows for testing of several user scenarios that may not come up during initial development. These edge cases can be identified and tested against. This also helps with code updates since these edge cases may not be remembered. For example, you might mail all orders USPS except to those in Texas must be mailed FedEx.  However, FedEx does not deliver to PO Boxes so a unit test for testing PO Boxes is needed to ensure updates to the mail service does not break our shipments to Texas as the next person may not know this inherit knowledge about FedEx which can cause a bug down the road.

In order to cover all our test cases the method of Test Driven Development or TDD for short can be used. TDD is when test cases are created before any coding of the feature is developed. This allows for the developer to think of test scenarios and try to think of edge cases before any logic is written as the logic can skew the developer into not thinking an edge case exists. For example, a simple function called "AddNumbers" which adds two numbers together is very simple. I write the code and add a unit test that inputs 2 numbers and the test returns success. Code works but is open for bugs, what happens if I pass a letter, a null value, an extremely large number, etc. Because I didn't sit down and think of scenarios before writing my function my tests passed but I didn't cover the entire out user cases available.  There are extensions that help with this such as Fine Code Coverage that help with identifying tests that are missed, but this will only go so far if you are not thinking of the use case to handle in the first place.

Unit Testing Terms

There are many terms that refer to testing. These terms are known as "Test Doubles" or referring to what is going to be standing in place of our dependencies like an stunt double would for an actor. 

Some of the most common are Dummy, Fake, Stub, and Mock. These terms are very similar to each other and the descriptions below are how I view them. They may not fit the exact description, but the name of the test double isn't as important as that it is used.

Dummy

Data and objects just used to keep a complier from yelling at you. It has no outcome on the test itself

Fake

A fake is when an object implementation is replaced with a simpler execution and usually replaces dependencies on outside sources. For example, a SQL database call would require a SQL database, the fake would simulate this call and return objects form say a json file or in memory database.

Stub

A stub is a object that is designed to create responses that are predictable so that our test knows what to expect. When unit testing a stub is used when the object's functionality is not necessary a response is needed to complete the test. In construction, stub out is when you leave a pipe out of the dry wall and hide all the plumbing behind. All you care about is the exposed pipe. Same is true in testing, we just need a response, we do not care how the response was created.

Mock

Mocks are a step up from a stub, a stub will always return. Mocks can contain some logic to validate what is being returned. This helps when you need basic logic and null checking. These come in handy for testing edge cases and other variable cases than just the clean path a stub would provide.



Setting up a Unit Test

Planning for unit testing needs to happen at the beginning of a project. To introduce unit testing to a mature project is a large undertaking. Taking a test driven approach allows for understanding the requirements better, planning for edge cases and allows for good design practices.

Using Interfaces in C# is a must when it comes to unit testing setup and design. The interface allows for creating different dependencies for our tests than our project. This allows for stubbing and mocking our dependencies to test our code. When ever there is a dependency on another class this is an indication of a new stub. Using Inversion of Control (dependency injection) will allow for the dependencies to be swapped out for testing and not require the code to be dependent on external items.

Arrange, Act, Assert

Unit tests consist of 3 parts per test: Arrange, Act, Assert. Arrange is prepping the data for the function test. This can be initializing Mocks or Stubs, prepping data models, or initializing variables. Act is next is is executing the function that needs to be tested. Finally Assert, assert is checking that the function executed successfully and returned the correct data. Digging into assert more opens the question, how do you test void and Task functions? NUnit has specific ways for testing these, but a professor once told me to avoid void functions whenever possible. This becomes apparent here in unit testing why you should. Void means empty, nothing, vacant a function that you execute and have no idea what happens, when you think of it in these terms a function should return at the very least bool. Yes I worked or no I didn't giving a concrete answer to assert than just it ran. Another reason is error handling, exceptions are expensive instead of throwing an error, return false and handle the error outside the function for a more flexible application.

Creating a Unit Test in 5 minutes

Create the Web Project

All Blazor projects come with an example of a weather app that fetches random weather forecasts. For our 5 minute project we will repurpose this code to be testable.To begin create a new blazor server application


dotnet new blazorserver -o FiveMinuteProject

Modify the Web Project to be Testable

Currently the weather is just a random set of integers btween -20 and 55. To make our WeatherForecastService testable lets incapsulate the weather call to a repository. First, create a new class called WeatherRepository.cs  under the data folder. Add a method called GetForecast, accepts an int range. Copy Enumerable Logic from the weather service.


static Random random = new Random(5);

/// 
/// Returns an array of WeatherForecast objects
/// representing the weather for the next Range days.
/// 
public WeatherForecast[] GetForecast(int Range)
{
	var rng = new Random();
	return Enumerable.Range(1, Range).Select(index => new WeatherForecast
	{
		Date = DateTime.Now.AddDays(index),
		TemperatureC = random.Next(-20, 55),
		Summary = Summaries[random.Next(Summaries.Length)]
	})
	.ToArray();
}

Next, create a folder under data called interfaces, then add an interface call IWeatherRepository. Add interface to weather repository with a function reference to the repository method just created.


public interface IWeatherRepository
{
	WeatherForecast[] GetForecast(int Range);
}

After the interface is created, go back and the repository class and inherit the interface. 


public class WeatherRepository : IWeatherRepository
{
	private static readonly string[] Summaries = new[]
	{
		"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
	};
	
	....


Go back to WeatherForecastService, and add a reference to the newly created interface. We will use dependency injection to inject the repository into the service so our dependency is loosely coupled. To do this add a readonly field to the service and a constructor that takes the interface as a parameter.


IWeatherRepository _weatherRepository;

public WeatherForecastService(IWeatherRepository weatherRepository)
{
	_weatherRepository = weatherRepository;
}

To call our repository, an update must be made to the GetForcast method. The method takes a DateTime so it must be checked that it is valid. Then the DateTime is converted to the range, or the number of days that are being fetching. Finally, call the repository.


public Task GetForecastAsync(DateTime startDate)
{
	int range = 5;
	DateTime now = DateTime.Now;

	if(startDate > now)
		throw new ArgumentException("startDate cannot be in the future", nameof(startDate));

	range = now.Subtract(startDate).Days;
	
	return Task.FromResult(_weatherRepository.GetForecast(range));
}

Last open the fetchdata.razor. At the top add a dropdown with 3, 7, 10 as options. default to 7. Then add an on change call blazor function that will be linked to the dropdown. Finally, update GetForecast function to convert days to DateTime and fetch the forecast. 


@page "/fetchdata"

Weather forecast

@using FiveMinuteProject.Data
@inject WeatherForecastService ForecastService

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from a service.</p>

<select bind="selectedTimeFrame">
    @foreach (var timeFrame in timeFrames)
    {
        <option value="@timeFrame">@timeFrame</option>
    }
<select>
<button onclick="OnSelectedTimeFrameChanged">Get Forecast</button>
@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
     
	<table class="table">
		<thead>
			<tr>
    			<th>Date</th>
    			<th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
        @foreach (var forecast in forecasts)
        { 
        	<tr>
            	<td>@forecast.Date.ToShortDateString()</td>
                <td>@forecast.TemperatureC</td>
                <td>@forecast.TemperatureF</td>
                <td>@forecast.Summary</td>
            </tr>
         }
         </tbody>
    </table>
}

@code {
    private int[] timeFrames = new int[] { 3, 7, 10 };
    private int selectedTimeFrame;

    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        selectedTimeFrame = 7;
        forecasts = await GetForecast();
    }

    private async Task OnSelectedTimeFrameChanged()
    {
        forecasts = await GetForecast();
    }

    private async Task GetForecast()
    {
        Console.WriteLine(selectedTimeFrame.ToString());
        return await ForecastService.GetForecastAsync(DateTime.Now.AddDays((selectedTimeFrame) * -1));
    }
}


Why are we converting from numbers in the dropdown to date, then back to numbers? That seems redundant. Answer is easy, we need something to test in our unit test!

Why are we using interfaces here? An interface creates a contract to the dependency for service. What this allows us to do is extract the dependency and create fake data to test our Weather Service. A weather repository is probably reliant on a 3rd party service such as weather.gov to get it's information. A unit test is meant to test our current classes code logic, and having a dependency on a weather API can make testing tricky. The interface allows us to create Mocks and Stubs so our unit test doesn't need to rely on the API service anymore and allows control over what is returned making the unit test more efficient and accurate.

Setup Unit Test

With the web project is created, the test project needs to be setup. To create a NUnit test project run the following terminal command


dotnet new nunit -o FiveMinuteProject.Tests

Next, the test needs to reference the web project. To do this, run this command in the terminal


dotnet add FiveMinuteProject.Tests/FiveMinuteProject.Tests.csproj reference FiveMinuteProject/FiveMinuteProject.csproj


Finally run the nuget command to install "Moq"

The Unit Test project is setup for testing

Create a Stub

For our example, the WeatherService is going to be tested. The weather service has a dependency on the weather repository. In this case we will stub out a response to test the logic of our weather service. Moq can also be used for mocking your interface, in this example a mock is not needed.

to create a stub, first create a new class and call it WeatherRepositoryStubs. Then we will create a class called build. I like to use the builder design pattern for stubs that way all our stubs will have a build function and is called to create our stub. In this example we will just create a function called BuildStub and forego the builder pattern to keep the example quick.



public class WeatherRepositoryStubs
{
	Mock mockWeatherRepository = new Mock();
	//Create a stub for IWeatherRepository using Moq

	public IWeatherRepository BuildStub()
	{
		mockWeatherRepository.Setup(x => x.GetForecast(It.IsAny()))
			.Returns(new WeatherForecast[]
			{
				new WeatherForecast
				{
					Date = DateTime.Now,
					TemperatureC = 32,
					Summary = "Freezing"
				}
			});
		return mockWeatherRepository.Object;
	}

}

Once the stub is created, the tests can now be setup.

Create the Test

In the test project, a new class is created called WeatherForecastService_Tests.cs. In order to use NUnit our class needs to be set as a test fixture. To do this use the text decorator to add TestFixture. Next the Setup function is created. I like to use the setup function to initiate my service and inject my stubs. This keeps me from repeating the constructor call in each arrange section of the test. some may require the setup of the constructor done in the arrange section so different stubs can be passed to produce errors such as a null reference exception.

Finally we can create tests. Each function will have a decorator [Test] so NUnit knows which functions are tests. I have setup 5 tests to represent different scenarios for the service.

    1. startDate is in the future
    2. startDate is in the past
    3. startDate is today
    4. startDate is DateTime.MinValue
    5. startDate is DateTime.MaxValue



using System;
using NUnit.Framework;
using System.Threading.Tasks;
using FiveMinuteProject;
using FiveMinuteProject.Data;
using FiveMinuteProject.Data.Interfaces;


namespace FiveMinuteProject.Tests;

[TestFixture]
public class WeatherForecastService_Tests
{
    //create test cases for the following: WeatherForecast[] GetForecastAsync(DateTime startDate, int range)
    //1. startDate is in the future
    //2. startDate is in the past
    //3. startDate is today
    //4. startDate is DateTime.MinValue
    //5. startDate is DateTime.MaxValue

    WeatherForecastService weatherForecastService;

    [SetUp]
    public void Setup()
    {
        WeatherRepositoryStubs weatherRepositoryStubs = new WeatherRepositoryStubs();
        IWeatherRepository weatherRepository = weatherRepositoryStubs.BuildStub();
        weatherForecastService = new WeatherForecastService(weatherRepository);
    }

    
    
    [Test]
    public void GetForecastAsync_StartDateIsInTheFuture_ThrowsArgumentException()
    {
        //Arrange
        
        DateTime startDate = DateTime.Now.AddDays(1);
        
        //Act
        //Assert
        Assert.Throws(() => weatherForecastService.GetForecastAsync(startDate));
    }

    [Test]
    public async Task GetForecastAsync_StartDateIsInThePast_ReturnsWeatherForecastArray()
    {
        //Arrange
        DateTime startDate = DateTime.Now.AddDays(-1);
        
        //Act
        var result = await weatherForecastService.GetForecastAsync(startDate);
        
        //Assert
        Assert.IsInstanceOf(result);
    }

    [Test]
    public async Task GetForecastAsync_StartDateIsToday_ReturnsWeatherForecastArray()
    {
        //Arrange
        DateTime startDate = DateTime.Now;
        
        //Act
        var result = await weatherForecastService.GetForecastAsync(startDate);
        
        //Assert
        Assert.IsInstanceOf(result);
    }


    [Test]
    public async Task GetForecastAsync_StartDateIsDateTimeMinValue_ReturnsWeatherForecastArray()
    {
        //Arrange
        DateTime startDate = DateTime.MinValue;
        
        //Act
        var result = await weatherForecastService.GetForecastAsync(startDate);
        
        //Assert
        Assert.IsInstanceOf(result);
    }

    [Test]
    public void GetForecastAsync_StartDateIsDateTimeMaxValue_ReturnsWeatherForecastArray()
    {
        //Arrange
        DateTime startDate = DateTime.MaxValue;
        
        //Act
        //Assert
        Assert.Throws(() => weatherForecastService.GetForecastAsync(startDate));
    }

}

Run the Test

Finally to run our tests, all that needs to be done is call the command "dotnet test". If you are using Visual Studio, the tests will show in the test explorer.

That's it, unit tests using Moq And NUnit created in 5 minutes. You can find the project on my Github repo here: https://github.com/fiveminutecoder/blogs/tree/master/FiveMinuteUnitTests


C#, C sharp, machine learning, ML.NET, dotnet core, dotnet, O365, Office 365, developer, development, Azure, Supervised Learning, Unsupervised Learning, NLP, Natural Language Programming, Microsoft, SharePoint, Teams, custom software development, sharepoint specialist, chat GPT,artificial intelligence, AI

Cookie Alert

This blog was created and hosted using Google's platform Blogspot (blogger.com). In accordance to privacy policy and GDPR please note the following: Third party vendors, including Google, use cookies to serve ads based on a user's prior visits to your website or other websites. Google's use of advertising cookies enables it and its partners to serve ads to your users based on their visit to your sites and/or other sites on the Internet. Users may opt out of personalized advertising by visiting Ads Settings. (Alternatively, you can opt out of a third-party vendor's use of cookies for personalized advertising by visiting www.aboutads.info.) Google analytics is also used, for more details please refer to Google Analytics privacy policy here: Google Analytics Privacy Policy Any information collected or given during sign up or sign is through Google's blogger platform and is stored by Google. The only Information collected outside of Google's platform is consent that the site uses cookies.