I've just been learning Unit Testing and I'm trying to understand how I could incorporate it with a project with existing code. Say I wanted to write tests for a specific class in that project, but that certain class requires an instance of another class for the the methods to be run and/or tested. How should I approach this?
-
2Did you even bother searching? programmers.stackexchange.com/questions/149782/… programmers.stackexchange.com/questions/135300/… programmers.stackexchange.com/questions/103523/… programmers.stackexchange.com/questions/123964/… programmers.stackexchange.com/questions/134716/…– EuphoricCommented Apr 21, 2014 at 7:03
4 Answers
If the code is already in production and has no tests for it, then by definition you cannot do TDD with it. At that point you have at least these alternatives:
- Discard the code and start again using TDD. This can obviously be time consuming, but judging from previous experience it would often have been faster than the other alternatives.
- Just use TDD for any new code. This leaves the old code to become a rotten core, a black box which eventually nobody will dare change and which is likely to drag down the performance, stability and security of the rest of the code for the lifetime of the project.
- Add tests to the existing code incrementally, and use TDD for any new code. Surprisingly, in my experience this doesn't seem to help much. Unless the code is refactored (see next point) it will still, eventually, become a black box.
- Use TDD for any new code, replacing bits of the old code with TDD code whenever you have to touch any of it. If alternative 1 is not going to happen, this at least gives you some chance that the project will not be chained to a ball of mud for the rest of time. But likely you'll find that pulling out one part of untested legacy code is likely to unravel it completely, and the combined effort of all the refactoring might take as much or more time than re-implementing.
-
1This is the only answer that actually addresses the fact that once code is implemented, TDD can no longer be used per its definition.– user22815Commented Apr 21, 2014 at 13:55
This is a very complex question, in a legacy systems designed without testability in mind probably there are a lot of coupling and this coupling make testing in isolation (unit testing) more difficult.
I can give concrete answer if you ask a concrete question, with code and a concrete situation, but if you ask for general advice the best thing i can do is recommend to you the fantastic book form michael feathers working effectively with legacy code (http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052).
-
I've just read about Mocking Frameworks and I think this is what I was thinking before asking the question. Say I have a class
FooController
that connects to a service instance and performs functions that require instances of other classes, how should I approach testing such complex code? I know I need to read and learn more about testing and all, and thanks for the time for answering! Commented Apr 21, 2014 at 6:10 -
Of course a mocking framework its a very useful tool for isolating code and testing only the subsystem (a class or a small subset of class) you want. But learning to organize your code in the correct way, for example using dependency injection and the other SOLID principles, its really the complex part. Commented Apr 21, 2014 at 21:19
It's not always easy and sometimes you just have to be pragmatic and use integration tests or end-to-end tests to begin with until you have refactored code to be more testable.
Perhaps try to refactor the class to use an interface instead of a concrete class and inject in the implementation during construction. Then use a mocking framework or roll your own mock class and inject that in instead during testing.
Some mocking frameworks are able to mock concrete classes but it depends on the language.
I'll be quite radical but please read this fully before criticising.
In short: test-driven design is a kind of myth. It's impossible for implementation even for a new code unless all specifications are already fixed and won't change (that's classic "Waterfall" case but not a "Agile" one, opposed to the usually declared TDD application area). When it works out of one-shot projects, it works only because its principles are violated, not satisfied.
To understand this, one should imagine case the specification is changed and a new requirement is added, but it's already satisfied in existing code. Should the existing code be dropped and rewritten from scratch? Classic TDD description can't answer this at all - it doesn't deal with an existing code case.
The second question is what shall be tested. If a code piece shall satisfy tests but do nothing more, could be it only listed test cases in a big switch-case? If it doesn't, why? That's what is explicitly requested (at least in classical compact explanation).
The third question is how to check all marginal cases. If a routine multiplies numbers, shall it check INT_MIN*INT_MIN
? Marginal cases knowledge is mainly discovered after some usage experience, but not a priori. (Yep, the more programmer's expertise is, the more ability to find such cases before writing the code he has. But 90% of any expertise is what one's shan't do, but not what one shall.)
My answer is that TDD fulfils two roles:
- It shows an ideal, unreachable in practice (as infinity in mathematics) but ease to understand and so approximated as close as needed.
- It provides an excuse for managers to require testing which is not postponed until "good times" but just now, before reporting a task finish.
So it shall be treated accordingly - "with enthusiasm but without fanaticism".
My personal attitude to TDD is formed with a specific experience of building complex systems which shall work "24*7*365" and an acceptance style approached to military grade. Such systems require, at developers' side, integrated testing of component chains and regular testing on the fly, at the same components and with the same algotithms, but with another tags (so, only final decision checker understands it wasn't real data). For our systems, TDD is intentionally replaced with the following approach:
- Of course, the principle that code shall do what it is intended to, shall be more important that TDD (or analog) principle that a code is as small and simple as possible. This also means that code readability and debuggability is very important. (All this is not a Captain Obvious' comment but the written rule for those guys who tend to follow instructions in the most literal way, and who, regrettably, are present in every large team.)
- Each component starts with a reasonably small test set which allows an experienced programmer to say it's checked, according to the current conditions and resources.
- A part of time (of each programmer separately, or of the whole team) is dedicated to expanding tests for already existing components. This can be, according to goal specifics, 20% to 80%, but definitely not less. This work shan't stop until the whole project development is abandoned.
- To answer the question whether a test works (which is answered by TDD in a one-shot, unscalable manner), the test itself shall have techniques to validate it. For this task, we use so-called inversions. An inversion is a change in incoming data or environment which shall break test in a known way. For example, if a function shall convert A1 to B1 and A2 to B2, its test which validates B1 is response to A1 shall fail if input value becomes A2, and, moreover, one can check failure details (generated exception type, error count, etc.) Not all tests shall have inversions; but, the more complex it is, the more principal inversions are.
P.S. For an initial guide to add tests to an existing project, you could start with M. Feather's book. It isn't based on TDD but, instead, is very practical in suggesting how to reform an existing code in a evolutionary manner.
-
1"It's impossible for implementation even for a new code unless all specifications are already fixed and won't change" Nope, TDD done right (and yes, I've been on teams creating successful code with TDD) makes the code very, very flexible. This creates trust in the possibility of major change even in core code, which without TDD is a nightmare.– l0b0Commented Apr 21, 2014 at 7:47
-
"It shows an ideal, unreachable in practice" How is it unreachable to write tests before writing code? The point is not to write all the tests before writing any code, but writing a single test at a time and then writing the absolute minimal code necessary to pass that test.– l0b0Commented Apr 21, 2014 at 7:51
-
@l0b0 flexibility isn't the thing specific for TDD and not any other approaches. It's the consequence of proper code testing which is fully orthogonal to TDD and its rule "test first", but instead related to the general programmming culture.– NetchCommented Apr 21, 2014 at 8:02
-
@l0b0 "writing a single test at a time" is a way to ensure anything except the final code work, even according to the current specification. Yes, satisfying sequence of tests is a good rule to order one's work. But there are too many cases it causes only very tangled way, instead of the direct one. And, where it's proper (e.g. in writing a mediator class with 100500 independent methods), it has no advantage over writing the same tests after the code, except managing-related issues.– NetchCommented Apr 21, 2014 at 8:07
-
1TDD is not a silver bullet. You also need (at the very least) acceptance tests to ensure all the bits are connected properly. If your tests get "tangled," that's a strong smell that either the code isn't easily testable (in which case it should be refactored) or that the tests are badly written, in which case they should be refactored. An important concept of proper TDD (as opposed to most instances I've seen of "TDD") is that the test code is just as important as the code it's testing.– l0b0Commented Apr 21, 2014 at 8:22