w0051977
w0051977

Reputation: 15787

Unit Testing concrete classes

I have inherited a project that has no interfaces or abstract classes i.e. concrete classes only and I want to introduce unit testing. The classes contain lots of functions, which contain business logic and data logic; breaking every rule of SOLID (http://en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29).

I had a thought. I was thinking about creating interfaces for each of the poorly designed classes, exposing all functions. Then at least I can Mock the classes.

I am relatively new to Unit Testing (I have experience with a project, which was very well developed using interfaces in the right places). Is it a good idea to do this i.e. create interfaces for all the concrete classes (exposing all the functions and sub routines), just for unit testing?

I have spent some time researching this but I have not found an answer.

Upvotes: 5

Views: 1443

Answers (6)

Steven Doggart
Steven Doggart

Reputation: 43743

Yes, that is a good start, however, having interfaces is less of a priority than having dependencies injected. If all of your legacy classes gain interfaces, but hidden internally they are still all interdependent, the classes will still be no easier to test. For instance, 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 saying... "I wish the legacy code was at least layered that well", but lets use that as an example of some legacy code which would be hard to test. The reason it's hard to test is because the code reaches out to the database and executes a time-consuming query on the database and then calculates the results from that. So, in order to test it in its current state, you would need to first write out a bunch of test data to the database, then run the code to see if it returns the correct results based on that inserted data. Having to write a test like that is problematic because:

  • It is a pain to write the code to setup the test
  • The test will be brittle because it depends on the outside database working properly and on it containing all the correct supporting data
  • The test will take too long to run

As you correctly observe, interfaces are very important to unit testing. So, as you recommend, lets add interfaces to see if it makes it any easier to test:

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 that make it any easier to test? Now we can easily create a mock data access object, which implements the same interface, but that's not really the core problem. The problem is, how do we get the business object to use that mock data access object instead of the real one? To do that, you need to take your refactoring to the next level by introducing dependency-injection. The real culprit is the New keyword in the following line of the business class:

Dim dataAccess As New LegacyDataAccess()

The business class clearly depends on the data access class, but currently it is hiding that fact. It's lying about it's dependencies. It's saying, come-on, it's easy, just call this method and I'll return the result--that's all it takes. When really, it takes a lot more than that. Now, let's say we stopped it from lying about it's dependencies and made it so it unabashedly stated them, like this:

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 a mock data access object, but now we can easily inject the mock data access object into the business object. Now we can create a mock which quickly and easily returns exactly the data we want it to return and then see if the business class returns the correct calculation--no database involved.

Unfortunately, while adding interfaces to existing classes is a breeze, refactoring them to use dependency-injection typically requires a lot more work. You will likely need to plan out which classes make the most sense to tackle first. You may need to create some intermediary old-school wrappers which work the way the code used to, so you don't break existing code while you are in the process of refactoring the code. It's not a quick and easy thing, but if you are patient and in it for the long-haul, it is possible to do it, and you will be glad you did.

Upvotes: 2

Icaro Bombonato
Icaro Bombonato

Reputation: 4162

I prefer to create interfaces and classes as you need to test things and not all upfront.

Besides interfaces, you can use some techniques to test legacy code. The one I often use is "Extract And Override", where you extract some piece off "untestable" code inside other method and make it overridable. Them derive the class that you want to test and override the "untestable" method with some sensing code.

Using a mock framework will be as easy as adding keyword Overridable to the method and sets the returning using the mock framework.

You can find many techniques on the book "Working Effectively with Legacy Code".

One thing about existing code, is that sometimes it is better to write integration tests than unit tests. And after you have the behavior under test, you create unit tests.

Another tip is to start with modules/class that have less dependencies, that way, you become familiar with the code with less pain.

Let me know if you need an example about "extract and override" ;)

Upvotes: 0

jsanchez
jsanchez

Reputation: 325

If your project has no tests at all, before adding any unit tests I'd much rather create higher level tests (i.e acceptance, functional and/or integration tests).

When you have those tests in place you know that the system is behaving as it should and also that it has certain level of 'external' quality (meaning by this that the inputs and outputs of your program are the expected ones).

Once your high level tests are working, you could try to add unit tests to the classes that already exist.

I bet that you will find yourself in the need to refactor some of the existing classes if you want to be able to unit test them so you can use your high level tests as a safety net that will tell you if you've broken anything.

Upvotes: 5

Matt
Matt

Reputation: 14531

This is a tough thing to tackle. I think you are on the right track. You'll end up with some ugly code (such as creating header interfaces for each monolithic class), but that should just be an intermediate step.

I'd suggest investing in a copy of Working Effectively with Legacy Code. First you could start by reading this distillation.

In addition to Karl's options (which let you mock via interception), you could also use Microsoft Fakes & Stubs. But these tools will not encourage you to refactor the code to adhere to SOLID principles.

Upvotes: 3

Surveon
Surveon

Reputation: 723

Creating interfaces to test the classes is not a bad idea - the goal of unit testing is to exercise if the functions on a class are functioning as expected. Depending on the classes you are working with, this could be easier said than done - if there are a lot of dependencies on global states, etc. you will need to mock accordingly.

Given how valuable unit tests are, putting a bit of work into them (to a limit) will benefit you and developers you work with.

Upvotes: 0

Karl Anderson
Karl Anderson

Reputation: 34846

I would recommend you go the interface route, but if you want to pay for a solution, then try one of these:

Upvotes: 1

Related Questions