TDD is not about testing

It’s actually all in the name. Test Driven Development.

the single biggest benefit from using TDD is that it becomes really easy to execute a specific line of code

In a nutshell, the single biggest benefit from using TDD is that it becomes really easy to execute a specific line of code - the exact line of code that you're currently working on. .. and at the end of the day, isn't that just the easiest way to make sure that code you're writing is working?

Let's consider the above login sequence which I just found on the web:

A pretty standard login flow

From the above, it's we might have something like the following (pseudocode):


class LoginPage extends Controller:
  GET login()
  POST login()
  _validate_post_login()
  _respond_with_error(code, message)
  _redirect(location)

class LoginService:
  user_exists(username)
  is_password_correct(username, password)
  
class User extends Model:
  filter()
  get()
  

Now, if we were to start by writing the is_password_correct method. To test this in the traditional sense .. there's quite a bit that needs to be in place first:

  1. The controller will need to be setup already (in most cases that is probably already the case
  2. We need our post() method in place along with some validation
  3. We need an existing user
  4. Then, finally, in order to run the piece of code that we are writing we need to manually login from the frontend with an existing user, we need to do this with at least the correct and incorrect passwords

or we could just do this (in pseudo python):

class LoginServiceTestCase:

  def setUp(self):
    User.objects.create_user("joesoap", "password")
    self.login_service = LoginService()
  
  def test_password_is_correct_with_valid_password(self):
    """A user should be able to login with valid credentials"""
    result = self.login_service.is_password_correct("joesoap", "password")
    assert result == True, \
      "Expect login attempt to be successful. Expected True. Got: {}" . format (result)
      
  def test_password_is_correct_fails_with_invalid_password(self):
    """login must fail if the user submits the incorrect credentials"""
    invalid_credentials = [
      ("notjoesoap", "password"),
      ("joesoap", "notjoespassword")
    ]
    for username, password in invalid_credentials:
      result = self.login_service.is_password_correct(username, password)
      assert result == False, \
        'Expect login to fail. Expected False, got: {}' . format (result)
        
  ...
    

Now that didn’t take me long (though I’m sure many will argue that it would have been quicker to just submit).

Maybe, but the benefits of this will compound the longer I do it. I write the test only once, but will benefit from it being there forever. Should I need to edit this code again in the future, I don’t need to re-write these tests. I just change the code and update the tests. On the other hand: There is no compounding benefit from hitting refresh.

Unit tests are a slow and methodical way to write good code that works. For a great write-up on this, read: The Frenzied Panic of Rushing by Uncle Bob

Scenarios where unit testing really shines

Having written my code in the TDD style for a while now, there are certain scenarios where I don't actually know how one could write code like this without unit tests:

  • When there is a dependency on a third party which you do not control.
    For example: if you integrate with a 3rd party API, there might be certain scenarios that you literally cannot re-create in a test environment.
  • When a certain peice of code requires a lot of interaction from a frontend in order to trigger the code you're writing. SPAs (Single Page Applications) are a great example. Often, the button that fires the function you are working on is hidden 3 or four clicks into the UI. To manaully execute this code requires you to manually click through all these steps everytime. Or, worse: you make tweaks to your production code to make it easy to test the code you're working on (for example: you might make this page the homepage so you don't have to click around to get to the page - I assume I don't need to explain to you why this is a bad idea!)
  • When there are a great many pemutations of scenarios to which code needs to react (a permissions system with a number of different roles is a great example of this)
  • When you are writing a class or module for a library .. this one is rather self-explanetary.
  • When you need to take advantage of tools provided to you by the framework in which you're developing (e.g.: Angular1 provided the MockHttpRequest object which made it easy to test certain conditions before and after an HttpRequest had fired).

Tests as documentation

Not only am I executing the code, I’m also documenting how it is expected to run (because sometimes the desired function of our code can be a little opaque)!

So when I (or someone else) come back in a year's time and make some changes, I’ll know immedidately if I’ve changed the code to function in a way that it was not supposed to. I can then look at the failing tests and decide if this change is by design (and therefore update the tests to reflect this), or is a mistake (and therefore fix my code to make the test pass).

  • Without tests, I can never be certain if changes I make are not changing the system in a way that it was not designed to work.

  • without tests, I may not even know how to execute a certain peice of code

Tests encourage good design

When we write unit tests, it is much easier to test small functions rather than to test a large blob of code. As such, it becomes natural to break your code down into sensible chunks.

Furthermore, because you are using your code on an API level as you write it: you pay more attention to how your code reads.

This leads to creating more fluent APIs: You tend to get better at breaking down the function of your methods as well as which arguments they should take and what they should return - because these things make your functions easier to test.

This is also a subtle reason why it is often difficult to write your tests after you've written you code. For example, it's not uncommon to find the above example written something like this:

class LoginPage:

  @post
  def do_login(request):
    if request.data.username && request.data.password:
      try: 
        user = User.objects.get(username)
        if User.objects.get(username, password)
          return 200, success
        else:
          return 401, incorrect password
      except User.DoesNotExist:
        return 401, user does not exist
    else: return 500, invalid inputs

This isn’t great design, but it’s something I notice almost everytime I look at code that was not written at the same time as the tests.

You see, it's easy to test code like this manually - because you have no precision when you test manually: you really are just using a big hammer and running all the code to test the piece that you want to validate.

If written with a TDDesque approach, this code would probably look a little more like this:

class LoginPage

  @post
  def do_login(request):
    is_valid = self.validate_login(request)
    if not is_valid():
      return return 500, invalid inputs
      
    login_service = LoginService(username, password)
    user_exists = login_service.user_exists()
    if not user_exists:
      return 401, invalid user
    
    valid_user = login_service.is_valid_password()
    if valid_user:
      return 200, success
    else: 
      return 401, invalid password
    
  

I find this pattern seems to become natural when writing using TDD. Your high level code is simply responsible of delegating the work based on the input. The work is then done by spcecialist classes. This is easy to test because the classes can easily be tested in isolation, and the delegation logic in your top level class (you main or your controller) is easily tested with mock objects.

When writing code at the same time as tests, you're likely to do the following:

  • Break code into smaller bits.
  • Delegate to Services / Helpers / whatever you want to call them because:
    • Methods on helpers are typically easy to test
    • Helpers can easily be mocked so that you can test the various branches of your main/controller code.

In my opinion, these leads to writing code that is easier to understand and more reusable

Why do people not write unit tests

The vast majority of people who do not write unit tests, "do so because there is not enough time".

Barrier to entry:

There is - in many languages - quite a barrier to entry in getting started with unit testing.

Typically it involves learning a unit-testing framework that couples with your language/framework of choice. This is often compounded by the fact that often you're learning the framework itself at the same time. During this period unit testing is certainly not a time-efficient chore. However, if you push past this learning curve there are great advantages to be had. Unfortunately, many people do not.

Writing tests after you write the code

Another thing which I think greatly hurts the adoption of writing tests is when people write the tests at the end of their coding cycle. Essentially what they're doing is: write code -> manually check that it works -> write tests to automatically check that it works. In this scenario you are duplicating work. If you write your tests after you have written your code you experience none of the speed-up that you will get from writing your tests as you code in order to test the code you're writing. In addition, as I mention above, oftentimes when not writing tests at the same time as code, you write code that is hard to test (and thus it takes longer to write the test code).

Conclusion:

The most common reason people give for not writing tests is that there is not enough time. I do not agree with this argument. Though I agree that there is an initial barrier, and that the TDD approach does require the adoption of a different mindset. I believe that if you write your tests as you write you code, you will be able to write better code faster (or at least at the same speed!)