Have you had started a project trying to do TDD “the right way”, writing “unit tests” for every class and method, trying to hit that famous 100% coverage….

… And then few months later, rewriting many of them just for a little change or refactor!

Did you thought that TDD was just not for you?…

Well, don’t worry… I am not sure why but this is a common stage while learning to use TDD.

The good thing is that this is not the only way to do it!

You can design your code in a little different way… Instead of focusing on your model and each class you build to solve the problem, put your eyes (and your tests) on your API (but not the HTTP API).

Work to build an API that can expose the expected use cases in a simple and clear way… (As a tip try to start with a function).

Then, in the process of TDD after you have been writing tests against that API, and the implemented behavior starts to grow, you will need more classes, modules and methods, and maybe you will be tempted to open a new file and write some tests for those new classes and methods… But don’t do it, yet!

Try to tests as much as you can through the API that you want to expose. This is what will let you refactor your code when you think is needed.

Let’s try to explain it with an example…

Imagine you are working on a blog application in the function to add a post. Maybe in your first tests (the happy path) you could have something like this…

describe "with all attributes..." do
  attr_reader :params

  before do
    @params = { "title" => "P1", "body" => "Super!" }
  end

  it "returns success" do
    status = add_post(params, store)
    expect(status).to be_success
  end

  it "creates a record" do
    expect(store).to receive(:create).with(title: "P1", body: "Super!")
    add_post(params, store)
  end
end

And to implement the expected behavior, maybe something like…

module Blog
  def self.add_post(attrs, store)
    store.create({ title: attrs["title"], body: attrs["body"] })
    SuccessStatus
  end

  class SuccessStatus
    def self.success?
      true
    end
  end
end

Do you think we need to write test for your SuccessStatus class?

Now you need to implement a new requirement…

To add a post the title will be required, and the system should return an error if the title is not present.

To test that behavior, maybe you could write something like this…

describe "without title..." do
  it "does not returns success" do
    status = add_post({}, store)
    expect(status).not_to be_success
  end

  it "does not creates the record" do
    expect(store).not_to receive(:create)
    add_post({}, store)
  end

  it "returns a blank error" do
    status = add_post({}, store)
    expect(status.form.errors[:title]).to eq ["can't be blank"]
  end
end

And a possible implementation could be something like…

module Blog
  def self.add_post(attrs, store)
    form = Form.new(attrs)

    if form.valid?
      store.create(form.to_h)
      SuccessStatus
    else
      ErrorStatus.new(form)
    end
  end

  class Form
    include ActiveModel::Model
    attr_accessor :title, :body
    validates_presence_of :title

    def to_h
      { title: title, body: body }
    end
  end

  class ErrorStatus
    attr_reader :form

    def initialize(form)
      @form = form
    end

    def success?
      false
    end
  end

  class SuccessStatus
    def self.success?
      true
    end
  end
end

Do you think you should write new test cases to test the ErrorStatus and Form objects directly?

Let’s think a little…

You have already tested the complete required behavior through the use case API and the tests are fast!

You can add tests for the ErrorStatus and Form objects, but…

  • Are those the right names for those objects?
  • How stable would be the implemented behavior of those objects?
  • Is this the right internal design?
  • If the behavior of the use case is already tested, how much value or confidence would those new tests add?

If you ask me… I would not write those new tests cases to test them directly.