Design and Testability

EDIT 12/23/2008:  I believe this post has been massively misconstrued in the comments.  Apparently, my analogy here was terrible from the way so many people are distorting what I was trying to say.  For the record, 

I DID NOT SAY DON’T WRITE TESTS

I DID NOT SAY YOU MUST BE A MASTER OF ALL DESIGN LORE BEFORE YOU GET TO WRITE A TEST

I DID NOT SAY THAT YOU MUST HAVE A PERFECT, PURIST DESIGN FIRST

I DID NOT SAY DO NOT WRITE TESTS FOR LEGACY CODE

 

I said, or attempted to say, that testing and testability cannot be decoupled from design, and any effort towards automated testing / unit testing / TDD is probably not going to be successful until the team has a good understanding of the basics ( and I did say basics here) of fundamental software design.

 

My most recent Patterns in Practice article entitled Design for Testability is available in this month’s issue of MSDN Magazine. This time around, I tried to tackle some of the design issues around supporting testability.  I’ve written four articles so far for MSDN on design fundamentals.  If you look at the whole body of work, there’s a very common theme:  good, fundamental design is being able to do one thing at a time.  I want to decompose the system so that I can change one thing at a time with less risk of destabilizing other parts of the system.  I want to be able to understand one element of the system at a time.  I want to solve one little problem at a time instead of tackling the whole mess in one try. 

Testability as a design concept is right in line with this kind of thinking.  Testability means being able to easily create rapid, effective, and focused feedback cycles against your code with automated tests.  Testability isn’t some strange and unnatural new way to shape code.  For the most part (ok, it might be if you cut your programming teeth with Microsoft’s RAD tools), Testability goes hand in hand with the same structural qualities like cohesion and coupling that we use to create code that’s easier to write, change, and understand.  On the other hand, we can use Testability within design techniques like Test Driven Development and/or Behavioral Driven Development to inform and help guide our design decisions.

 

Unit Testing – Solid Design == PAIN!

A couple months back Roy Osherove wrote a post entitled Unit Testing decoupled from Design == Adoption that posited the theory that things will work out better for the larger community if we drop all this hard design theory and TDD discipline and just get developers to write unit tests first.  The theory being that more developers would be more likely to adopt some sort of unit testing, fail, then naturally circle back to learning about design or try out TDD after they get started. 

I couldn’t possibly disagree more with Roy on this.  I would describe Roy’s suggestion as akin to telling an out of shape 30 something with no prior athletic experience to start playing competitive basketball by just walking onto the court and getting into a pickup game.  That out of shape 30 something fellow isn’t going to be able to compete, and he’s likely to limp off too discouraged and humiliated to continue.  After all, why would the average person sign up for another beating?  Likewise, a developer with little foundational knowledge about good design principles will be very unlikely to succeed in any way with unit testing because it’ll be just too hard with poorly structured code (and no, I don’t think any “Isolation” framework will change that).  I would tell the 30 something to work on his cardiovascular conditioning (and his jumpshot) before he steps onto that basketball court for the first time.  I’d tell that developer to focus on his understanding (not rote knowledge, but understanding) of good design first and be ready to succeed upfront. 

I guess to summarize and beat the analogy into the ground, cardiovascular health is beneficial for more than just playing pickup basketball after work and good design is valuable for more things than making unit testing and acceptance testing easier.  In short, good design is far more beneficial and important than the practice of unit testing.  However, good design + TDD/Behavioral Driven Development/Acceptance Test Driven Development?  That’s where the real rewards happen.  Don’t settle for mediocre efforts.  Do both and reap the rewards.

A couple years ago I was reading some writing from older guys like Steve McConnell and Robert Glass that stated that Test Driven Development couldn’t work because unit testing was proven to be only marginal cost effective – and they based those statements on studies that dated back to COBOL developers in the ‘70s and ‘80s writing purely procedural code.  TDD with well factored OOP or Functional code compared to unit testing tightly coupled structured programming is apples and oranges.  I have consistently found that the cost effectiveness of unit testing and Test Driven Development is almost entirely based on the structural design of the class structure of the system being built. 

 

Other Reading

I think this is a huge topic, and one measly 3500 word article isn’t going to cover the topic adequately.  Learning xUnit tools and even mock object frameworks is a piece of cake compared to learning how to design and write code that enables efficient testing.  Here are some of my older writings from the time when I had just gotten over the hump and started to figure out how to compose code to enable Test Driven Development.

 

  • TDD Design Starter Kit: It’s All about Assigning Responsibilities
  • TDD Design Starter Kit – State vs. Interaction Testing
  • TDD Design Starter Kit – Dependency Inversion Principle
  • TDD Design Starter Kit – Responsibilities, Cohesion, and Coupling
  • TDD Design Starter Kit – Static Methods and Singletons May Be Harmful
  • Succeed with TDD by designing with TDD
  • Unit Testing Business Logic without Tripping Over the Database

     

  • Jeremy’s First Rule of TDD
  • Jeremy’s Second Law of TDD: Push, Don’t Pull
  • Achieve Better Results by following Jeremy’s Third Law of TDD: Test Small Before Testing Big – I think this is the most important lesson for making TDD work for you
  • Jeremy’s Fourth Law of Test Driven Development: Keep Your Tail Short

     

  • Mock Objects and Stubs: The Bottle Brush of TDD
  • Why and When to Use Mock Objects
  • Best and Worst Practices for Mock Objects
  • Mock Objects are your Friend

     

  • Testing Granularity, Feedback Cycles, and Holistic Development

     

    The Future

    The next article is already in editing.  For February I’m discussing Ruby on Rails inspired design ideas like convention over configuration, opinionated software, sensible defaults, and once and only once.  For April I’m going to write about data access and persistence patterns to fit in with the theme of that month’s issue.  After that?

    • Patterns for Testability – I missed a lot of important topics in this month’s article.  I’d like to hit separated presentation patterns and Inversion of Control as a design pattern maybe.
    • Designing an Internal DSL
    • Some kind of post on Composition and IoC
    • Patterns for Client/Server Communication

    Any requests?  Thoughts?  I’m bypassing (D)DDD for the most part because there are articles on the way on those very topics from more qualified folks (Don’t tell anyone, but I’m not entirely sure I completely buy off on DDD).

     

    Self Reflection

    The first three articles in this series are effectively tributes to my software design heroes (Fowler, Rebecca Wirfs-Brock, Uncle Bob Martin, and the Pragmatic Programmers).  This last one was largely based on my own prior writings on testability and design.  It’s also the one I’m least happy with.  Back to hero tributes after this;)

     

  • About Jeremy Miller

    Jeremy is the Chief Software Architect at Dovetail Software, the coolest ISV in Austin. Jeremy began his IT career writing "Shadow IT" applications to automate his engineering documentation, then wandered into software development because it looked like more fun. Jeremy is the author of the open source StructureMap tool for Dependency Injection with .Net, StoryTeller for supercharged acceptance testing in .Net, and one of the principal developers behind FubuMVC. Jeremy's thoughts on all things software can be found at The Shade Tree Developer at http://codebetter.com/jeremymiller.
    This entry was posted in Uncategorized. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
    • http://codebetter.com/blogs/jeremy.miller Jeremy D. Miller

      @David,

      “So why not use tests, so that I can build up a suite of tests as I refactor and end up being much more confident in my refactored code?”

      One last time, I DID NOT SAY DON’T WRITE TESTS GODDAMMIT!!!!!!!!!!!!!!!!!!!!

      I definitely agree with that statement, and please note that this article wasn’t meant to address legacy code at all. My experience with retrofitting tests to legacy iode, and I’ve got quite a bit of experience with that btw, is that you generally need to write coarse grained tests on the outside before you can refactor the code safely or EFFICIENTLY write unit tests. Yes, sometimes you have to reshape the code before you can get the tests in, but that’s risky, especially with really bad code that you don’t completely understand.

      I’ve written extensively about legacy code in the past:

      http://codebetter.com/blogs/jeremy.miller/archive/2006/05/04/144032.aspx

      http://codebetter.com/blogs/jeremy.miller/archive/2007/04/01/Removing-the-_2200_Legacy_2200_-from-your-Code.aspx

      If you look at those, you’ll see that I talk a lot about retrofitting tests to ugly code.

      Comments are closed. This was a stupid waste of my time and I regret ever posting this.

    • http://www.commongenius.com David Nelson

      I wasn’t trying to put words in your mouth. Having re-read your post again, it still sounds to me like that’s what you are trying to say. I don’t know what other conclusion to draw from your basketball analogy. However, given your revised statement in your comment, “you cannot unlink TDD or just plain unit testing, with software design topics”, I agree with that statement in principle, but I fail to see the point. Of course good software design principles are important for TDD. They are important for all software development! Any time you try to develop software without a solid foundation in core principles, you are going to have problems. There is nothing special about TDD in that regard. And it doesn’t in any way mean that someone without that foundation should eschew TDD altogether until they have it, any more than they should eschew programming altogether until they have it. Learning to program is a hands-on process, and practicing TDD can do just as much to teach a developer design principles as a “Hello, World!” sample.

      As far as legacy code, if I am going to refactor, why shouldn’t I start with unit tests, just like I would if I were starting a new project with TDD? In a new project, I would start by writing a test. It wouldn’t compile because the code doesn’t exist. So I would write stubs to make it compile, but the test will fail because there is no functionality. So I write code to make the test pass. It seems to me that the same approach could work with legacy code. Write a (well-designed) test, which won’t compile because the legacy code is poorly organized. Refactor the legacy code so that the test compiles. But the test will fail because the legacy code has bugs. So fix the bugs until the test passes. Obviously refactoring the design and fixing the bugs will be more difficult than writing the code from scratch, but that will be true whether I am using tests or not. So why not use tests, so that I can build up a suite of tests as I refactor and end up being much more confident in my refactored code?

    • http://codebetter.com/blogs/jeremy.miller Jeremy D. Miller

      @David Nelson,

      Please don’t put words into my mouth. What part of this post said “don’t write unit tests until your a complete master of design?” All I’m trying to say, and apparently doing it very badly, is that you cannot unlink TDD or just plain unit testing, with software design topics. Nothing more, nothing less.

      “Wouldn’t it be better for me to start writing tests again the application that I have to modify, so that I can use those tests to help me verify behavior when I have a bug to fix?”

      Yes, but this is another place where I disagree with Roy. With legacy code, and basically all legacy code is nasty spaghetti code, *unit* tests are essentially impossible or at least grossly inefficient because of the coupling. With legacy code you compromise and generally go for more coarse grained integration tests on the outside to preserve behavior, then you might try to clean up the code second.

    • http://www.commongenius.com David Nelson

      “TDD against bad code is going to hurt, and hurt badly”

      It sounds like you are saying that someone who is not already an expert at testing shouldn’t be writing unit tests against code that wasn’t written with TDD, because they will have a bad testing experience that will turn them off to testing in the future. I have to say that I agree with Roy in this case. Lets say I’m a developer who hasn’t used TDD and whose primary job is maintaining a legacy code base (not such a stretch). If you don’t think I should write tests against that codebase because it wasn’t written with TDD, then what should I be writing tests against? Should I design and build a completely new application, one which has no value to me or my employer, for the sole purpose of learning how to test? Wouldn’t it be better for me to start writing tests again the application that I have to modify, so that I can use those tests to help me verify behavior when I have a bug to fix?

      Sure, its going to be hard. Sure, I am going to have a hard time isolating the functionality under test because of the coupling in the application. Sure I am going to have to change tests more often because other changes to the application have too large of a ripple effect. But isn’t it better to have something than nothing? Isn’t it better for me to get started with what I have, and make some hard progress, and start seeing some gains in confidence due to at least having some testing, instead of never starting at all?

    • http://blah.winsmarts.com Sahil Malik

      Dude, did you just group me with Smell-aware? DAMN!

    • http://codebetter.com/blogs/jeremy.miller Jeremy D. Miller

      @Ian,

      I’m certainly not implying that you have to be a master of design before you attempt to write a unit test, only that unit testing just isn’t going to be successful with badly structured code. I think you’re putting some words in my mouth that I didn’t say.

      If there’s anything I’m really trying to get across it’s that TDD against bad code is going to hurt, and hurt badly. I don’t think it’s even remotely obvious to most people that their struggles with unit testing and TDD are often caused by design deficiencies. I strongly believe that de-linking unit testing and design the way that Roy is suggesting is not going to work.

    • Kerry MacLean

      Jeremy, thanks for saving me a lot of struggle and helping to inform an internal discussion. As usual, you said it far better than I could anyway (especially as you and Chad provided the jumping off point for my education).

    • Ian Cooper

      I do not want to anger you as well, but as I posted here:

      http://codebetter.com/blogs/ian_cooper/archive/2008/09/23/learning-and-crafstmanship.aspx

      I am just not sure that I agree with you.

      People learn differently, but for many learning tends to go hand-in-hand with doing. For a number of those people the desire to improve their tests is the spur that leads them to search out better design.

      I have the same design heroes as you, hell we had some Robert Martin hero worship in the office only yesterday, but asking people to appreciate design often seems to fall on deaf ears.

      For me nore people seem to be able to see the benefits of TDD and can be led to good design through it.

      But I don’t have an education background, so working out how best to impart this information is just guesswork for me

    • Jeff

      Couldn’t agree more on the emphasis of good design in combination with TDD, Jeremy. Your blog has been my go-to-guide on real world TDD and mocking and your statements here further confirm that.

      I hope to see a comprehensive book, from you, on these topics one day.

      Nice work.

    • http://codebetter.com/blogs/jeremy.miller Jeremy D. Miller

      “I mean isolated tests that test just one thing, without reliance on external methods and or data and runs quickly, vs. a 700 line test that has 50 lines of set up code, another 100 lines of getting data in the right place, and 300 lines of clean up code that takes 2 minutes to run. ”

      Ok, but I would say that that’s a matter of design, or at least of using “Testability” as a design heuristic and/or design goal.

      “My point being that if you can understand and master the difference between writing a good maintainable test and a bad unmaintainable slow test you will come to understand the design side of it when it comes time to improve your design because a good test writing skills lives independently outside of good design skills. ”

      That’s a nice way of putting it.

      I’ll apologize for getting hot over the previous exchange.

    • Brian Johnston

      Well I apologize as I really do respect you and wasn’t trying to inflame your wrath/piss you off, and I don’t disagree with you that design is important, most important in the end.

      But when I say learning testing techniques, I didn’t mean Assert.AreEqual, I mean isolated tests that test just one thing, without reliance on external methods and or data and runs quickly, vs. a 700 line test that has 50 lines of set up code, another 100 lines of getting data in the right place, and 300 lines of clean up code that takes 2 minutes to run.

      My point being that if you can understand and master the difference between writing a good maintainable test and a bad unmaintainable slow test you will come to understand the design side of it when it comes time to improve your design because a good test writing skills lives independently outside of good design skills.

      So the idea being, rather than trying to take an old dog (as is the case many of the times) and teach him this new finagled TDD thing, teach them about good tests firsts that can be quickly run and cover his rear if he makes changes; don’t challenge some legacy design/application that he’s proud of by saying it’s not testable or it’s poorly designed, and when he learns good testing techniques, he might be more apt to adopt TDD.

      Again I apologize for apparently insulting you, it was NOT my intention.

    • http://codebetter.com/blogs/jeremy.miller Jeremy D. Miller

      @Brian,

      Congratulations. I don’t think anyone (not even jdn, Frans Bouma, Sahil Malik, or Bellware) has ever pissed me off as much as you did with this asinine comment.

      You more or less completely missed my point. I’m urging developers to learn more about design before trying TDD because I think you’re likely to fall flat on your face with TDD if you don’t have a solid background with design. And why do I think this? Because it happened to me, and I’ve seen it happen to many other people and teams over the last 5 years.

      “assuming that everyone who wants to test their code is as interested in perfect theoretical design principals as you.”

      This is inflammatory bullshit on your part. I’m not looking for theoretical purity. I’m not saying go out and memorize the fucking GoF book. I’m saying “why don’t you learn not to couple your business logic code to socket manipulation code” before you try to write automated unit tests.

      I’m trying to actually help people succeed here, and I think the biggest hurdle for people trying to adopt TDD/BDD/automated testing isn’t learning the testing tools or terminology, IT’S THE STRUCTURAL DEFICIENCIES OF THEIR DESIGN. Successful TDD, efficient TDD, using TDD without ripping your hair out in frustration. Any of that requires good cohesion and coupling qualities in your code. TDD or even just automated testing against poorly structured code will not provide much value compared to the effort you’ll spend on doing it.

      To Roy’s point, I think that learning basic testing techniques is basically trivial. Assert.AreEqual(expected, actual) == child’s play.

      Could you maybe try thinking a little bit before you comment?

    • Brian Johnston

      So your saying, learning good overall testing skills (become a mater of a single domain) should be avoided until you can design correctly because it’s akin to telling someone of my age and size to go play a pick-up game of basketball to learn how to play basket ball. That you should instead focus on TDD which requires being able to design AND test (thus it’s name) at the same time (a master of two domains, one of which is testing, which you just told them they’re not good enough for), which would be akin to telling me to try out for Nets to learn basketball – a much more demanding task.

      So back when you were in Shadow IT did you tell your users to first learn the SMTP protocol and authentication tokens before sending an encrypted email so that instead of having lack-luster security that could be improved on over time (but at least there was some), that they instead should have no security while they learned the intricateness of the technologies that make it possible so that they were fully aware of the decisions they were making?

      Interesting J – I love your writings, and please take this with respect, but I think you’re missing the forest for the trees on this one and are assuming that everyone who wants to test their code is as interested in perfect theoretical design principals as you.

    • SteveJ

      I’m interested in why unit testing with OOP is more cost effective than with procedural programming. I love me some unit tests, but I’ve only done it with OOP. And sometimes it’s pretty painful and you throw up your hands and go, why oh why did I change the interface and now I have to change reams of test code to accommodate a change in design. As a design tool I find TDD to be great, as a regression catcher – I spend more time fixing tests than finding bugs. Of course I work R&D so changes like the world is flat AND round AND kinda rhombosodial are the norm for me.

    • http://jonathan-oliver.blogspot.com/ Jonathan

      I readily agree with your analysis of Roy’s post. I remember having the same reaction when I read it a few months ago. I suppose that we’re all entitled to a post that’s out in left field from time to time.

    • http://michaelstechnical.blogspot.com Michael

      I would like to hear your criticisms of DDD. Something about it doesn’t seem right to me, but I can’t point out exactly what that is. Could you elaborate?

    • Geovanny Tejeda

      Greetings from the Caribbean,
      FANTASTIC post Jeremy, I think you finally nailed with me, I’m that 30 something (do have a killer jump shot tho).

      I have been trying to get me and my team on TDD for months now; this has been without a doubt, the biggest (and hardest) challenge of my career, yet, your post made me realized that while TDD might bring a much needed Code-ZEN I’m looking for, I should (and will) really start focusing now is on my design foundations.

      Keep up the good Work,
      Geo

    • http://javadots.blogspot.com/ Itay Maman

      The inherent trade off that I find with testability is that it may be at odds with encapsulation. In order to test something the code that computes it must expose to the test. It likely that other than testing there’s no need for this exposure.

      This often ends up with making some part of the behavior of the class plugable (a-la the State/Strategy design pattern), which makes the class somewhat more complicated, just for the sake of testability.