How to write Unit tests in dotnet using c#

How to write unit tests in Dotnet using C#

In this article, I will show you different ways of writing unit tests in .Net using MSTest unit testing framework.
We write unit tests to confirm whether the logic that we have implemented in our code is working as expected or not. The actual result that is returned after the execution of our code is compared with the expected result and if both match then the test case passes else it fails.
Unit tests are also useful when we are implementing new logic and want to make sure that it does not break the current logic already implemented.
For this article, I am creating an asp.net webApi project along with a test project. The github repository can be found at the end of the article.

Add Business Logic Layer in WebApi

First, create an ASP.NET web API project and add a MSTest test project along with it in the root folder. We will use the default ToDoApi template of asp.net and name the test project as Tests. After that add a business logic layer(BLL) folder and add 2 files IInterestManager.cs(Interface) and InterestManager.cs. The contents of these 2 files are as follows
1) IInterestManager.cs


namespace TodoApi.BLL
{
	public interface IInterestManager
	{
		double CalculateInterest(int principal, double interestRate, int period);
	}
}

2) InterestManager.cs


using TodoApi.DAL;
using TodoApi.Models;

namespace TodoApi.BLL
{
	public class InterestManager : IInterestManager
	{

		public IInterestRepository _InterestRepository { get; set; }
		public InterestManager(IInterestRepository interestRepository)
		{
			_InterestRepository = interestRepository;
		}
		public double CalculateInterest(int principal, double interestRate, int period)
		{
			return (principal * Math.Pow((1 + interestRate / 100), period)) - principal;
		}

		public double CalculateInterest()
		{
			var interestData = _InterestRepository.GetInterestData(Guid.NewGuid());
			return (interestData.principal * Math.Pow((1 + interestData.interestRate / 100), interestData.period)) - interestData.principal;
		}
	}
}

And the project structure looks like follows:

Add Data Access Layer in WebApi

Add data access layer to our Web API project which fetches data to calculate interest. We add a folder called DAL to our project and 2 files to it IInterestRepository.cs and InterestRepository.cs. The content of the 2 files are as follows
1) IInterestRepository.cs


using TodoApi.Models;

namespace TodoApi.DAL
{
	public interface IInterestRepository
	{
		Interest GetInterestData(Guid UserId);
	}
}

2) InterestRepository.cs


using TodoApi.Models;

namespace TodoApi.DAL
{
	public class InterestRepository : IInterestRepository
	{
		public Interest GetInterestData(Guid UserId)
		{
			//In real life scenario we will get data from database but for demo purpose we will hardcode it for now

			return new Interest
			{
				principal = 20000,
				interestRate = 15,
				period = 4
			};

		}
	}
}

The project structure looks like this

Add Test Cases

Now create a InterestManagerTests.cs file in the Tests project where we will add our test cases.

Mocking framework for unit tests(Moq)

We use mocking framework in our unit tests to mock data as we cannot connect to database and create new data each time we run our unit tests. Mock data means that we create dummy data according to the scenario we want to test.
Moq is one of the most widely used mocking framework in dotnet. When we say we mock data, It basically means that when we are assigning a value to a method to return when it is called inside the unit test. Like this
moqManager.Setup(m => m.GetInterestData(It.IsAny<Guid>())).Returns(interestData);
So we basically mock data using interface methods which have been implemented and used in the method that we are testing in our test case. So that when that method is called in our code, it will automatically return data that we set above using Setup() method.

Different types of unit tests

There are 3 ways you can write unit tests using MSTest

1) simple unit test with no data being passed

In this kind of unit tests, no data is passed from outside. We create the dummy data in the method itself and then test it. In our case, we will be creating dummy interest data for testing. The test case looks like this

[TestMethod]
	public void check_calculateInterest_returns_correct_amount()
	{
		// Given I have Interest Data
		var interestData = new Interest
		{
			principal = 20000,
			interestRate = 12,
			period = 2
		};
		var expectedInterestAmount = 5088.00;

		// And I have mocked my interest repository to return interest data
		var moqManager = new Mock<IInterestRepository>();
		moqManager.Setup(m => m.GetInterestData(It.IsAny<Guid>())).Returns(interestData);

		// And I have instantiated my manager
		var interestManager = new InterestManager(moqManager.Object);

		// When I call the service
		var result = interestManager.CalculateInterest();

		// Then I expect the resulting interest amount to match the expected interest amount
		Assert.AreEqual(Math.Round(expectedInterestAmount, 2), Math.Round(result, 2));
	}

In the above method, first we have created dummy data and then mocked our interest repository to return that dummy data. So when we call our manager, the repository method inside it automatically returns the mocked data. The interestManager method executes our logic and returns data that we assert or check against our expected data. Both should match otherwise our logic is wrong.

2) Passing data to unit test using DataRow

In this type of unit tests, we pass data using DataRow attribute and which are passed using parameters inside the method. They are used like below

[DataTestMethod]
	[DataRow(15000, 10, 2, 3150.00)]
	[DataRow(25000, 20, 5, 37208.00)]
	public void check_calculateInterest_returns_correct_amount(int principal, double interestRate, int period, double expectedInterestAmount)
	{
		// Given I have Interest Data
		var interestData = new Interest
		{
			principal = principal,
			interestRate = interestRate,
			period = period
		};

		// And I have mocked my interest repository to return interest data
		var moqManager = new Mock<IInterestRepository>();
		moqManager.Setup(m => m.GetInterestData(It.IsAny<Guid>())).Returns(interestData);

		// And I have instantiated my manager
		var interestManager = new InterestManager(moqManager.Object);

		// When I call the service
		var result = interestManager.CalculateInterest();

		// Then I expect the resulting interest amount to match the expected interest amount
		Assert.AreEqual(Math.Round(expectedInterestAmount, 2), Math.Round(result, 2));
	}

In the above method, we are using the data similar to previous method but the dummy data is being passed using DataRow attribute. The testing is done similar to previous method.

3) Passing data to unit test using DynamicData

In this type of test, we pass dynamic data which uses another method to fetch data. Like below

[DataTestMethod]
	[DynamicData(nameof(GetCalculateInterestData), DynamicDataSourceType.Method)]
	public void check_calculateInterest_returns_correct_amount2(Interest interestData, double expectedInterestAmount)
	{
		// I have mocked my interest repository to return interest data
		var moqManager = new Mock<IInterestRepository>();
		moqManager.Setup(m => m.GetInterestData(It.IsAny<Guid>())).Returns(interestData);

		// And I have instantiated my manager
		var interestManager = new InterestManager(moqManager.Object);

		// When I call the service
		var result = interestManager.CalculateInterest();

		// Then I expect the resulting interest amount to match the expected interest amount
		Assert.AreEqual(Math.Round(expectedInterestAmount, 2), Math.Round(result, 2));
	}

	private static IEnumerable<object[]> GetCalculateInterestData()
	{
		var interestData = new Interest
		{
			principal = 20000,
			interestRate = 12,
			period = 2
		};
		var expectedInterestAmount = 5088.00;
		yield return new object[] { interestData, expectedInterestAmount };

		interestData = new Interest
		{
			principal = 15000,
			interestRate = 10,
			period = 2
		};
		expectedInterestAmount = 3150.00;
		yield return new object[] { interestData, expectedInterestAmount };

		interestData = new Interest
		{
			principal = 25000,
			interestRate = 20,
			period = 5
		};
		expectedInterestAmount = 37208.00;
		yield return new object[] { interestData, expectedInterestAmount };

	}

In the above method, we are passing dynamic data using the method GetCalculateInterestData which passes 3 different types of dummy data using the yield keyword. Whenever we use yield keyword, the code executed once will not be executed again. Apart from this, the rest of the logic of the test case remains the same.

The final InterestManagerTests.cs class

using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using TodoApi.BLL;
using TodoApi.DAL;
using TodoApi.Models;

namespace Tests;

[TestClass]
public class InterestManagerTests
{
	[TestMethod]
	public void check_calculateInterest_returns_correct_amount()
	{
		// Given I have Interest Data
		var interestData = new Interest
		{
			principal = 20000,
			interestRate = 12,
			period = 2
		};
		var expectedInterestAmount = 5088.00;

		// And I have mocked my interest repository to return interest data
		var moqManager = new Mock<IInterestRepository>();
		moqManager.Setup(m => m.GetInterestData(It.IsAny<Guid>())).Returns(interestData);

		// And I have instantiated my manager
		var interestManager = new InterestManager(moqManager.Object);

		// When I call the service
		var result = interestManager.CalculateInterest();

		// Then I expect the resulting interest amount to match the expected interest amount
		Assert.AreEqual(Math.Round(expectedInterestAmount, 2), Math.Round(result, 2));
	}

	[DataTestMethod]
	[DataRow(15000, 10, 2, 3150.00)]
	[DataRow(25000, 20, 5, 37208.00)]
	public void check_calculateInterest_returns_correct_amount(int principal, double interestRate, int period, double expectedInterestAmount)
	{
		// Given I have Interest Data
		var interestData = new Interest
		{
			principal = principal,
			interestRate = interestRate,
			period = period
		};

		// And I have mocked my interest repository to return interest data
		var moqManager = new Mock<IInterestRepository>();
		moqManager.Setup(m => m.GetInterestData(It.IsAny<Guid>())).Returns(interestData);

		// And I have instantiated my manager
		var interestManager = new InterestManager(moqManager.Object);

		// When I call the service
		var result = interestManager.CalculateInterest();

		// Then I expect the resulting interest amount to match the expected interest amount
		Assert.AreEqual(Math.Round(expectedInterestAmount, 2), Math.Round(result, 2));
	}

	[DataTestMethod]
	[DynamicData(nameof(GetCalculateInterestData), DynamicDataSourceType.Method)]
	public void check_calculateInterest_returns_correct_amount2(Interest interestData, double expectedInterestAmount)
	{
		// I have mocked my interest repository to return interest data
		var moqManager = new Mock<IInterestRepository>();
		moqManager.Setup(m => m.GetInterestData(It.IsAny<Guid>())).Returns(interestData);

		// And I have instantiated my manager
		var interestManager = new InterestManager(moqManager.Object);

		// When I call the service
		var result = interestManager.CalculateInterest();

		// Then I expect the resulting interest amount to match the expected interest amount
		Assert.AreEqual(Math.Round(expectedInterestAmount, 2), Math.Round(result, 2));
	}

	private static IEnumerable<object[]> GetCalculateInterestData()
	{
		var interestData = new Interest
		{
			principal = 20000,
			interestRate = 12,
			period = 2
		};
		var expectedInterestAmount = 5088.00;
		yield return new object[] { interestData, expectedInterestAmount };

		interestData = new Interest
		{
			principal = 15000,
			interestRate = 10,
			period = 2
		};
		expectedInterestAmount = 3150.00;
		yield return new object[] { interestData, expectedInterestAmount };

		interestData = new Interest
		{
			principal = 25000,
			interestRate = 20,
			period = 5
		};
		expectedInterestAmount = 37208.00;
		yield return new object[] { interestData, expectedInterestAmount };

	}
}

The code for this tutorial is available on Github at https://github.com/juzer-hakimji/Test-Proj

Thank you for reading this article. Have a good day.

Share :

Leave a Comment

Your email address will not be published. Required fields are marked *