Reader small image

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

Product typeBook
Published inSep 2022
PublisherPackt
ISBN-139781803230191
Edition1st Edition
Right arrow
Author (1)
Adam Tibi
Adam Tibi
author image
Adam Tibi

Adam Tibi is a London-based software consultant with over 22 years of experience in .NET, Python, the Microsoft stack, and Azure. He is experienced in mentoring teams, designing architecture, promoting agile and good software practices, and, of course, writing code. Adam has consulted for blue-chip firms including Shell, Lloyds Bank, Lloyd’s of London, Willis Towers Watson, and for a mix of start-ups. As a consultant who has a heterogeneous portfolio of clients, he has gained a solid understanding of the TDD intricacies, which he has transferred into this book.
Read more about Adam Tibi

Right arrow

The FIRSTHAND Guidelines of TDD

TDD is more than a test-first unit testing or a Red-Green-Refactor approach. TDD includes best practices and guidelines that steer the way you work with unit testing.

I wanted to make a memorable list, from my experience, of the most useful guidelines on unit testing and TDD. So, here are nine proven best practices that I’ve abbreviated as FIRSTHAND. FIRSTHAND stands for:

  • First
  • Intention
  • Readability
  • Single-Behavior
  • Thoroughness
  • High-Performance
  • Automation
  • No Interdependency
  • Deterministic

In this chapter, we will go through each of these nine guidelines and support them with relevant practical examples. By the end of the chapter, you should have a fair understanding of the ecosystem of TDD and its guidelines.

Technical requirements

The code for this chapter can be found in the following GitHub repository:

https://github.com/PacktPublishing/Pragmatic-Test-Driven-Development-in-C-Sharp-and-.NET/tree/main/ch06

The First guideline

Unit tests should be written first. This might seem odd or unintuitive at the beginning, but there are valid reasons for this choice.

Later means never

How many times have you heard, we’ll test it later? I have never seen a team finishing a project and releasing it to production and then allocating time to unit test their code.

Moreover, adding unit tests at the end will require code refactoring, which might break the product, and it is hard to justify to a non-technical person that a working system was broken because the team was adding unit tests. Actually, the statement we broke production because we were adding unit tests sounds ironic. Yes, you can refactor a working system while covered by other types of tests, such as Sintegration and acceptance tests, but it would be difficult to imagine that a team that didn’t have time to unit test previously had the time to build other tests that would fully cover the system.

Testing first ensures...

The Intention guideline

When your system grows, it drives more unit tests that will naturally cover system behavior and documentation. And with more tests comes greater responsibility: readability and maintenance.

The tests will grow in quantity to an extent where the team will not remember the reason for writing them. You will be looking at a failing test and scratching your head for clues about the intention of the test.

Your unit tests should be understood with the least possible time and effort; otherwise, they will be more of a liability than an asset. An agile software team should be prepared in advance for such test failure scenarios. Intention can be demonstrated by having a clear method signature and a well-structured method body.

Starting with the method signature, here are two popular conventions that should clarify the unit test’s intention.

Method_Condition_Expectation

I have been using this convention in naming the unit test methods across the book...

The Readability guideline

Is this method readable? Do you need to run it and start debugging to understand what it does? Does the Arrange section make your eyes bleed? This might be violating the readability principle.

Having the Intention guideline established is fabulous, but it is not enough. You will have at least 10x more lines of code in your unit tests compared to your production code. All this needs to be maintained and grow with the rest of your system.

Tidying up the unit test for readability follows the same practices as the production code. However, there are some scenarios that are more dominant in unit tests, which we are going to address here.

SUT constructor initialization

Initializing your SUT will require that you prepare all the dependencies and pass them to the SUT, something like this:

// Arrange
const double NEXT_T = 3.3;
const double DAY5_T = 7.7;
var today = new DateTime(2022, 1, 1);
var realWeatherTemps = new[] 
    {2, NEXT_T...

The Single-Behavior guideline

Every unit test should test one and only one behavior. Throughout this book, this concept has been enforced naturally by:

  • The naming of the unit test method’s signature, which reflects one condition with one expectation
  • A single AAA structure that enforced a single Act

Before digging further, I would like to define the word behavior.

What is behavior?

The definition of behavior varies in the industry, so it is important to set an accurate one for the context of this book. Each SUT is supposed to do something. A SUT does this thing by:

  • Communicating with dependencies: Communication can be by calling a method on a dependency or setting a field or a property – this is referred to as external behavior.
  • Returning a value to the outside world (the caller): This could be via an Exception or the return value (if a method is not a void or a Task method) – this is also referred to as external behavior.
  • ...

The Thoroughness guideline

When unit testing some naturally occurring questions are as follows:

  • How many tests are enough?
  • Do we have a test coverage metric?
  • Should we test third-party components?
  • What system components should we unit test and what should we leave?

The Thoroughness guideline attempts to set the answers to these questions.

Unit tests for dependency testing

When you encounter a dependency, whether this dependency is part of your system or a third-party dependency, you create a test double for it and isolate it in order to test your SUT.

In unit tests, you do not directly call a third-party dependency; otherwise, your code will be an integration test and with that, you lose all the benefits of unit tests. For example, in unit tests, you do not call this:

_someZipLibrary.Zip(fileSource, fileDestination);

For testing this, you create a test double for the .zip library to avoid calling the real thing.

This is an area that unit...

The High-Performance guideline

Your unit tests should not take, in today’s hardware, over 5 seconds to run, ideally no more than a couple of seconds after the tests are loaded. But why all this fuss? Can’t we just let them take whatever time is needed to run without sweating over it?

First, your unit tests will have to run many times throughout the day. TDD is about running a chunk of your unit tests or all of them with every change; therefore, you don’t want to spend your time waiting and lose valuable time that could be spent more productively.

Second, your unit tests need to provide fast feedback to your CI pipeline. You want your source control branches to be green all the time, so that other developers are pulling green code at any given time and, of course, it is ready to ship to production. This is even more important for larger teams.

So, how do you keep your unit tests performing as fast as possible? We will attempt to answer this question in...

The Automation guideline

When we say automation, we mean CI. CI has a dedicated chapter in this book, Chapter 11, Implementing Continuous Integration with GitHub Actions, so we won’t go into the details here.

This guideline is about realizing that the unit tests will run on other platforms than your local development machine. So, how do you make sure your unit tests are ready for automation?

CI automation from day 1

Agile teams dedicate the first sprint to setting up the environment, including the CI pipeline. This is usually called sprint or iteration zero. If CI is set up from day 1 to listen to source control, there is less chance it is omitted or a CI-incompatible test is introduced.

Implement CI from the start of the project.

Platform-independent

.NET is multi-platform and the trend nowadays is to use Linux servers to run CI pipelines. Also, the developer dev machine can be Windows, macOS, or Linux.

Make sure your code does not rely on any OS-specific...

The No Interdependency guideline

First, I would like to elevate this from a guideline to a principle. This principle ensures that the unit test does not alter a state permanently; or, in other words, executing a unit test should not persist data. Based on this principle, we have the following rules:

  • Test A shouldn’t affect test B.
  • It doesn’t matter whether test A runs before test B.
  • It doesn’t matter whether we run test A and test B in parallel.

If you think about it, a unit test is creating test doubles and doing its operations in memory, and as soon as it finishes execution, all the changes are lost, except the test report. Nothing is saved in the database, in a file, or anywhere, because these dependencies were all provided as test doubles.

Having this principle in place also ensures that the test runner, such as Test Explorer, can run the tests in parallel and use multi-threading if needed.

Ensuring this principle is a shared responsibility...

The Deterministic guideline

A unit test should have a deterministic behavior and should lead to the same result. This should be the case regardless of the following:

  • Time: This includes changes in time zone and testing at different times.
  • Environment: Such as the local machine or CI/CD server.

Let’s discuss some cases where we risk making non-deterministic unit tests.

Non-deterministic cases

There are cases that can lead to non-deterministic unit tests. Here are a few of them:

  • Having interdependent unit tests, such as a test that writes to a static field.
  • Loading a file with an absolute path as the file location on the development machine will not match that on the automation machine.
  • Accessing a resource that requires higher privileges. This can work, for example, when running VS as an admin but may fail when running from a CI pipeline.
  • Using randomization methods without treating them as dependencies.
  • Depending on the system...

Summary

FIRSTHAND accumulates valuable guidelines and best practices in the industry. I trust this chapter topped up the learnings of the previous chapters to help you understand TDD and its ecosystem. I also hope that it made these guidelines memorable as TDD comes up often in developer discussions and it is certainly likely to be an interview topic.

This chapter marks the end of this section, where we looked at dependency injection, unit testing, and TDD. This section was only an introduction to TDD, with scattered small and mid-size examples. If you’ve made it to this point, then hats off, you have covered the basics of TDD.

The next section will take all the basics and apply them to more lifelike scenarios. To make sure that you are ready for this application and to mimic a realistic application that uses TDD, our next chapter will be about domain-driven design (DDD) as you will be using the DDD concepts in later chapters.

lock icon
The rest of the chapter is locked
You have been reading a chapter from
Pragmatic Test-Driven Development in C# and .NET
Published in: Sep 2022Publisher: PacktISBN-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.
undefined
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

Author (1)

author image
Adam Tibi

Adam Tibi is a London-based software consultant with over 22 years of experience in .NET, Python, the Microsoft stack, and Azure. He is experienced in mentoring teams, designing architecture, promoting agile and good software practices, and, of course, writing code. Adam has consulted for blue-chip firms including Shell, Lloyds Bank, Lloyd’s of London, Willis Towers Watson, and for a mix of start-ups. As a consultant who has a heterogeneous portfolio of clients, he has gained a solid understanding of the TDD intricacies, which he has transferred into this book.
Read more about Adam Tibi