A medior dev's take on TDD

A medior dev's take on TDD

How, Why and When I TDD

There was a recent Twitter storm around the subject of TDD. I have wanted to write about my journey with TDD for a while. This Twitter is as good an excuse as any. And probably, this article will appear when the next 'controversy' is there, so... I won't be jumping on any bandwagon.

I will first digress about how I TDD, then why I TDD and I will finish by exposing when I TDD

This article aims to provide a glimpse into my personal experience, it might differ from yours, and that is "quite OK"; I am sharing what works for me, not a universal recipe.

How do I TDD

I have chosen not to review all the steps of TDD as I believe that if you are here, you have heard of Red-Green-Refactor and want some perspective, or you will find more use in other resources such as:

As to how I manage the logistics on my end, I will first describe the big picture, a quick example of how I TDD and, finally, a couple of tips and tricks if you use RSpec:

Big picture

I will stick to the 'by the book' approach of never writing any code before I see my test fail and then write the minimum amount of code to 'change' the error.

I usually will have two layers of tests at any given point:

One layer that will serve as a feature/integration test, this one will have longer 'red' periods, and I will make it run every five to ten cycles. This feature test will be a test that looks at browser interactions and any such things and focuses on a happy path.

The other (and most interesting) layer is the layer of the module/class I am developing at the moment. I will focus really on the minimal test to make things move forward (think steps like 'should return an array' way before testing each member of the array)

Finally, when debugging, I will create a test that will reproduce the bug programmatically before any attempt at solving it.

Quick Example

Let's imagine I want to add a `Book'show page. That page will have to show the book title and subtitle. Here are the steps that I would take:

# specs/features/books_pages_spec.rb
RSpec.describe 'When I visit a book page', :focus do 
  let(:book) { Book.create(title: 'Blah', subtitle: 'bleh')
  it 'can be visited' do 
     visit book_path(book)
  end
end

At this point, I run rspec -t focus and receive an error: The route does not exist. So I:

# routes.rb
  ressources :books, only: [:show]

# books_controller.rb
class BooksController < ApplicationController
  def show
  end
end

# show.html.erb
<h1> Dumb Title </h1>

I will rerun the tests, and they will be green. At this point, I believe my next step will be to show the title on the page:

# specs/features/books_pages_spec.rb
RSpec.describe 'When I visit a book page', :focus do 
  let(:book) { Book.create(title: 'Blah', subtitle: 'bleh')

  it 'displays the book title' do 
     visit book_path(book)
     expect(page).to have_content('Blah')
  end
end

Do note that I have removed the 'previous' test, as it was redundant with this latest test. After rerunning the tests, my new error is that the 'Blah' title is not displayed.

Here, it is clear to me that I need to make sure that the Book model has the correct attributes, so I will leave the books_pages_spec.rb alone for a while and:

# specs/models/book_spec.rb

RSpec.describe Book, :focus do
  it 'is expected to have a title' do 
    book = Book.create(title: 'title', subtitle: 'subtitle')
    expect(book).to have_attributes( {title: 'title', subtitle: 'subtitle'})
  end 
end

Rerun tests, fail, create the model: rails g model book title subtitle rerun tests, it works!

I can focus back on my 'feature' test now.

# books_controller.rb
class BooksController < ApplicationController
  def show
    @book = Book.find(params[:id])
  end
end

# show.html.erb
<h1> <%= @book.title %> </h1>

I run the tests, and they are now green. Just one step missing:

# specs/features/books_pages_spec.rb
RSpec.describe 'When I visit a book page', :focus do 
  let(:book) { Book.create(title: 'Blah', subtitle: 'bleh')

  it 'displays the book title and subtitle' do 
     visit book_path(book)
     expect(page).to have_content('Blah')
      expect(page).to have_content('bleh')
  end
end

Test, fail, implement:

# show.html.erb
<h1> <%= @book.title %> </h1>
<h2> <%= @book.subtitle %> </h2>

Test and... We win!!

I could not show it in this simplistic example, but after every 'green' I will take time to assess and refactor (I did do it for my tests, though).

Tips and Tricks (Aka Useful commands)

Run tests that you tagged with my_tag => rspec -t my_tag

Run only 'non-feature' tests => rspec -t ~type:feature

Run only 'feature' tests => rspec -t type:feature

Run tests until the first failure => rspec --fail-fast

Run only 'previously failed tests => rspec --only-failures

All these options can be combined And will help speed up the TDD process when battling a test suite that is a bit on the slow side. You can also take a look at my article on parallelizing tests if you wish to speed up tests

Why do I TDD

Several reasons push me to develop using TDD. The first (and, in my eyes, the most important) is that it makes me more productive. It might sound counterintuitive as I am doing 'more' work by thinking of the next 'minimal' part. But it forces me to focus on what is right in front of me instead of trying to take stabs at what I believe will be the 'final' state.

Another clear benefit of TDD is that the tests are not an afterthought; I not only write them every time, but I also want to write them. That makes it, so I write my tests less 'lazily' and will usually cover more edge cases.

Another benefit of TDD is that it gives me a clear place to consider if my code makes sense. And it allows me to refactor away the doubts and decisions I took when I had incomplete information, with an all-important peace of mind.

Last but not least, it makes my code more testable. I know this one is cliché, but it is true; since you know what you are going to test, you architecture your code in a way that makes it testable (and, almost by design, very modular).

When do I TDD

Honestly? 80% of the time. If I don't, it's either that I am working with legacy code that is hard to test, and I do what I can, or I am pairing with someone that does not enjoy/like TDD.