I will use Ruby in this example, and assume that the feature will be built inside rails. Also, this concentrates more on the process and leaves the actual implementation out

One of the reasons why I keep using TDD is that it helps my thinking process when I write code. It makes me think the feature before implenting it making the implementation part really straightforward.

Feature spec

Let’s say I need to create a feature where user can send a message to other user. I’d start with following feature-spec

feature "User can send a message" do
  scenario "User logs in and sends a message to another user" do
    user = FactoryGirl.create :user
    receiver = FactoryGirl.create :user

    sign_user_in user, "password"

    click_link "Messages"
    click_link "Send a new message"

    fill_in "To", with: receiver.name
    fill_in "Message", with: "Hello world!"
    click_button "Send"

    expect(page).to have_text("Message was sent successfully")

    expect(receiver.messages.map(&:receiver)).to eq([receiver.name])
  end
end

This is the first draft of the flow I’d like that user has to go through for sending a message. Most probably it will need changes in the future, but the main point (for me) is try to make sending message as simple as possible. Also note that we’re testing only that user gets A) notification about successful sending and B) there was one message that were sent to correct receiver.

At this point I start to run specs and write production code until I need to create a new class (e.g. controller).

Controller

Next spec file will be for MessagesController (as we don’t have controller named like that yet).

RSpec.describe MessagesController, type: :controller do
  describe "#create" do
    context "receiver exists" do
      it "message is sent with username"
      it "message is sent with userid"
      it "message is sent with email"
    end
    context "receiver does not exist" do
      it "message is not sent"
    end
  end
end

So before I write any line of tests, I try to write down the “features” of the class I’m testing. As this is a new feature and we’re missing the Message object, we’ll need to hop on to write spec file for that

Note: In some cases, there’s no point for creating specs for controller (for example if action is just listing items, that can be tested in feature spec just fine). But in this example, we’re not sure how we’re going to implement the feature, so I’d follow the style and create the spec file (we can always refactor specs and remove them if they become obsolete).

Model

On the model, I follow same process as with controller (and other classes), drafting out what the class should do.

RSpec.describe Message, type: :model do
  describe "#send_message" do
    context "receiver exists" do
      it "message is sent with username"
      it "message is sent with userid"
      it "message is sent with email"
    end
    context "receiver does not exist" do
      it "message is not sent"
    end
  end
end

As we can see, it’s pretty similar with our controller spec file. So after we’ve implemented Message, we can refactor the controller.

Refactor controller spec

So we have now tests to test that message can be sent with username, userid and email, we don’t need to test the same things in controller. Instead, we should just test that message is either sent or not.

RSpec.describe MessagesController, type: :controller do
  describe "#create" do
    context "receiver exist" do
      it "message is sent"
    end
    context "receiver does not exist" do
      it "message is not sent"
    end
  end
end

Now we’re testing two paths, happy and unhappy. And to be honest, we probably should get rid off the whole controller spec by adding unhappy-feature spec. But it’s ultimately up to you and how you like to test things.

As feature specs are “expensive”, so we shouldn’t test everything there. But testing one happy and one unhappy path from end to end may serve the purpose.

Conclusion

This is how I design my code with TDD. I’ve found it really helpful to keep focus on the thing I’m working on. I either think and design the functionality or I follow my decisions and write the code “without-thinking”.