This week, I was working on a project with a co-worker that has very few automated tests. There’s a SpecFlow project that does some integration testing, but it’s really clunky for fine-tuning and bug fixing. The problem with this project is that it’s a classic example of software built without a proper design and without any consideration for how it would be tested. The result is that you’ve got a huge mess of code with no intuitiveness.
Think About How You’d Expect the Software to Work
One of the things I’m very good at is writing software that works the way I expect it to. If I have some settings that are stored in an XML file, I think to myself, “iTunes wouldn’t require me to edit an XML file to change these settings. They’d give me a way to configure it through the UI.” And so, I build a way to configure my settings through the UI because that’s what I would expect a good application to do.
I apply the same logic to objects that I create in a class libraries. If there’s a piece of functionality that the class needs to provide, I provide a simple way to access it. I don’t expose a bunch of methods that need to be called in a particular order when it’s not necessary.
I wanted to simplify the messy project described earlier to make testing easier and more efficient. As I was thinking about how to accomplish this, I found myself asking the question, “How would I expect this to work?” And then, I would answer myself, “Well, I want to pass in <foo> and get back <bar>.” With that thought, a light came on: that’s a test–a test that should have been written before anything else was coded.
Focus On What You’re Trying to Accomplish
If the original developer would have taken a minute to think about how the software should work and write tests accordingly, this project would be in much better shape. In its current state, it’s mostly functional, but there is no way to hammer out the last few bugs other than by performing manual, end-to-end testing. As items are fixed, there is nothing in place to ensure nothing was broken. There is also no way to ensure that fixing future issues will not re-break the item that was just fixed.
A lot of developers struggle with writing tests first, and I think it’s because we learn to write tests by writing code first. “I can’t write the test because the methods don’t exist.” You’re thinking too implementation-y about the test. You’re writing a method. It should do one thing. What is that thing, and how can you test it? That’s the test you write. “But the method does way more than that!” Well, it’s probably a bad method. Whoever wrote it should have taken a minute upfront to think about what they were trying to accomplish with that method before creating a huge, unmanageable mess!
Make It Intuitive
I like “clean” and “simple” as characteristics of good software, but above all-else it needs to be intuitive. It’s not intuitive to browse the hard disk to find an XML configuration file to change configuration settings. It’s (generally) not intuitive to instantiate a class and call five different methods in order to accomplish a single task. The simplest thing you can do when creating a new class is to list out what you need from the class. Don’t worry about all the methods you’ll need to implement that functionality, just focus on the functionality.
Let’s say you have a requirement that the class needs to do X. So, how about creating a method called DoX? Now, you’re ready to write a test: DoX_DoesX. It’s easy to get straight to the meat for testing purposes, and you’ve exposed a clean, intuitive interface. What does DoX do? It does X.
As you build out functionality, continue to take time to think about what the object you’re creating should do. Maybe DoX needs some settings from the registry, but reading settings from the registry is not part of X. Don’t just shrug and put a bunch of registry stuff in your method! That adds unnecessary overhead for testing and, at the end of the day, it has nothing to do with the functionality that you’re interested in achieving with that method. Instead, create an ISettingsProvider interface and implement a RegistrySettingsProvider class to be used by your object, or simply provide the object with settings retrieved elsewhere in the application in its constructor.
Remember, hard to test usually means hard to use. Writing tests first help ensure that your code can be consumed easily and intuitively. If you find that your code is difficult to test, there’s probably a way to simplify to make it easier and improve the design.