Implementing modern automated testing on software requires that the system in question be broken up into pieces. This is not to say broken into different systems, just that the code that makes up the entire application need to be composed of distinct pieces, or modules (or classes, or components, etc.) When things are properly designed, an application will be composed of many different components, and they all work together via interfaces – the information they pass among themselves. Current automated testing approaches can then take each module and feed it different values for the various interface elements, and check to see that the component behaves in the proper way.
For example, say we have a system that is supposed to accept a data feed (from another system, not a person), validate the data in that feed and record any errors, then email one person on success or a different person on failure. We could break this system into the following components:
- Input reader – reads the incoming data (i.e. in XML) and breaks it into the distinct data fields
- Data Validator – looks at each piece of data to ensure it follows business rules
- Error Logger – saves any errors found (to text file, database, or elsewhere)
- Emailer – creates and sends emails
With the system broken into these parts, automated tests could feed the Input Reader different XML files (some valid, some invalid, some incomplete, etc.) and ensure it handled them all correctly. A different unit test could create a Data Validator and send it samples of valid and invalid data, and so on.
When working with Brownfield software, introducing automated testing can be one of the most difficult parts because the code is normally a Big Ball of Mud. In order to introduce automated testing you need to refactor the code into modules, and you do this by finding seams. Michael Feathers describes seams as “… a place where you can alter behavior in your program without editing in that place.” In the brownfield software arena, a seam can be thought of as a convenient place where a module can naturally be broken off into a separate piece.
To break a module off at a seam first create an empty class, then add appropriate code to the legacy Ball of Mud to create this class. Note that the new class should not be static (i.e. there can be more than one instance of this new class), though it can be global if that’s easier. Creating global variables for classes is not a recommended practice, but working with Brownfield Software is often a process where we trade bad practices for something not-quite-so-bad, then later swap that one out for something better. This keeps the application continually available during the upgrading process.
Now pick a method in the Ball of Mud that fits well into your new class. In other words, the idea behind the method and the idea behind the class are very similar. Refactor the method to remove any references to global variables. All data needed for the method to do its job should be passed in via parameters from the calling code. This may require adjustments to code in other parts of the application.
Now cut that method out of the code in the Ball of Mud, and paste it into your new class. Fix any build errors that result by just creating a new copy of your class when needed, and calling the method you pasted in. Your application should now build and run EXACTLY like it did before. We’ve not changed any logic, just adjusted the structure a little.
You now have a new class and method which can be tested using automated tools. Create unit tests to fully exercise the new method, and fix any defects that you may find during the unit test creation. However, be careful when fixing newly found defects. They have been in the system for a long time and other parts of the application may rely on these “bugs” to occur. You may find that fixing one bug uncovers others elsewhere, and so the net result after all your hard work is a system that is more unstable than before.