You are here

SOLID principles of object-oriented design are an important consideration for anyone looking for good software design. The problem is they can be hard to understand and implement. In my personal experience unit-testing can actually help understanding these principles better and provide a genuine use case for implementing them.

When you first read about them, SOLID principles may make sense to you, however you have to really understand them well to be able to implement them in your own code. Instead, start thinking about unit-testing your code and you will in many ways be urged to honour the SOLID principles, even if you are not aware of consciously doing so.

Dependency Inversion

A dependency can be anything like a database, the system clock, a web-service, or even a method in a different class. Unit-testing code presents interesting challenges and forces you to think about how you can isolate the bit you want to test from other code it interacts with-- its dependencies. In order to successfully unit-test your classes, you will need to be able to move dependencies from your code to one place, usually the constructor, and make them replaceable. The idea behind this is that then in your test harness you can substitute away those dependencies for dummy values or dummy providers and invoke code under test without hitting the real external dependency. So in order to isolate your code under test from other code that effects its behaviour, you are forced to firstly think about what other dependencies it has and isolate them and secondly think how those dependencies can be replaced during unit-testing. What you are doing is Dependency Inversion, and it is much easier to do Dependency Inversion when thinking about how you can unit test your code rather doing it just because you read it in a book.

Interface Segregation

Once you have identified and isolated dependencies, the next thing you have to do in your test code is to somehow mock/stub/fake the dependencies, so you can substitute them in your code under test in place of the real dependency. This is usually a challenging part of unit testing and ideally you want to mock as little as possible, enough so that the code under test will run correctly. If you find yourself needing to mock a large interface or a complex interaction of methods you should think about the design of those dependencies and about splitting those interfaces or interactions into smaller chunks, so that you can mock only the chunk that concerns your code under test right now. What you are indirectly forced to do is to think about the interfaces provided by the dependencies and how your code under test uses them, then split each interface so that it makes logical sense to mock it for your code under test. That is Interface Segregation for your dependencies. Not only are you thinking about improving the code under test, you are also improving interfaces provided by its external dependencies, so that they are easier to mock. It is far easier to design interfaces when you have a client using it, then to imagine a design for an interface your client may probably want to use. A badly designed interface will make mocking difficult or will need several different mocks depending on how it is being used.

Single Responsibility

Most test harnesses allow a setup and teardown operation where you setup the dependencies before actual testing begins and cleanup afterwards. If you find yourself filling up these methods with lots of unrelated dependencies, for example, you find that your class depends on the database, logger, email service, soap client interface, and everything else under the sun and you find you are spending a lot of time setting all of these up in your test and its giving you a headache, this is a clear warning that your code under test is actually violating the Single Responsibility principle. When your code violates this principle, your class loses focus and starts doing several often unrelated operations. Should your business entity class represent just the entity? should it also save and load it from the database? should it also have methods that enable the report view to display aggregated data?

If the test setup smells, most probably the code does too. The harder it is to unit test a piece of code, the greater the chances that SOLID principles are being violated.

Your unit-test code tries to keep up with this bloat and in the process becomes increasingly complex as you try and satisfy the various dependencies (database, report view, aggregator) your code under test needs. What is more, some tests use a certain dependency while for others it is just dead-weight. In this example, the database code does not care about the aggregator, but you have to mock it nevertheless. The aggregator needs just the entity but you still find yourself needing to mock the database. Unit-testing starts to feel harder and often it is time to take a step back and think if you would do better splitting the code under tests into several classes, each handling some aspect of the larger operation. Then the unit-tests would also be split and each unit-test will have far fewer dependencies and will become easy to setup and teardown again.

Open/Closed

Once your class has been thoroughly unit-tested, you have indirectly closed it for modification. Any change to the internal logic of your class will result in a unit-test failure. This discourages people from changing your working code. Internal logic changes are of course required if fixing a bug, but I am not talking about bug-fixing here. Often someone who cleverly changes the logic in your class to make it do something extra will cause a test failure and will have to rethink your test or revert their change. So due to unit-tests extra effort is required to change behaviour and most good programmers will intuitively explore ways of avoiding that extra effort. And often the correct way to do that is to think how the class can be extended or enclosed within another class where the new functionality can live without breaking existing unit-tests. Good unit-tests enforce behaviour, and change in behaviour will break tests causing more rework for the programmer who breaks them. Unit tests encourage you to explore ways of using your class without breaking unit-tests.

Liskov Substitution Principle

This can often be one of the hardest to understand from SOLID principles, and really only applies when using inheritance. Generally, when you can successfully substitute a mock object for the real one, you are in a way following this principle, because your code under test can't tell the difference. It does not care that the real object has been substituted by a mock, it still uses the dependency in exactly the same way as before. Another way to put it is that if you find you are having to tell your code under test that it is running in "test mode" and doing something special in your code under test when in test mode you are very likely violating this principle. Specifically, when testing inheritance, if your derived class object can still pass all of the base class tests you have honoured this principle. If it cannot, you probably should not be using inheritance.

Conclusion

The SOLID principles are harder to implement without cause. Unit testing provides a genuine need for implementing and following these principles. So the next time you are wondering if your code follows the SOLID principles of design, take a closer look at its unit tests and how they are setup. If the test setup smells, most probably the code does too. The harder it is to unit test a piece of code, the greater the chances that SOLID principles are being violated. Their effect is additive and the more of these principles you violate, the more complex it becomes to write successful unit tests.