60

Most of the projects that I work on consider development and unit testing in isolation which makes writing unit tests at a later instance a nightmare. My objective is to keep testing in mind during the high level and low level design phases itself.

I want to know if there are any well defined design principles that promote testable code. One such principle that I have come to understand recently is Dependency Inversion through Dependency injection and Inversion of Control.

I have read that there is something known as SOLID. I want to understand if following the SOLID principles indirectly results in code that is easily testable? If not, are there any well-defined design principles that promote testable code?

I am aware that there is something known as Test Driven Development. Although, I am more interested in designing code with testing in mind during the design phase itself rather than driving design through tests. I hope this makes sense.

One more question related to this topic is whether it's alright to re-factor an existing product/project and make changes to code and design for the purpose of being able to write a unit test case for each module?

3
  • 4
    Look at this: googletesting.blogspot.in/2008/08/…
    – S2S2
    Commented Jun 19, 2012 at 6:10
  • Thank you. I only just started reading the article and it already makes sense.
    – bot
    Commented Jun 19, 2012 at 6:27
  • 2
    This is one of my interview questions ("How do you design code to be easily unit tested?"). It single-handidly shows me if they understand unit testing, mocking/stubbing, OOD, and potentially TDD. Sadly, the answers usually are something like "Make a test database". Commented Jun 19, 2012 at 23:51

5 Answers 5

63

Yes, SOLID is a very good way to design code that can be easily tested. As a short primer:

S - Single Responsibility Principle: An object should do exactly one thing, and should be the only object in the codebase that does that one thing. For instance, take a domain class, say an Invoice. The Invoice class should represent the data structure and business rules of an invoice as used in the system. It should be the only class that represents an invoice in the codebase. This can be further broken down to say that a method should have one purpose and should be the only method in the codebase that meets this need.

By following this principle, you increase the testability of your design by decreasing the number of tests you have to write that test the same functionality on different objects, and you also typically end up with smaller pieces of functionality that are easier to test in isolation.

O - Open/Closed Principle: A class should be open to extension, but closed to change. Once an object exists and works correctly, ideally there should be no need to go back into that object to make changes that add new functionality. Instead, the object should be extended, either by deriving it or by plugging new or different dependency implementations into it, to provide that new functionality. This avoids regression; you can introduce the new functionality when and where it is needed, without changing the behavior of the object as it is already used elsewhere.

By adhering to this principle, you generally increase the code's ability to tolerate "mocks", and you also avoid having to rewrite tests to anticipate new behavior; all existing tests for an object should still work on the un-extended implementation, while new tests for new functionality using the extended implementation should also work.

L - Liskov Substitution Principle: A class A, dependent upon class B, should be able to use any X:B without knowing the difference. This basically means that anything you use as a dependency should have similar behavior as seen by the dependent class. As a short example, say you have an IWriter interface that exposes Write(string), which is implemented by ConsoleWriter. Now you have to write to a file instead, so you create FileWriter. In doing so, you must make sure that FileWriter can be used the same way ConsoleWriter did (meaning that the only way the dependent can interact with it is by calling Write(string)), and so additional information that FileWriter may need to do that job (like the path and file to write to) must be provided from somewhere else than the dependent.

This is huge for writing testable code, because a design that conforms to the LSP can have a "mocked" object substituted for the real thing at any point without changing expected behavior, allowing for small pieces of code to be tested in isolation with the confidence that the system will then work with the real objects plugged in.

I - Interface Segregation Principle: An interface should have as few methods as is feasible to provide the functionality of the role defined by the interface. Simply put, more smaller interfaces are better than fewer larger interfaces. This is because a large interface has more reasons to change, and causes more changes elsewhere in the codebase that may not be necessary.

Adherence to ISP improves testability by reducing the complexity of systems under test and of dependencies of those SUTs. If the object you are testing depends on an interface IDoThreeThings which exposes DoOne(), DoTwo() and DoThree(), you must mock an object that implements all three methods even if the object only uses the DoTwo method. But, if the object depends only on IDoTwo (which exposes only DoTwo), you can more easily mock an object that has that one method.

D - Dependency Inversion Principle: Concretions and abstractions should never depend on other concretions, but on abstractions. This principle directly enforces the tenet of loose coupling. An object should never have to know what an object IS; it should instead care what an object DOES. So, the use of interfaces and/or abstract base classes is always to be preferred over the use of concrete implementations when defining properties and parameters of an object or method. That allows you to swap one implementation for another without having to change the usage (if you also follow LSP, which goes hand in hand with DIP).

Again, this is huge for testability, as it allows you, once again, to inject a mock implementation of a dependency instead of a "production" implementation into your object being tested, while still testing the object in the exact form it will have while in production. This is key to unit testing "in isolation".

16

I have read that there is something known as SOLID. I want to understand if following the SOLID principles indirectly results in code that is easily testable?

If applied correctly, yes. There's blog post by Jeff explaining SOLID principles in a really short way (mentioned podcast is worth listening too), I suggest giving a look there if longer descriptions are throwing you off.

From my experience, 2 principles from SOLID play major role in designing testable code:

  • Interface segregation principle - you should prefer many, client-specific interfaces instead of fewer, general-purpose ones. This goes in pair with Single Responsibility Principle and helps you design feature/task-oriented classes, which in return are much easier to test (as compared to the more general ones, or often abused "managers" and "contexts") - less dependencies, less complexity, more fine-grained, obvious tests. In short, small components lead to simple tests.
  • Dependency inversion principle - design by contract, not by implementation. This will benefit you the most when testing complex objects and realizing you don't need whole graph of dependencies just to set it up, but you can simply mock the interface and be done with it.

I believe these two will aid you the most when designing for testability. Remaining ones also have an impact, but I'd say not as large.

(...) whether it's alright to re-factor an existing product/project and make changes to code and design for the purpose of being able to write a unit test case for each module?

Without existing unit tests, it is simply put - asking for troubles. Unit test is your guarantee that your code works. Introducing breaking change is spotted immediately if you have proper tests coverage.

Now, if you want to change existing code in order to add unit tests, this introduces a gap where you don't have tests yet, but do have changed code already. Naturally, you might not have a clue what your changes broke. This is situation you want to avoid.

Unit tests are worth writing anyways, even against code that is difficult to test. If your code is working, but is not unit tested, appropriate solution would be to write tests for it and then introduce changes. However, note that changing tested code in order to make it more easily testable is something your management might not want to spend money on (you'll probably hear it brings little to no business value).

1
  • iaw high cohesion and low coupling
    – jk.
    Commented Jun 19, 2012 at 12:37
8

YOUR FIRST QUESTION:

SOLID is indeed the way to go. I find that the two most important aspect of the SOLID acronym, when it comes to testability, is the S (Single Responsibility) and the D (Dependency Injection).

Single Responsibility: Your classes should really only be doing one thing, and one thing only. a class that creates a file, parses some input, and writes it to the file is already doing three things. If your class only does one thing, you know exactly what to expect of it, and designing the test cases for that should be fairly easy.

Dependency Injection (DI): This gives you control of the testing environment. Instead of creating forreign objects inside your code, you inject it through the class constructor or the method call. When unittesting, you simply replace real classes by stubs or mocks, that you control entirely.

YOUR SECOND QUESTION: Ideally, you write tests that document the functioning of your code before refactoring it. This way, you can document that your refactoring reproduces the same results as the original code. However, your problem is that the functioning code is hard to test. This is a classic situation! My advice is: Think carefully about refactoring before unit testing. If you can; write tests for the working code, then refactor the code, and then refactor the tests. I know it will cost hours, but you will be more certain, that the refactored code does the same as the old. Having said that, I have given up a lot of times. Classes can be so ugly and messy that a rewrite is the only way to make them testable.

6

In addition to the other answers, which focus on achieving a loose coupling, I'd like to say a word about testing complicated logic.

I once had to unit-test a class whose logic was intricate, with lots of conditionals, and where it was hard to understand the role of the fields.

I replaced this code with many small classes that represent a state machine. The logic became much simpler to follow, since the different states of the former class became explicit. Each state class were independent of the others, and so they were easily testable.

The fact that the states were explicit made it easier to enumerate all the possible paths of the code (the state transitions), and thus to write a unit-test for each one.

Of course, not every complex logic can be modeled as a state machine.

3

SOLID is an excellent start, in my experience, four of the aspects of SOLID really work well with unit testing.

  • Single Responsibility Principle - each class does one thing and one thing only. Calculating a value, opening a file, parsing a string, whatever. The amount of inputs and outputs, as well as decision points should therefore be very minimal. Which makes it easy to write tests.
  • Liskov substitution principle - you should be able to substitute in stubs and mocks without altering the desirable properties (the expected results) of your code.
  • Interface segregation principle - separating contact points by interfaces makes it very easy to use a mocking framework such as Moq to create stubs and mocks. Instead of having to rely on the concrete classes you are simply relying on something that implements the interface.
  • Dependency Injection Principle - This is what allows you to inject those stubs and mocks into your code either through a constructor, a property or a parameter in the method you want to test.

I would also look into different patterns, especially the factory pattern. Let's say you have a concrete class that implements an interface. You would create a factory to instantiate the concrete class, but return the interface instead.

public interface ISomeInterface
{
    int GetValue();
}  

public class SomeClass : ISomeInterface
{
    public int GetValue()
    {
         return 1;
    }
}

public interface ISomeOtherInterface
{
    bool IsSuccess();
}

public class SomeOtherClass : ISomeOtherInterface
{
     private ISomeInterface m_SomeInterface;

     public SomeOtherClass(ISomeInterface someInterface)
     {
          m_SomeInterface = someInterface;
     }

     public bool IsSuccess()
     {
          return m_SomeInterface.GetValue() == 1;
     }
}

public class SomeFactory
{
     public virtual ISomeInterface GetSomeInterface()
     {
          return new SomeClass();
     }

     public virtual ISomeOtherInterface GetSomeOtherInterface()
     {
          ISomeInterface someInterface = GetSomeInterface();

          return new SomeOtherClass(someInterface);
     }
}

In your tests you can Moq or some other mocking framework to override that virtual method and return an interface of your design. But as far as the implementing code is concerned the factory has not changed. You can also hide a lot of your implementation details this way, your implementing code doesn't care how the interface is built, all it cares about is getting an interface back.

If you want to expand on this a bit, I highly recommend reading The Art of Unit Testing. It gives some great examples on how to use this principles, and it is a pretty quick read.

1
  • 1
    It's called the dependency "inversion" principle, not "injection" principle. Commented Jan 15, 2016 at 16:53

Not the answer you're looking for? Browse other questions tagged or ask your own question.