Code listing #1
Code listing #2
Code listing #3
Code listing #4
One of the biggest complaints that I hear from developers making their first foray into unit testing is that a lot of their code is untestable. Upon reviewing their code, I find that they're generally correct, but not because the code is particularly complicated. More often than not, the problem lies in the design of the application. This article details a quick exercise that I like to walk these developers through in order to get them thinking about designing their software with unit testing in mind.
Please keep in mind that the code I will present throughout this article has many problems such as the lack of proper error handling, variables used and sent to various systems without proper data scrubbing, and so forth. What I really want you to concentrate on is the overall design of the system and how it applies to unit testing.
We begin with a very simple class that we need to unit test. SEE CODE LISTING #1. As we see, this is a very simple class that stores, and allows access to, preferences for a particular user in our system. This class is extremely representative of the type of design I see in web-based applications written by young developers. On the surface, this appears to be a fine design and it certainly works inside the application without any problems. It exhibits qualities of OOP that we like to see, encapsulation, separation of concerns, high cohesion, etc.
We're off to a good start, but now we need to write a few unit tests, and almost immediately we run into problems. In order to write even the most basic of unit tests for the Preferences class, we need to setup a database and create a table (prefs) with some demo data in there. First of all, that's a lot of work for some simple unit tests, and we end up testing things that unit tests don't need to test, namely the database, the connection to the database, and the library we use to access the database. Let me be clear, these things will eventually need to be tested, but these tests usually come much later in the form of functionality testing, aka application testing, aka integration testing.
So how can we go about refactoring our design to make unit testing easier? What if we remove the MySQL specific code that handles the actual communication with the database into a separate object and refactor the Preferences class to do away with the specifics of the persistence mechanism? SEE CODE LISTING #2. We're on our way to being able to unit test Preferences, but we're not there yet. Even though we've moved the persistence code into a separate class, we still have to have the database setup in order to unit test the code.
In order to have a truly meaningful unit test, we need to be able to control the inputs and outputs. By moving the code specific to our persistence mechanism to another class, we can easily create another class which will allow us to mock up the data persistence mechanism. SEE CODE LISTING #3. With a small update to the preferences class, namely we set the $dao variable to be an instance ofPrefsDAOMock instead of PrefsDAOMysql, we can now run our program without needing to setup a database first. We are now able to write unit tests that are specific to the Preferences class.
However, looking at the code shows us something interesting. Both of the PrefsDAO classes have the same set of methods, even though their implementation is different. This is a clue that we can create a higher level class (an interface or abstract class) to guide how a concrete class should be implemented. By making this change, we'll introduce polymorphism into our application which allows us to program the application to a single standard without knowing or caring what the actual implementation looks like. SEE LISTING #4. Notice that we've also moved the DAO creation code outside of the Preferences class. By doing so, we can now allow the application itself (or the testing code) to choose which persistence mechanism to use. Note here that we could take this one step further by using a Factory Pattern and allow the DAO creation to happen at runtime instead of compile time as it is now, but that's a matter for another article.
We've managed to pull all the persistence code outside of the Preferences class, and we can now write our unit tests to test the Preferences class in an extremely controlled manner. This is exactly what we want to do, but we also realize another benefit from this refactor. If we ever change our persistence mechanism, all we have to do write a new concrete class which implements the DAO interface and swap out the DAO object creation code. A real world example of why this might happen is if we moved our data access from directly accessing our SQL database to requiring the access to go through a SOAP- or REST-based web service.
I't ve got some serious problems with this approach. Where's your actual unit test and framework?
I't ve got some serious problems with this approach
Thats not very descriptive. What are your serious problems. That unit testing has proven to provide higher quality (less bugs and refactoring) code that not unit testing? I think this example is great!
You're in Easy Mode. If you prefer, you can use XHTML Mode instead. |