DEV Community

Matheus Richard
Matheus Richard

Posted on • Originally published at matheusrich.com on

A Simple Way To Get Started With TDD

TDD is awesome , but also confusing (and even scary) for those who never practiced it. But it doesn’t have to! We’re gonna learn how to get started with it by fixing bugs (so we can kill two birds with one stone).

» If 3 min is too long, here’s the TL;DL

OBS: the code snippets are below are in ruby, but the core concept applies to any language.

Oh, no! You got a bug in production!

Your monitoring tool is screaming at you the famous Billion Dollar Mistake: NoMethodError: undefined method 'split' for nil:NilClass

You look at the logs a see where this exception raised:

NoMethodError: undefined method `empty?' for nil:NilClass
your_app/models/user.rb:2:in `invalid?'
Enter fullscreen mode Exit fullscreen mode

Something went wrong with that User model. Let’s check its code:

class User < BaseModel
  def invalid?
    @name.empty? # <-- The error occurred here
  end

  def save!
    raise 'Cannot save invalid user!' if invalid?

    :saved_on_db # I'm simplifying the record creation on DB here
  end

  # ...
end
Enter fullscreen mode Exit fullscreen mode

So, before saving the user, it had a nil name. We have a validation, but it only checks if @name is not empty. Well, this is probably the bug: it shouldn’t allow nil values too.

Before we run to fix this bug, let’s confirm our thesis by writing a test that reproduces the error. This is very important! If our test fails with the same error that the monitoring tool had, we’re on the right track.

class UserTest < Test::Unit::TestCase
  def test_that_user_with_name_is_valid
    user = User.new(name: 'Matz')

    refute user.invalid?
  end

  def test_that_user_with_empty_name_is_invalid
    user = User.new(name: '')

    assert user.invalid?
  end

  def test_that_user_with_nil_name_is_invalid # <-- New test here
    user = User.new(name: nil)

    assert user.invalid?
  end

  def test_that_can_save_user_with_name
    user = User.new(name: 'Matz')

    assert_equal :saved_on_db, user.save!
  end

  def test_that_cannot_save_user_with_empty_name
    user = User.new(name: '')

    assert_raise_message('Cannot save invalid user!') do
      user.save!
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

We run it and… BOOM!

Error: test_that_user_with_nil_name_is_invalid(UserTest):
  NoMethodError: undefined method `empty?' for nil:NilClass
Enter fullscreen mode Exit fullscreen mode

So, we’re able to reproduce the error. Now we must fix the bug, and if we patch it correctly, this test will pass.

We should check if user’s name isn’t nil before checking it isn’t empty:

class User < BaseModel
  def invalid?
    @name.nil? || @name.empty? # <-- In Rails this could be written as `@name.blank?`
  end

  # ...
end
Enter fullscreen mode Exit fullscreen mode

We rerun our tests, and now we’re green!

Loaded suite /your_app/tests/user_test
Started
.....
Finished in 0.000720944 seconds.
------------------------------------------------------------------------------------
5 tests, 5 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed
Enter fullscreen mode Exit fullscreen mode

We TDDed, so…?

With this approach, we’ve not only fixed the bug but added a test confirming our fix (making sure the error doesn’t happen again). Now, just open a Merge Request for it (or push to master you’re feeling rebel enough).

TL;DL

Let’s review the steps:

  1. Identify the bug;
  2. Write a test that reproduces the error; (This is the critical step)
  3. Fix the bug;
  4. Watch the test pass.

I hope this helps you too see some TDD niceties! Happy TDDing!

Top comments (5)

Collapse
 
baweaver profile image
Brandon Weaver

You could also use the lonely operator:

@name&.nil?
Enter fullscreen mode Exit fullscreen mode
Collapse
 
matheusrich profile image
Matheus Richard

You mean

@name&.empty?
Enter fullscreen mode Exit fullscreen mode
Collapse
 
baweaver profile image
Brandon Weaver

....and this is a note to read twice, post once 😅

Collapse
 
lvlario0o0o profile image
Mario Lafleur

Should we write a test that pass the exception first, then we write a test that pass the correction?
Or maybe the way you do it make both of them at once?

Collapse
 
matheusrich profile image
Matheus Richard

I tend to write a test that reproduces the exact error that raised in production. Then I make the exception go away by fixing the bug.

So I think I do both at once. But if you can split those phases safely, should be equally valid.