On Test Driven Development

08 Aug 2018

Why

One of the things I see most on job descriptions, brought up in interviews, and thrown around in general programming discussion is Test Driven Development. For the most part, everyone understands how the methology works and how to implement it. But, I see a lot of developers miss out on the benefits when it comes to fixing a bug.

How

First, it needs to be said that every single bug should have at least one test in the pull request that fixes the bug. That’s a given, I hope, but we can expand on this.

When I bug rears its ugly head, the first thing that should happen is adding at least one test to prove the bug’s existence. Reproduce the bug with tests.

describe CustomAddingClass do
  let(:amount) { 1 }
  
  describe '#add_one' do
    subject { CustomAddingClass.new(amount).add_one }
    it { is_expected.to be(2) }
  end
end

This is your test, and it passes. Look at our class:

class CustomAddingClass
  def initialize(amount = 1)
    @amount = amount
  end

  def add_one
    @amount + 1
  end
end

We expect CustomAddingClass#add_one to increment the constructor arg by one, but it isn’t working. The test passes, but reports are an error message: TypeError (no implicit conversion of Integer into String).

What gives? You could guess all day, and with this particular error message, you’d probably figure it out really quickly. This is just a simple example. Imagine the system is far more complex.

We need a test to reproduce the bug we’re seeing in production.

describe '#add_one' do
  context 'when amount is a string' do
    let(:amount) { '1' }
    subject { CustomAddingClass.new(amount).add_one }
    
    it { is_expected_to raise_error(TypeError) }
  end
end

Ah, ha! We’ve found our bug. Somehow the frontend is allowing a string to be passed to CustomAddingClass#new.

So, we update our class to ensure that we’re always dealing with an integer:

class CustomAddingClass
  def initialize(amount = 1)
-    @amount = amount
+    @amount = amount.to_i
  end

  def add_one
    @amount + 1
  end
end

And now our test fails because we no longer have an error when a string is passed into the constructor. So we update the new test to fit the new behavior.

describe '#add_one' do
  context 'when amount is a string' do
    let(:amount) { '1' }
    subject { CustomAddingClass.new(amount).add_one }
    
-    it { is_expected_to raise_error(TypeError) }
+    it { is_expected_to equal(2) }
  end
end

All we’ve done here is replace is the assertion that the class will raise an error with the new assertion that we expect it to return the originally expected value.

And all tests pass again.

Huzzah! Now you can rest easy as you’ve proven how the bug manifests and the fix.

TDD implemented for bug fixing.