21

I wanted to teach myself to use the TDD approach and I had a project I had been wanting to work on for a while. It wasn't a large project so I thought it would be a good candidate for TDD. However, I feel like something has gone awry. Let me give an example:

At a high level my project is an add-in for Microsoft OneNote that will allow me to track and manage Projects more easily. Now, I also wanted to keep the business logic for this as decoupled from OneNote as possible in-case I decided to build my own custom storage and back end some day.

First I started with a basic plain words acceptance test to outline what I wanted my first feature to do. It looks something like this (dumbing it down for brevity):

  1. User clicks create project
  2. User types in title of project
  3. Verify that the project is created correctly

Skipping over the UI stuff and some intermediary planning I come to my first unit test:

[TestMethod]
public void CreateProject_BasicParameters_ProjectIsValid()
{
    var testController = new Controller();
    Project newProject = testController(A.Dummy<String>());
    Assert.IsNotNull(newProject);
}

So far so good. Red, green, refactor, etc. Alright now it needs actually save stuff. Cutting out some steps here I wind up with this.

[TestMethod]
public void CreateProject_BasicParameters_ProjectMatchesExpected()
{
    var fakeDataStore = A.Fake<IDataStore>();
    var testController = new Controller(fakeDataStore);
    String expectedTitle = fixture.Create<String>("Title");
    Project newProject = testController(expectedTitle);

    Assert.AreEqual(expectedTitle, newProject.Title);
}

I'm still feeling good at this point. I don't have a concrete data store yet, but I created the interface how I anticipated it would look.

I'm going to skip a few steps here because this post is getting long enough, but I followed similar processes and eventually I get to this test for my data store:

[TestMethod]
public void SaveNewProject_BasicParameters_RequestsNewPage()
{
    /* snip init code */
    testDataStore.SaveNewProject(A.Dummy<IProject>());
    A.CallTo(() => oneNoteInterop.SavePage()).MustHaveHappened();
}

This was good until I tried to implement it:

public String SaveNewProject(IProject project)
{
    Page projectPage = oneNoteInterop.CreatePage(...);
}

And THERE is the problem right where the "..." is. I realize now at THIS point that CreatePage requires a section ID. I didn't realize this back when I was thinking at the controller level because I was only concerned with testing the bits relevant to the controller. However, all the way down here I now realize I have to ask the user for a location to store the project. Now I have to add a location ID to the datastore, then add one to the project, then add one to the controller, and add it to ALL of the tests that are already written for all of those things. It has become tedious very quickly and I can't help but feel like I would have caught this quicker if I sketched out the design ahead of time rather than letting it be designed during the TDD process.

Can someone please explain to me if I've done something wrong in this process? Is there anyway this kind of refactoring can be avoided? Or is this common? If it is common are there any ways of making it more painless?

Thanks all!

2
  • You'd get some very insightful comments if you'd post this topic at this discussion forum: groups.google.com/forum/#!forum/… which is specifically for TDD topics. Oct 2, 2013 at 17:54
  • 1
    If you need to add something to all your tests it sounds like your tests are poorly written. You should refactor your tests and consider using a sensible fixture. Oct 3, 2013 at 10:30

3 Answers 3

19

While TDD is (rightly) touted as a way to design and grow your software, it's still a good idea to think about the design and architecture beforehand. IMO, "sketching out the design ahead of time" is fair game. Often this will be at a higher level than the design decisions you will be led to through TDD, however.

It's also true that when things change, you will usually have to update tests. There's no way to eliminate this completely, but there are some things you can do to make your tests less brittle and minimize the pain.

  1. As much as possible, keep implementation details out of your tests. This means only test through public methods, and where possible favor state-based over interaction-based verification. In other words, if you test the result of something rather than the steps to get there, your tests should be less fragile.

  2. Minimize duplication in your test code, just like you would in production code. This post is a good reference. In your example, it sounds like it was painful to add the ID property to your constructor because you invoked the constructor directly in several different tests. Instead, try extracting the creation of the object to a method or initializing it once for each test in a test initialize method.

5
  • I've read the merits of state-based vs interaction-based and understand it most of the time. However, I don't see how it's possible in every case without exposing properties EXPLICITLY for the test. Take my example above. I'm not sure how to check that the datastore was actually called without using an assertion for "MustHaveBeenCalled". As for point 2, you're absolutely correct. I did wind up doing that after all the edits, but I was just wanted to make sure my approach was generally consistent with accepted TDD practices. Thanks!
    – Landon
    Oct 1, 2013 at 6:25
  • @Landon There are cases where interaction testing is more appropriate. For example, verifying that a call was made to a database or web service. Basically, whenever you need to isolate your test, especially from an external service.
    – jhewlett
    Oct 1, 2013 at 6:47
  • @Landon I´m a "convinced classicist", so I´m not very experienced with interation-based testing... But you don´t need to make an assertion for "MustHaveBeenCalled". If you are testing an insertion, you can use a query to see if it was inserted. PS: I use stubs due to performance considerations when testing everything but the database layer.
    – Hbas
    Oct 2, 2013 at 1:54
  • @jhewlett That's the conclusion I've arrived at as well. Thanks!
    – Landon
    Oct 2, 2013 at 19:26
  • @Hbas There's no database to query. I agree that would be the most straight forward way to go if I had one, but I'm adding this to a OneNote notebook. The best I can do instead is to add a Get method to my interop helper class to try to pull the page. I COULD write the test to do that, but I felt like I'd be testing two things at once: Did I save this? and Does my helper class correctly retrieve pages? Although, I guess at some point your tests may have to rely on other code that's tested elsewhere. Thanks!
    – Landon
    Oct 2, 2013 at 19:30
10

...I can't help but feel like I would have caught this quicker if I sketched out the design ahead of time rather than letting it be designed during the TDD proces...

Maybe, maybe not

On the one hand, TDD worked just fine, giving you automated tests as you built up functionality, and immediately breaking when you had to change the interface.

On the other hand, perhaps if you had started with the high-level feature (SaveProject) instead of a lower-level feature (CreateProject), you would have noticed missing parameters sooner.

Then again, maybe you wouldn't have. It's an unrepeatable experiment.

But if you're looking for a lesson for next time: start at the top. And think about the design as much as you want first.

0
0

https://frontendmasters.com/courses/angularjs-and-code-testability/ From about 2:22:00 to the end (about 1 hour). Sorry that the video isn't free, but I haven't found a free one that explains it so well.

One of the best presentations of writing testable code is in this lesson. It's a AngularJS class, but the testing part is all around java code, primarily because what he is talking about has nothing to do with the language, and everything to do with writing good testable code in the first place.

The magic is in writing testable code, rather than writing code tests. It's not about writing code that pretends to be a user.

He also spends some time writing the spec in the form of test assertions.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.