A major challenge associated with legacy modernization is testing legacy code associated with the outdated application....
Long, multi-purpose code makes isolating logic difficult, especially code connected to database access.
In this tip, I discuss how tight coupling with database access can cause frustrating issues when testing legacy code and explain how adding a data access layer into your existing code can eliminate these testing issues. Learn how to refactor a repository pattern, discover how to add unit tests to those methods, and find suggestions for working with older code.
Testing legacy applications proves challenging
Automated testing of code in legacy applications is notoriously bad. It is common to encounter legacy systems where methods are extremely long and classes, if they exist, are multipurpose. This makes it challenging to write tests because it is so difficult to isolate the logic in the methods.
A common contributor to these complications is a tight coupling with the database access. When the database is queried directly from inside a method, the testing complications flow down to the database where certain data scenarios must be maintained in a test database in order to write the few tests that are possible. When a test begins to fail, the first thing you must check is that someone else didn’t mess up the data scenario built specifically for that test. Injecting a data access layer (DAL) into existing code can eliminate the problem of requiring a static test database. A DAL is a class whose sole responsibility is interacting with the database. All queries, whether a select, update or delete, should go through this layer. When you have a DAL in place, testing legacy code becomes much easier because you can use a mocking framework to mimic the database interaction, thus "pretending" to be connected to an actual database. This mock can return any data set required, even an empty one, in order to test your application sufficiently.
How to refactor to a repository pattern
My personal favorite data access layer pattern is the repository pattern. This pattern defines methods to retrieve certain pieces of data from the database, and return that data to the caller. It is a good encapsulation of the grimy details of data access, enabling the calling code to be much cleaner and easier to read.
Refactoring to the repository pattern is simple, probably time consuming, and always tedious. The process begins by creating a new empty class and associated interface. Find an appropriate place in your code where you are accessing the database directly, then move that code into a separate method that is still in the same class. Test this to ensure everything is working; this small change should not have affected the business logic at all.
When creating this method, make sure to convert queries into parameterized queries, and pass in any necessary parameters. This will make the method more generic and allow you to use it in other places once it is working correctly.
Now move that method into your new class and make the proper changes to the interface. Creating and implementing this first method will bring in various structural considerations. Your new repository will need to get a connection string, handle database connections properly, implement proper error handling, etc. If you are using an Inversion of Control container, you should inject the repository on the constructor line.
Add unit tests by creating a mock
Now that the repository is in place, you can add unit testing of that method by creating a mock of the repository. A mock is a type of Test Double that has pre-defined, hard-coded actions to enable easy testing. Using a mocking framework -- Moq from Google is a good, free framework -- you define a method that replaces the one you created above, and hard-code it to return an appropriate dataset.
For example, assume the method created was called GetCustomerData and returned a CustomerData class. Using mocks, you can redefine this method at run-time to return a CustomerData class that was created specifically for the test. This class can contain valid data, bad data or even be non-existent (i.e., the method returns nothing). By changing what gets returned, you can test different scenarios in the method to see if they are handled properly.
Suggestions for working with older code
If you are working with older code, I have a few other suggestions. The legacy code is probably using ADO or some other low-level data access technology. In this case, your new repository method will be returning the same type of object that the original code was using -- some sort of row-set, not a business object. As you go through your application refactoring, you will eventually want use business classes instead of the raw data set.
When refactoring to business objects, create another repository for this instead of refactoring the new repository. The new business object repository will use the row-set repository for data access then map the resulting rows to business objects before returning them. This makes the process more manageable because if you were to convert the row-set repository to business objects, it would force conversion of all methods using the converted repo to also be converted, resulting in work that you may not be ready for.
If you have a mix of older and new technology in your application, such as ASP Classic and ASP.NET, you can put your new repository into a COM object for access by the ASP Classic side; this way you only maintain a single code line for all database access.
Find answers and ask questions about software testing in our IT Knowledge Exchange
Learn about key database tools for testing
What are the best cloud application testing techniques?
Is BPM a viable solution for app modernization?