Yes, this is a good start, however, the presence of interfaces is less priority than nested dependencies. If all your obsolete classes get interfaces, but are hidden inside, they are still interdependent, classes will still not be easier to test. For example, let's say you had two classes that looked like this:
Public Class LegacyDataAccess Public Function GetAllSales() As List(Of SaleDto) ' Do work with takes a long time to run against real DB End Function End Class Public Class LegacyBusiness Public Function GetTotalSales() As Integer Dim dataAccess As New LegacyDataAccess() Dim sales As List(Of SaleDto) = dataAccess.GetAllSales() ' Calculate total sales End Function End Class
I know what you're already talking about ... "I would like the old code to be at least layered," but it can be used as an example of some outdated code that will be difficult to verify. The reason this is difficult to verify is because the code accesses the database and performs a laborious query on the database, and then calculates the results. So, to test it in its current state, you need to first write test data to the database, and then run the code to see if it will return the correct results based on this inserted data. The requirement to write such a test is problematic because:
- It's a pain to write code to set up a test
- The test will be fragile because it depends on the normal operation of the external database and contains all the correct supporting data.
- The test will take too long.
As you rightly noted, interfaces are very important for unit testing. Therefore, as you recommend, add interfaces to check if this makes testing easier:
Public Interface ILegacyDataAccess Function GetAllSales() As List(Of SaleDto) End Interface Public Interface ILegacyBusiness Function GetTotalSales() As Integer End Interface Public Class LegacyDataAccess Implements ILegacyDataAccess Public Function GetAllSales() As List(Of SaleDto) _ Implements ILegacyDataAccess.GetAllSales ' Do work with takes a long time to run against real DB End Function End Class Public Class LegacyBusiness Implements ILegacyBusiness Public Function GetTotalSales() As Integer _ Implements ILegacyBusiness.GetTotalSales Dim dataAccess As New LegacyDataAccess() Dim sales As List(Of SaleDto) = dataAccess.GetAllSales() ' Calculate total sales End Function End Class
So now we have the interfaces, but really, how does this make testing easier? Now we can easily create a mock data access object that implements the same interface, but this is not the main problem. The problem is, how can we get a business object to use this mock data access object instead of the real one? To do this, you need to take your refactoring to the next level by introducing dependency injection. The real culprit is the key word New in the next row of business class:
Dim dataAccess As New LegacyDataAccess()
The business class clearly depends on the data access class, but it currently hides this fact. He lies about this addiction. He says come easy, just call this method and I will return the result - all that is required. When this is true, it requires much more. Now let's say we stopped him from lying about these dependencies and made him shamelessly expound them, for example:
Public Class LegacyBusiness Implements ILegacyBusiness Public Sub New(dataAccess As ILegacyDataAccess) _dataAccess = dataAccess End Sub Private _dataAccess As ILegacyDataAccess Public Function GetTotalSales() As Integer _ Implements ILegacyBusiness.GetTotalSales Dim sales As List(Of SaleDto) = _dataAccess.GetAllSales() ' Calculate total sales End Function End Class
Now, as you can see, this class is much easier to test. Not only can we easily create an object to access data, but now we can easily enter a data access object into a business object. Now we can create a layout that quickly and easily returns exactly the data that we want to return, and then see if the business class returns the correct calculation - there was no database.
Unfortunately, adding interfaces to existing classes is a breeze, refactoring them to use dependency injection usually requires much more work. Most likely, you will have to plan which classes are most suitable for the solution in the first place. You may need to create some old-school intermediate shells that work the way code is used, so you don't break existing code while you're in the process of refactoring the code. It is not fast and easy, but if you are patient and in it for long trips, you can do it and you will be glad that you did it.