What is Unit Testing?
Unit test is an automated test written and run by the developer to ensure that a piece of an application behaves as intended. Usually, unit tests cover the smallest logic unit of an application, and it helps developers to be sure that the application’s pieces work correctly.
A single unit test has a proper structure – well-known as triple A. Arrange, Act, Assert. It helps to identify what happens inside the test case.
Let’s take a look at the test case where we have triple-A separation:
public void WhenCallPlusWithTwoOperands_ThenItSumsValues() { // Arrange var a = 1; var b = 2; var expected = 3; // Act var sum = a + b; // Assert Assert.AreEqual(sum, expected); }
This test case verifies that operator +
correctly sums two operands a
and b
. To keep the assertion part as simple as possible, we should define the expected value in the Arrange part.
The test case in our example verifies the result. But also, we can be interested in verifying a component interaction. We’ll have a look at how to test it later.
Test Method Naming Convention
The most important part of the development and unit testing is a proper naming convention. The name of the unit test should reflect what we are trying to test and the conditions for the test case.
Let’s take a look at one of the commonly used naming conventions:
[GivenX]_When[Y]_Then[Z]
It starts with an optional “Given” part that gives some context to the test, after which we describe some action that needs to be done in the “When” part. Finally, we describe what we expect to happen for a particular test case in the “Then” part of the method name.
Test Project and Files Naming Convention
It’s common good practice to keep separate test projects for each business project inside the solution. So if our solution has a business project named Project.Business
than test’s project name should be Project.Business.UnitTests
.
We use a proper suffix to show the intention of tests. For functional tests, the name would be Project.Business.FunctionalTests
.
We should keep the same structure that we have in our business project. Each class we cover with tests should have a test class in the test project named accordingly with Test
suffix.
NUnit Project Setup
Let’s define the simplest business logic class that we are going to test in this article. We are going to create a NUnitProject
project with a single Calculator
class that has just one method, Divide
. It’s enough for our purposes:
public class Calculator { public double Divide(int a, int b) { return a / b; } }
The next step would be to add a test project to the solution. We should rely on the naming convention and name it NUnitProject.UnitTests
. It’s a simple class library. Also, we are going to add the reference from the main project to the testing project.
Now it’s time to add the required NuGet packages.
Let’s run the command to install the NUnit package in the Test
project:
To be able to integrate with visual studio properly, we should also add the NUnit adapter package and Microsoft test SDK:
dotnet add package NUnit3TestAdapter dotnet add package Microsoft.NET.Test.Sdk
First Test Case
Now when we have the test project ready, we can start covering the Calculator
class with unit tests.
Let’s define a simple test case to verify that the Divide
method works properly:
[Test] public void WhenDivideTwoNumbers_ThenReturnDivisionOfTwoNumbers() { // Arrange var a = 300; var b = 100; var expected = 3; var calculator = new Calculator(); // Act var actual = calculator.Divide(a, b); // Assert Assert.AreEqual(expected, actual); }
To mark a method as a test case, we need to add a Test
attribute. It tells the NUnit
framework that it should run this method.
We can run all tests in a solution using Visual Studio with a few simple steps:
TopMenu -> Tests -> Run All Tests
, with a shortcut Ctrl R + T
, or from the console – navigate to the test project directory and run the command:
It’s a good approach to run our test methods with different arguments, without copy-pasting our test methods. We can define test method parameters by using the [TestCase]
attribute.
The [TestCase]
attribute allows us to provide params for the test method:
[TestCase(300,100, 3)] [TestCase(400, 200, 2)] public void WhenDivideTwoNumbers_ThenReturnDivisionOfTwoNumbers(int a, int b, double expected) { // Arrange var calculator = new Calculator(); // Act var actual = calculator.Divide(a, b); // Assert Assert.AreEqual(expected, actual); }
The[TestCase]
attribute allows only constant values for params, but we can find ourselves in a situation where we need to create a value on the fly.
Providing a Source With the TestCaseSource Attribute
We can solve this by using the [TestCaseSource]
attribute. It accepts the name of the static method or property, which provides values for the test method:
[TestCaseSource(nameof(SourceProvider))] public void WhenDivideTwoNumbers_ThenReturnDivisionOfTwoNumbersProvidedBySource(int a, int b, double expected) { // Arrange var calculator = new Calculator(); // Act var actual = calculator.Divide(a, b); // Assert Assert.AreEqual(expected, actual); } public static IEnumerable<int[]> SourceProvider() { yield return new int[] { 300, 100, 3 }; yield return new int[] { 400, 200, 2 }; }
Now, we face the problem of creating a calculator instance inside each test method. NUnit
provides a convenient way to execute a piece of code before all test methods or before each test method run.
With the [Setup]
attribute, we can execute a method before each test case run, and we can create a Calculator
class instance inside this method:
private Calculator _calculator; [SetUp] public void Setup() { _calculator = new Calculator(); }
We have another attribute that runs just once before all test cases run. It’s named OneTimeSetup
.
It depends on what kind of resources we are going to set up before test case execution. Sometimes we have a strict restriction to use a single instance of an object between all test cases and using the OneTimeSetup
method to create such an object is the best option.
Also, we have replacements for the Setup
attributes that we can use to execute code after the test case is finished. The attributes as TearDown
and, accordingly, OneTimeTearDown
give us such ability.
Unit Test Metadata
In a huge project, we can end up with a very large number of unit tests. In that case, we can use additional metadata attributes to add information to our unit tests.
Later we can run tests relying on specific values provided by metadata attributes.
Pumarehistro
Thank you for your sharing. I am worried that I lack creative ideas. It is your article that makes me full of hope. Thank you. But, I have a question, can you help me?
binance
Thanks for sharing. I read many of your blog posts, cool, your blog is very good.