Search icon
Arrow left icon
All Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletters
Free Learning
Arrow right icon
Pragmatic Test-Driven Development in C# and .NET

You're reading from  Pragmatic Test-Driven Development in C# and .NET

Product type Book
Published in Sep 2022
Publisher Packt
ISBN-13 9781803230191
Pages 372 pages
Edition 1st Edition
Languages
Author (1):
Adam Tibi Adam Tibi
Profile icon Adam Tibi

Table of Contents (21) Chapters

Preface 1. Part 1: Getting Started and the Basics of TDD
2. Chapter 1: Writing Your First TDD Implementation 3. Chapter 2: Understanding Dependency Injection by Example 4. Chapter 3: Getting Started with Unit Testing 5. Chapter 4: Real Unit Testing with Test Doubles 6. Chapter 5: Test-Driven Development Explained 7. Chapter 6: The FIRSTHAND Guidelines of TDD 8. Part 2: Building an Application with TDD
9. Chapter 7: A Pragmatic View of Domain-Driven Design 10. Chapter 8: Designing an Appointment Booking App 11. Chapter 9: Building an Appointment Booking App with Entity Framework and Relational DB 12. Chapter 10: Building an App with Repositories and Document DB 13. Part 3: Applying TDD to Your Projects
14. Chapter 11: Implementing Continuous Integration with GitHub Actions 15. Chapter 12: Dealing with Brownfield Projects 16. Chapter 13: The Intricacies of Rolling Out TDD 17. Index 18. Other Books You May Enjoy Appendix 1: Commonly Used Libraries with Unit Tests 1. Appendix 2: Advanced Mocking Scenarios

Implementing requirements with TDD

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

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

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

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

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

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

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.

You have been reading a chapter from
Pragmatic Test-Driven Development in C# and .NET
Published in: Sep 2022 Publisher: Packt ISBN-13: 9781803230191
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $15.99/month. Cancel anytime}