Modern Programming: Object Oriented Programming and Best Practices
上QQ阅读APP看书,第一时间看更新

Test-Driven Development

TDD (Test-Driven Development) is such a big topic, plenty of books have been written about it. Indeed, one of those books was written by me: Test-Driven iOS Development (http://www.pearsoned.co.uk/bookshop/detail.asp?item=100000000444373). So, I won't go into too much detail here. If you've never come across test-driven development before, or the phrase "red-green-refactor," I recommend Growing Object-Oriented Software, Guided By Tests (http://www.growing-object-oriented-software.com/) (unless you're focusing on iOS, of course).

The point of TDD

People talk about test-driven development as a way to ensure high test coverage. It does that, for sure. But its main utility is as a design tool. You can construct an executable specification of a module or class, based on how you need to use that class in your product. Often, I'll create some tests while I'm designing a class, but remove them as the code changes and they become obsolete.

I've delegated classes to other developers before by writing a suite of tests and asking them to fill in the implementation. I've left myself a failing test on a Friday evening, so I know what I'm supposed to be doing on Monday morning (the #error C preprocessor command, which inserts a compiler error with a custom message, is also useful for this). TDD has plenty of utilities beyond generating automated regression tests.

Notice that TDD only helps you with your design when you limit yourself to designs that can be (easily) achieved by doing TDD. That's no bad thing, as it means that everything will be designed in similar, understandable ways. It's like a magazine having a tone and style guide, so readers have a base level of expectations of any article.

Particular constraints, or at least suggestions, derived from allowing TDD to elicit design choices include that your design will probably be loosely coupled (that is, each module will not depend greatly on other modules in the system) with interchangeable dependencies injected from the outside. If your response to that is "great - that's what I'd want," then you'll have no problem.

The Software I'm Writing Can't Be Tested

Actually, it probably can. Apart from the sample code from the afore-mentioned book, there's code in every project I've written that hasn't been tested. In most cases, it probably can be tested. Let's look at some of the real reasons the tests haven't been written.

I've already written the code without tests, and can't work out how to retroactively test it

This is a common complaint. Don't let a TDD proponent smugly say "well, you should have written tests in the first place" – that's dogmatic and unhelpful. Besides, it's too late. Instead, you should decide whether you want to (and can) spend the time changing the code to make it amenable to testing.

It's not just time, of course; there's a risk associated with any change to software. – As mentioned elsewhere in this book, any code you write is a liability, not an asset. The decision regarding whether or not you adapt the code to support tests' adaptation should consider not only the cost of doing the work, but the potential risk of doing it. (I'm deliberately calling this work "adaptation" rather than "refactoring." Refactoring means to change the structure of a module without affecting its behavior. Until you have the tests in place, you cannot guarantee that the behavior is unchanged.) These need to be balanced against the potential benefits of having the code under test, and the opportunity cost of not getting the code into shape when you get the chance.

If you decide you do want to go ahead with the changes, you should plan your approach so that the work done to support the tests is not too invasive. You don't want to change the behavior of the software until you can see whether such changes reflect your expectations. A great resource for this is Michael Feathers' Working Effectively With Legacy Code (https://c2.com/cgi/wiki?WorkingEffectivelyWithLegacyCode).

I don't know how to test that API/design/whatever

Often, "this can't be tested" comes down to "I don't know how this could be tested." Sometimes, it's actually true that some particular API doesn't lend itself to being used in isolation. A good example is low-level graphics code, which often expects that some context exists into which you're drawing. It can be very hard (if indeed it's possible at all) to reproduce this context in a way that allows a test harness to capture and inspect the drawing commands.

You can provide such an inspection capability by wrapping the problematic API in an interface of your own design. Then, you can swap that out for a testable implementation – or for an alternative API, if that becomes desirable. OK, the adaptor class you wrote probably can't be tested still, but it should be thin enough for that to be a low risk.

In other cases, there is a way to test the code that can be brought out with a bit of thought. I'm often told that an app with a lot of GUI code can't be tested. Why not?

What's in a GUI app? For a start, there are a load of data models and "business" logic that would be the same in any other context and can easily be tested. Then, there's the interaction with the UI: the "controller" layer in the MVC world. That's code that reacts to events coming from the UI by triggering changes in the model and reacts to events coming from the model by updating the view. That's easy to test too, by simulating the events and ensuring that the controller responds to them correctly; mocking the "other end" of the interaction.

This just leaves any custom drawing code in the view layer. This can indeed be both difficult (see above) and irrelevant – sometimes, what's important about graphics isn't their "correctness" but their aesthetic qualities. You can't really derive an automated test for that.

If your app really is mainly custom drawing code, then: (i) I might be willing to concede that most of it can't be tested; (ii) you may need to rethink your architecture.

I don't have time right now

There! There's a real answer to "why aren't there tests for this?" It genuinely can be quicker and/or cheaper to write code without tests than to create both, particularly if working out how to test the feature needs to be factored in. As I said earlier though, a full cost analysis of the testing effort should include the potential costs of not having the tests. And, as we know, trying to predict how many bugs will be present in untested code is hard.

So Is Test-Driven Development A Silver Bullet?

As you will see later in this chapter, it is not believed that there is a silver bullet to making software. Plenty of people are happy with the results they get from TDD. Other people are happy with the results they get from other practices. My opinion is that if you are making something that solves problems and can demonstrate with high confidence that what you are doing is solving those problems, then you are making a valuable contribution. Personally, I am currently happy with TDD as a way to show which parts of a problem I have solved with software.