Driving Development with Tests
From Software By Jeff
Here is a tried-and-true Test Driven Development technique. The methods discussed are using Eclipse, but a NetBeans discussion will be forthcoming. In general, the techniques are the same, but the menu items and other idiosyncracies are different.
| Table of contents |
Get a bug or task
It is strongly suggested that some tracking tool be used. We have discussed Bugzilla as a tool. Find a task assigned to you or in your realm of expertise. Claim it, mark it as being worked on by you. This will let other developers know they can let that one go, and allow the tracking software to monitor progress. This is more important in a multi-developer setup, but is very helpful if you work alone, too. If you have to leave the work for a while, it's an easy way to remind yourself where to pickup.
Get a clean workset
Synchronize before starting work. Always. Every time. Never doubt this step. If you've just finished delivering work, and you're in the middle of a development cycle, there might not be anything more to do, but test it out anyway.
In Eclipse, using CVS, this is easy.
Do not worry about re-synchronizing again until either something critical is needed or you are ready to deliver your work. Unless you are expecting something and it gets delivered before you're done, it will only add distraction if you run into merge conflicts on your files or if other things are broken that you don't really affect with your work.
Synchronize infrequently, not often.
Do your work
Write or modifiy a test before you make your changes to classes and files! There is some disagreement on how much to test first, and how much to test during development.
This is key to test-driven-development--start with the test. Make your test expect the new changes. Expect it to fail. When it fails, make the appropriate changes to make it pass. Make changes in small steps, adding members and methods one at a time, testing between each addition, repeating as necessary. Only make changes when the compiler complains or the test you write fails. Never just because you want to or know that you'll have to. If you know you'll have to, write a test for it and prove it.
If you make a new test class, add it to AllLocalTests. Somewhere in your test project, there should be a test that has a list of all of the tests that should be run by JUnit, we're calling it AllLocalTests. If you see a test that is not part of AllLocalTests, add it. Complete coverage is the key.
See also: Good Base Test Case Writing Effective Tests
Stubs and iterative development
If your work requires additional classes or files that don't yet exist, make them, but only fill in what is absolutely necessary.
For example, if you need to create a class to pass as a parameter, make the class without members. If you need to use a method in the class, make it, and only what is required for the class to intelligently use it (fields and properties). If you need to make a Hibernate mapping, make only what you require and what the database requires (not-nullable columns).
Develop in baby steps. Don't work in such a manner that if you need a class as a parameter that you should feel you must make the complete model as in the sequence diagram, nor do you have to make the associated Hibernate mapping. This will all be done in due time, and with the tests driving the development, it should be completely covered as it's developed.
Clean your code
Organize your imports and format your code. This will aid merging more than anyone can possibly explain. Use agreed upon IDE settings, not your personal preferences. This should also be done as you work, but you can easily find the files you modified, and do it before you test for the last time. Unused imports should be in your Eclipse Task list, if nothing else--make sure there are none.
Imports and formatting should not break the tests, but we want to find out before the tests run, not after. It could be the case, for example, that the imports get organized higher than the last class (i.e., java.lang.*, which is imported by default, not java.lang.String). It might be the case that collapsing all of the packages to this level causes ambiguity. Organizing the imports to support easy code-sharing should not impact compilation or running the classes. Find out early by organizing before you run the tests.
Run your tests
Run AllLocalTests, or whatever you called your master test class. If you caused something to fail, find out why and fix it. Either your change was incomplete, had unexpected side effects, or the same bit of work was tested somewhere you weren't expecting. You broke it, you fix it. Repeat as necessary. Do not move on until all of the tests pass.
Do not comment out tests just because they fail. Look at the JUnit output, identify the line that fails. Often, you'll find that it is because you forgot something (like inserting database data), changed something (perhaps as designed in your change), duplicated something (base data now exists and the test no longer needs to insert), or the like. If you make small enough changes, anything you break will be small enough to fix.
The purpose of the test is to ensure that unfamiliar developmers respect intent. It may be that the intent has changed, and therefore a bunch of tests have to be fixed. If you break a test, however, and don't correct it, you've lost control of the project and potentially caused gaps in test coverage.
If you break the rule to not comment out tests, make the next task you work on after delivery fixing the test you commented out. If you commented out a test after discussing it with someone else and they said they'd fix it for you, make sure to follow up with the now missing test.
Do not abandon commented code. Mark the test with a TODO comment so that you can find it easily and others will be able to see that it was commented out with reason (put that in the TODO comment). If you're tracking changes and bugs, add a bug to remind and alert that this test is broken and needs to be repaired.
Merge your code
Yes, this could be the only synchronization you need to do! Get all of the other changes. This is easy if you're working alone, but if your on a collective project, others may have made changes that you need to have. Synchronize as you did when starting work.
Since you have made changes, however, this may result in conflicts.
Merge wisely. There are better merge tools, but this is the one that we have. Try to note what you had to merge, and verify that the merge didn't break the Java. Automated merging is included in this; it can be the case that style, formatting, or even order of elements will be merged and form duplicates. The merger won't complain, but Java will.
Overlap will happen! If you have to throw away your work because you stubbed an Entity that someone else delivered, you should have only a bit of work to discard. Be mindful when merging that your temporary work is meant to be discarded, and that the expected delivery from someone else may not be the same as yours--you will have to fix any problems that come from bad merging or design differences.
Verify that everything compiles. Have Eclipse refresh to ensure the changes are recognized. Possibly rebuild the project or related projects, if the project you're working on delivers archives to other projects.
Run AllLocalTests again, to make sure nothing broke. If it did, it's likely because of merge problems. If there are errors, correct them, returning in the process to the steps above, repeating as necessary.
Commit
Now that everything works, and no more changes are necessary to synchronize, commit your code. This will tell the version control software to take everything that you've worked on, and add it to the repository. This will make it available when the next developer tries to synchronize.
Mark your bug or task done
If you're tracking work in Bugzilla or something similar, go back to your task, mark it complete, or whatever level you've agreed to mark tasks leaving development. This will let others know that it's ready, and allow the tracking to identify one less thing outstanding.
Finish
Move on to the next task. Always start with a clean baseline. If you were able to succesfully commit, you probably have the latest baseline. If you start on the next task right away, you can skip the first step of resynchronizing, although a better practice is to do it anyway--if there's nothing new, you'll get that result almost immediately.