What’s the nature of TDD?

This is a big one. I’ll try not to apologize for its philosophical nature, because I know I can’t help it. That’s how I learn. So I’ll just say that if you’re into looking at things philosophically, I’d very much appreciate your perspective on this question.

What’s test-driven development really about?

I want to understand TDD. I am not asking whether I should test first, or how small the tests should be. I’ve read and conversed a lot about red, green, refactor and I’m learning to do it, day by day. I am as elated with a clear red test as I am with a well-earned green one. (Refactoring is harder to get, but that’s for another story.) I’m also not asking whether TDD produces nicer code, or whether it’s ok to only have unit tests holding up some of the code. (My dentist says “You don’t have to floss all your teeth. Just the ones you want to keep.) I don’t need to be reminded that code with tests embedded is way less scary to change.

Oh, and I don’t need to be convinced that it’s a good idea. I already love it.

So what am I asking, then?

I’m asking what we are doing when we test drive design. And I’ll divulge that I’m asking partly because I’ve been talking with @kaleidic about TDD (and BDD) as it might apply to functional programming, and realize that my curiosity is really about understanding benefits we gain from TDD, and how we gain them. Unit testing might look different applied to function-level programming, and if that’s the case, different how? And what can we change without losing the heart of what testing provides?

As far as I can tell, a unit test doesn’t prove that a given algorithm is “right”. For any given test, we can produce a pass without actually solving the problem in front of us. So the unit test doesn’t cause us to write “good” code. That has to happen inside us, doesn’t it?

Here’s an illustration, in case that wasn’t clear. I can decide I could really use the square of some number. I could write a test that asks if I get four when I send two. And I could get four because I have a method that multiplies the number it’s given by two. There’s nothing inherent in the test that creates the right code for the job. Yes, more tests might help, but when the solution appears, it won’t be because the tests produced it or made it inevitable. So far, the only way I know for the solution to appear is if somebody thinks it up.

Do unit tests offer a statement of the problem? Of the solution?

I’ve been working on this one for a while. I think I’ve concluded that they don’t do either of those things, but they do something intimately connected. They offer a picture of the solved-problem.

Walking on a balance beam, I look at the end of the beam, and let life emerge a way for me to get there. I’m thinking about what the solved problem looks like (me at the end of the beam, having not fallen off) rather than looking intently at the problem (avoiding falling) or the solution (which my body is better at creating than my thinker is).

When I’m solving a math problem (or other kinds, actually) I like to write out what I know and where I want to get with it. Given: Sue’s balloons. Sam’s balloons. Joe’s balloons. Result: the color of Joe’s balloons. That helps me think about the things in between—which are also givens of a sort, except that I haven’t noticed them yet. Once I notice the right ones, they’ll lead me down the path. So writing a statement of the solved-problem helps me begin to see how to get there.

My impression (so far) is that one thing I get from TDD is a statement of the solved-problem. Since unit tests are little, the steps between are easier to get hold of, but the test is still like looking at the end of a balance beam, and letting what needs to happen happen.

Do unit tests provide criticism?

Popper taught us that we can’t support theories by testing them. We can only poke at them in an attempt to break them. If we give it a good go, and the theory holds up, then we stick with it until it breaks, often in the face of a better theory that has come along. We don’t get certainty, but we do get strengthened confidence in our theories (or ideas).

So I imagine that unit tests also provide criticism of our code. We want to cause our tests to fail (when & where our code isn’t working). That is, given that our code is incomplete or broken, we want tests to tell us that, so we can distrust the code in question, and find new code that we trust (because it doesn’t fail tests).

This would mean that a good test is one that will break most easily if the code is bogus (or nonexistent), and will not break if the code is just fine, thankyouverymuch.

So here are the bottom line questions about TDD

  • Have I missed some of the purposes that TDD serves? Which ones?
  • How can I use TDD with skill, rather than just following some rote instructions, and hoping for the best?
  • Given that TDD offers us teh awesum, how to extend that to functional/denotative/function-level programming? Can we? Do we want to?

10 thoughts on “What’s the nature of TDD?

  1. Sean McMillan

    My view of TDD is pretty simple. I don’t claim universalness — Others get other things from it — but this is what I think.

    I want to write beautiful code. I rely on my sense of aesthetics to help me make the code I write beautiful. But I don’t know what is the most beautiful form up front, so I refactor. Refactoring lets me move the code into a more beautiful form as I build it. To refactor effectively, I need a strong automated test suite. TDD is the simplest and most effective way to build that test suite.

    Reply
    1. Angela Post author

      @sean Thanks for the comment. A lot of what I’m wanting to learn is what the “juice” is that different people get from TDD.

      Reply
  2. coreyhaines

    Great post, Angela. I’m enjoying your thoughts, as you begin your journey.

    Just one comment right now.
    You ask
    “How can I use TDD with skill, rather than just following some rote instructions, and hoping for the best?”

    My answer would be ‘patience.’ Patience in following the instructions, as you build up the deeper understanding through concrete experience.

    Just like learning Object-Oriented design and the differences with Functional Programming design, understanding the subtleties of TDD takes years.

    But, this isn’t a suggestion to stop asking these questions, though. Keep asking them, keep thinking about them. They cause the rest of us to stop and think, as well.

    Also…. study the 4 rules of simple design. :)

    Reply
    1. Angela Post author

      Thanks, Corey! I do put most of my attention to practice, & following the advice of practitioners. (Your comment sounds like a rant I’ve heard myself say more than once, to another beginner I know.) But the philosophy, I dunno, just happens. ^__^

      In case other beginners show up and wonder, here are the 4 rules Corey referred to:

      1. Runs all tests
      2. No duplication
      3. Expresses developer intent
      4. Minimizes the number of classes and methods

      Reply
  3. Michael Avila

    Angela! I, like Corey, already enjoy what’s on your mind and how you’re sharing it. I don’t have an answer to any of your questions, but I have a thought you may enjoy. Admittedly, I probably don’t have much to contribute to what you’re thinking. But I’m going to take a stab at it anyway.

    Bob Glass, in his book Software Conflict 2.0, describes some research aimed at revealing the process of design. The main insight from the research is that the process of design is a mental process. More importantly it’s a rapid mental process (I believe he described it as “fast as lightning”). This has stuck with me pretty intensely.

    Given a problem we need to design a solution. What our mind does is continuously (and rapidly!) compare alternative designs keeping the parts of each design that we find most appealing until one whole design exists. Our comparison of potential designs is based on our understanding of what a good design looks like. And our understanding of what a good design looks like is all based on our experience.

    A key to becoming a good? programmer is to clearly understand what is going on in your head while you’re programming.

    I go as far as saying (probably wrongly) that nearly everything we do as programmers is purely to support our ability to design. Pair programming, UML, TDD, and design reviews all force us to seek clarity of vision with what’s going on in our head. Where I find these practices break down is when the person doing them can’t reason their way through what they are doing.

    Note: I’m not saying that pair programming, UML, TDD, and design reviews don’t serve other useful purposes.

    Explaining your design using the UML /can/ force you to slow down and understand then articulate the relationships between the components of your design. Pair programming /can/ force you to slow down and understand that what you’re designing is in fact what you should be designing. TDD /can/ force you to slow down and understand what needs to be done next.

    I like TDD for a lot of reasons, but the part that has changed the way I approach programming the most (that is outside of TDD too) is how often I find myself slowing down what I’m thinking so that I can better understand what I’m doing. It’s the same reason I like pair programming and design reviews.

    I don’t know exactly what I’m getting at, but the idea intrigues me. I’d be curious to get your thoughts! Thanks.

    Reply
    1. Angela Post author

      Hey, Michael A., I like this idea of slowing us down to design. (I’ve been getting a lot of lessions in ‘slow down to go faster’ lately.) And that TDD helps us reason, and notice our reasoning. And I imagine that will make a real difference as I work on understanding how this looks in functional programming. Thanks!

      Reply
  4. Esko Luontola

    My view of TDD is that by writing a test I’m stating what I’m trying to achieve, in a way that allows me to forget my intentions because the computer can check them automatically. I can better focus on the task at hand when I don’t need to think about the all intentions which I made earlier.

    I consider the name of a test [1] to be one of the most important things about the test and I spend much time thinking about how best to explain the feature I’m implementing, so that when the test fails I would be quickly reminded that which of the earlier made intentions I forgot or accidentally broke. Converting the name of the test into executable test code is most of the time a simple mechanical act of writing a few simple lines of code – unless the code to be tested has design problems, which leads us into my second point.

    Another thing that I find very valuable in TDD, is that it makes dealing with bad code painful. [2] Writing tests for the code and adding features to existing code put design pressure on the code, which leads me to improve the things which make the code hard to test or modify.

    [1] http://blog.orfjackal.net/2010/02/three-styles-of-naming-tests.html
    [2] http://blog.orfjackal.net/2010/04/direct-and-indirect-effects-of-tdd.html

    Reply
    1. Angela Post author

      Thanks, Esko.

      I’m really enjoying this idea of “intention.” I mentioned that I’ve been puzzling over how the test isn’t a statement of the problem itself, nor of the solution that will emerge. It seemed to me that it gives us an image of some future world where the solution is in place, or something like that. Your description helps clarify my fuzziness around these ideas. Maybe I could go farther, and use not just “intention”, but “ambition”. A test states the developers’ ambition.

      Looking forward to reading your blog posts. Thanks for joining the conversation.

      Reply
  5. Uncle Bob

    There are two major denominations of TDD. The Mockists, and the Statists. When the statists test the square function, they say:

    (fact (square 2) -> 4) (fact (square 3) -> 9))

    Therefore the statist uses induction to prove that the square function does what is intended.

    The mockist uses a very different approach.

    (fact (square "a") -> "a*a"
    (provided (* x x) -> (str x "*" x)))

    The mockist ensures that the multiplication function called with two arguments, both equal to the argument of the square function.

    Statists risk a certain level of ambiguity. Mockists risk tight coupling to the implementation. The statist might be fooled by a function that happens to return 4 and 9 for inputs of 2 and 3. The mockist might be confused if the square function does not call the multiply function.

    In other words the statist is never sure that a passing test indicates correctness. On the other hand the mockist is never sure that a failing test indicates error. To the extent you know one, you don’t know the other.

    We could call this the uncertainty principle of TDD.

    dP*dF > K

    The uncertainty of passing times the uncertainty of failing is always greater than some constant. i.e. you can never be sure that both passing and failing are meaningful.

    Reply
  6. Angela Post author

    That’s exactly the kind of thing I was wondering about. You understood my question–does that mean you were a philosophy major too?

    Now that you say it, it seems kind of obvious, but it’s much clearer to me now. Thanks.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>