No, I find that TDD generally encourages good design. Things which are easily testable are often easy to use in production too... because when coming up with a test, you think, "What would I like to do next?" and then make it work.
When practising TDD you are forced to think through the 'use cases' for an API, how the programmer is going to use the classes/methods and you will end up with a very usable framework. If you have to create a hundred FooFactory
and BarAuthenticator
s, or the API is becoming 'ultra modular' like you say, you will likely pick up on that while writing your test code and think about how to simplify it.
As for parameters and dependency injection - I usually find that dependencies become a lot clearer using TDD. They're usually constructor arguments rather than method arguments, but making it clear that an API implementation needs an authenticator, a source of randomness etc. is useful in understanding what it's doing. You can be sure that the code isn't going to hit the network or a remote database somewhere because it's hiding these things in its implementation. These kind of hidden details are what makes an API hard to use in a test environment.
Note that when dependencies are part of the constructor call, it's not part of the interface that class may implement - the dependencies within an interface are usually hidden, but TDD means that the implementation exposes them in an obvious way.
A good source of information about dependency injection and TDD can be found on the Google Testing Blog. Pay particular attention to the posts by Miško Hevery or watch some of his videos on Youtube: Don't Look For Things and more.
TDD is an API design technique. Every time you write a unit test, you are either creating an API for the first time, or using a previously created API. Each one of those tests lets you "feel" how easy or difficult the API is to use. Each new test forces you to consider the API from a new point of view. There can hardly be a better way to design APIs than to exhaustively use them the way TDD forces you to.
Indeed, this is the reason that TDD is considered a design technique rather than a testing technique. When you practice TDD, you do not design your APIs in a vacuum. You design them by using them!
I've been using TDD for several years now and I find that it drives an API design towards a more usable design by providing you with two different clients for the API from the start; you have production code and test code both of which want to drive the API in different ways.
It's true that sometimes I add things for the sake of making it easier to test the API but I almost always find that things that I think I'm putting in just for testability's sake are actually very useful for monitoring purposes. So, for example, a FooAllocator
might end up with an optional constructor argument which is a monitoring interface (IMonitorFooAllocations
) which is very useful for mocking out during testing to enable me to take a peek inside but which also tends to be very useful when you suddenly find that you have to expose some allocation metrics to the rest of the world whilst in production. I now tend to think of the extra bits that I might want to add to enable easy testing in terms of their dual use for optional production monitoring. I generally write server code and being able to expose the internals of things as perfmon counters is VERY useful...
Likewise you're correct to say that often the objects that makes up an API might take some other objects explicitly rather than reaching out and getting them from a known place but this is a good thing. Trust me, once you get used to dealing with explicit dependencies you wont want to go back to having to dig through class after class to work out how and why your Widgets
are accessing active directory when there's no hint in the API that they would want to do such a thing. Also it's often the case that you break open these dependency relationships during design and testing and then hide them again as you put all of the pieces together. You still 'parameterize from above' but more often than not an API's object model might mean that you never really see the 'above' as a user of the API. You end up with one place to configure the API with the things that it needs and often this looks no different to how it would have looked if you had a mass of singletons and globals and hidden dependencies.
But remember, TDD is a tool, when it doesn't fit don't use it.
TDD leads to emergent design which ultimately produces a very usable and extensible API. TDD isn't about testing. It's about creating seams in your code that you can use to add behavior as you discover more about your project.
Most of the things you have listed as disadvantages of TDD are considered by many to be elements of good design.
If particular note the idea of something being passed in instead of something the method should "just know" - the latter often leading to singletons or other anti-cohesive designs.
While it may be true that you may end up with a design where some aspects are only there to support testability this is not necessarily a bad thing.
As a rule, though, if you have to rethink your design to make it testable you will general arrive at a design that is better in terms of cohesiveness and fitness for purpose - while still being flexible in the sense that it can easily be changed to whatever your future needs are through refactoring because you have the tests there to give you the confidence to make changes quickly.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With