Before writing any code, it makes sense that we understand some terminologies and conventions to tune our brain on unit test-related keywords. So, we will briefly touch on system under test (SUT), red/green tests, and Arrange-Act-Assert (AAA). More details on these terminologies will follow in later chapters, but for now, we will cover the minimum to get a few tests running.
While we are learning about terminology and conventions, we will ease into our implementation. One thing that you might find new or unordinary is writing a unit test first, then writing production code later. This is one main aspect of TDD, and you will first experience it in this section.
SUT
We refer to the code that you usually write to build a product as production code. Typical object-oriented (OO) production code looks like this:
public class ClassName
{
public Type MethodName(…)
{
// Code that does something useful
}
// more code
}
When we test this code, the unit test will call MethodName
and assess the behavior of this method. When MethodName
is executed, it may call other parts of the class and may use/call other classes. The code executed by MethodName
is called SUT or code under test (CUT). However, the term SUT is used more often.
The SUT will have an entry point that will be executed by the unit tests. The entry point is usually the method that we are calling from the unit tests. The following screenshot should clarify the idea of a SUT and a SUT entry point:
Figure 1.14 – Unit tests operating on a SUT
In the previous screenshot, you can see multiple unit tests calling the same SUT entry point. A detailed discussion of SUT is available in Chapter 3, Getting Started with Unit Testing.
Testing class
A typical unit testing class uses the same names from the SUT, by convention. This is what a typical unit testing class looks like:
public class ClassNameTests
{
[Fact]
public void MethodName_Condition1_Expectation1()
{
// Unit Testing Code that will call MethodName
}
// Other tests…
[Fact]
public void MethodName_ConditionN_ExpectationN()
{
// Unit Testing Code that will call MethodName
}
…
}
Notice that the ClassName
and MethodName
methods in the two previous snippets are not a coincidence. We want them to be the same, again, by convention. To start forming our test class, we need to design the class name and the method name.
Class name
From the requirements, we will need a class that will contain all our division methods, so let’s simply call the class Division
; and if we were to create a unit test class to test the Division
class, our unit test name would be called DivisionTests
. Next, we will rename the UnitTest1
class DivisionTests
and rename the file as well so that it appears as DivisionTests.cs
.
Tip
You can set your text cursor anywhere within the class name in the source code (in the previous case, it was UnitTest1
) and hit Ctrl + R, R (hold Ctrl then press R quickly twice). Type the new name DivisionTests
and hit Enter. This will also rename the file if the Rename symbol’s file checkbox is ticked.
Method name
Luckily, the requirements are simple, so our method name will simply be Divide
. Divide
will be accepting two integer (int32
) arguments, per the requirements, and returns a decimal
value. We will go ahead and refactor our existing unit test from Test1
to Divide_Condition1_Expectation1
.
Note
Arithmetic terminology-naming reminder: If we have 10 / 5 = 2, then 10 is the dividend, 5 is the divisor, and 2 is the quotient.
Conditions and expectations
When we test, we are setting a condition and defining what we expect when this condition is met. We start with the core case, also known as the positive path or the happy path. We finish all the positive paths first before going to other cases. Our mission in unit tests boils down to determining the condition and its expectation and having a unit test for every combination.
To show the relationship between the method we are testing (the method in our SUT) and the associated condition and expectation, we will employ a well-used convention, as illustrated in the following code snippet:
[Fact]
public void MethodName_Condition_Expectation()
{
…
Here are random examples of unit test method names to familiarize you with the previous convention:
SaveUserDetails_MissingEmailAddress_EmailIsMissing
ValidateUserCredentials_HashedPasswordDoesntMatch_False
GetUserById_IdDoesntExist_UserNotFoundException
We will see more examples while designing our unit tests.
The core requirement is dividing two integers. The straightforward and simplest implementation is dividing two divisible integers and getting back a whole number. Our condition is divisible integers and we expect a whole number. Now, we should update the signature of our unit test to Divide_DivisibleIntegers_WholeNumber
and write the body of the test method, as follows:
[Fact]
public void Divide_DivisibleIntegers_WholeNumber()
{
int dividend = 10;
int divisor = 5;
decimal expectedQuotient = 2;
decimal actualQuotient = Division.Divide(dividend,
divisor);
Assert.Equal(expectedQuotient, actualQuotient);
}
This code doesn’t compile as the Division
class doesn’t exist at this stage, and we know that already as we have a squiggly line under Division
. This is one of the rare occasions where not being able to compile due to a missing class is good. This indicates that our test has failed, which is also good!
While it does look silly that the test has failed because the code doesn’t compile as the Division
SUT class is missing, this means that there is no SUT code yet. In Chapter 5, Test-Driven Development Explained, we will understand the reason behind considering the no-compilation case.
Assert
is a class from the xUnit library. The Equal
static method has many overloads, one of which we are using here:
public static void Equal<T>(T expected, T actual)
When run, this method will flag to the xUnit framework if what we expect and what we’ve actually got are equal. When we run this test, if the result of this assertion is true
, then the test has passed.
Red/green
Failure is what we were looking for. In later chapters, we will discuss why. For now, it is sufficient to know that we need to start with a failed build (compilation) or failed test (failed assertion), then change that to a passed one. The fail/pass is also known as the red/green refactor technique, which mimics the idea of bad/good and stop/go.
We need to add the Division
class and the Divide
method and write the minimal code to make the test pass. Create a new file called Division.cs
in the Uqs.Arithmetic
project, like this:
namespace Uqs.Arithmetic;
public class Division
{
public static decimal Divide(int dividend, int divisor)
{
decimal quotient = dividend / divisor;
return quotient;
}
}
Tip
You can create a class by placing the text cursor anywhere within the class name (in the previous case, it was Division
) and hitting Ctrl + . (hold down the Ctrl key and then press .). Select Generate new type…, then from the Project dropdown, select Uqs.Arithmetic
, and then hit OK. Then, to generate the method, place your text cursor on Divide
and hit Ctrl + ., select Generate method ‘Division.Divide’, and then you get the method shell in Division
ready for your code.
It is important to remember that dividing two integers in C# will return an integer. I have seen senior developers fail to remember this, which led to bad consequences. In the code that we implemented, we have only covered the integers division that will yield a whole quotient. This should satisfy our test.
We are now ready to run our test with Test Explorer, so hit Ctrl + R, A, which will build your projects, then run all the tests (currently one test). You’ll notice that Test Explorer indicates green, and there is a green bullet with a tick mark between the test name and the Fact
attribute. When clicked, it will show you some testing-related options, as illustrated in the following screenshot:
Figure 1.15 – VS unit testing balloon
For the sake of completion, the full concept name is red/green/refactor, but we won’t be explaining the refactor bit here and will leave this for Chapter 5, Test-Driven Development Explained.
The AAA pattern
Unit testing practitioners noticed that test code format falls into a certain structure pattern. First, we declare some variables and do some preparations. This stage is called Arrange.
The second stage is when we invoke the SUT. In the previous test, it was the line on which we called the Divide
method. This stage is called Act.
The third stage is where we validate our assumption—this is where we have the Assert
class being used. This stage is, not surprisingly, called Assert.
Developers usually divide each unit test with comments to denote these three stages, so if we apply this to our previous unit test, the method would look like this:
[Fact]
public void Divide_DivisibleIntegers_WholeNumber()
{
// Arrange
int dividend = 10;
int divisor = 5;
decimal expectedQuotient = 2;
// Act
decimal actualQuotient = Division.Divide(dividend,
divisor);
// Assert
Assert.Equal(expectedQuotient, actualQuotient);
}
You can learn more about the AAA pattern in Chapter 3, Getting Started with Unit Testing.
More tests
We haven’t finished implementing the requirements. We need to add them iteratively, by adding a new test, checking that it fails, implementing it, then making it pass, and then repeating it!
We are going to add a few more tests in the next sections to cover all the requirements, and we are also going to add some other tests to increase the quality.
Dividing two indivisible numbers
We need to cover a case where two numbers are not divisible, so we add another unit testing method under the first one, like so:
[Fact]
public void Divide_IndivisibleIntegers_DecimalNumber()
{
// Arrange
int dividend = 10;
int divisor = 4;
decimal expectedQuotient = 2.5m;
…
}
This unit test method is similar to the previous one, but the name of the method has changed to reflect the new condition and expectation. Also, the numbers have changed to fit the new condition and expectation.
Run the test by employing any of the following methods:
- Clicking the blue bullet that appears below
Fact
, then clicking Run
- Opening Test | Test Explorer, selecting the new
test name
code, and clicking the Run button
- Pressing Ctrl + R, A, which will run all tests
You will notice that the test will fail—this is good! We have not implemented the division that will yield a decimal yet. We can go ahead and do it now, as follows:
decimal quotient = (decimal)dividend / divisor;
Note
Dividing two integers in C# will return an integer, but dividing a decimal by an integer returns a decimal, therefore you almost always have to cast the dividend or the divisor—or both—to a decimal.
Run the test again, and this time it should pass.
Division-by-zero test
Yes—bad things happen when you divide by zero. Let’s check whether our code can handle this, as follows:
[Fact]
public void Divide_ZeroDivisor_DivideByZeroException()
{
// Arrange
int dividend = 10;
int divisor = 0;
// Act
Exception e = Record.Exception(() =>
Division.Divide(dividend, divisor));
// Assert
Assert.IsType<DivideByZeroException>(e);
}
The Record
class is another member of the xUnit framework. The Exception
method records whether the SUT has raised any Exception
object and returns null
if there is none. This is the method’s signature:
public static Exception Exception(Func<object> testCode)
IsType
is a method that compares the class type between the angle brackets to the class type of the object that we passed as an argument, as illustrated in the following code snippet:
public static T IsType<T>(object @object)
When you run this test, it will pass! My first impression would be one of suspicion. The problem is that when it passes without writing explicit code, we don’t know yet whether this is a true or a coincidental pass—a false positive. There are many ways to validate whether this pass is incidental; the quickest way—for now—is to debug the code of Divide_ZeroDivisor_DivideByZeroException
.
Click the Test Bullet, and then click the Debug link, as illustrated in the following screenshot:
Figure 1.16 – The Debug option in the unit testing balloon
You will hit the exception directly, as illustrated in the following screenshot:
Figure 1.17 – Exception dialog
You’ll notice that the exception is happening at the right place at the division line, so this is what we actually wanted. While this method violated our initial attempt of red/green, having a pass immediately is still a genuine case that you would encounter in day-to-day coding.
Testing extremes
The story did not mention testing the extremes, but as a developer, you know that most software bugs come from edge cases.
You want to build more confidence in your existing code, and you want to make sure that it can handle extremes well, as you’d expect it to.
The extreme values of an int
data type can be obtained by these two constant fields of int
:
int.MaxValue
=
int.MinValue
=
What we need to do is to test the following cases (note that we will only test for 12 decimal digits):
int.MaxValue / int.MinValue = -0.999999999534
(-int.MaxValue) / int.MinValue = 0.999999999534
int.MinValue / int.MaxValue = -1.000000000466
int.MinValue / (-int.MaxValue) = 1.000000000466
So, we will need four unit tests to cover each case. However, there is a trick available in most unit test frameworks, including xUnit. We don’t have to write four unit tests—we can do this instead:
[Theory]
[InlineData( int.MaxValue, int.MinValue, -0.999999999534)]
[InlineData(-int.MaxValue, int.MinValue, 0.999999999534)]
[InlineData( int.MinValue, int.MaxValue, -1.000000000466)]
[InlineData( int.MinValue, -int.MaxValue, 1.000000000466)]
public void Divide_ExtremeInput_CorrectCalculation(
int dividend, int divisor, decimal expectedQuotient)
{
// Arrange
// Act
decimal actualQuotient = Division.Divide(dividend,
divisor);
// Assert
Assert.Equal(expectedQuotient, actualQuotient, 12);
}
Notice that now we have Theory
rather than Fact
. This is xUnit’s way of declaring that the unit test method is parametrized. Also, notice that we have four InlineData
attributes; as you will have already figured out, each one of them corresponds to a test case.
Our unit test method and the InlineData
attributes have three parameters. When running the unit tests, each parameter will map to the corresponding unit test method’s parameter in the same order. The following screenshot shows how each parameter in the InlineData
attribute corresponds to a parameter in the Divide_ExtremeInput_CorrectCalculation
method:
Figure 1.18 – InlineData parameters are mapped to the decorated method parameters
For assertion, we are using an overload of the Equal
method that supports decimal precision, as illustrated in the following code snippet:
static void Equal(decimal expected, decimal actual,
int precision)
Run the tests, and you’ll notice that Test Explorer treats the four attributes as separate tests, as depicted in the following screenshot:
Figure 1.19 – VS Test Explorer showing grouped tests
Even More Tests
For brevity, and given that this chapter is a limited introduction, we didn’t explore all possible testing scenarios—take, for example, int.MaxValue/int.MaxValue
, int.MinValue/int.MinValue
, 0/number
, and 0/0
.
The limits of the required tests are going to be discussed in later chapters, along with their pros and cons.
Writing tests before writing the code is not to every developer’s taste and might look unintuitive at the beginning, but you have a complete book to make you decide for yourself. In Chapter 5, Test-Driven Development Explained, you will dig deeper into implementation and best practices.