TDD as a design tool
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”.