Today I want to discuss applications testability and how it refers to Object-Oriented design.
First of all, testability includes two concepts: controllability and visibility. Controllability determines the work it takes to set up and run test cases and the extent to which individual functions and features of the module under the test can be made to respond to test cases. In other words, controllability is responsible for how hard it is to run tests under some class or methods. Visibility – the ability to observe states and outputs (what we see and what wee test) or if class/methods under tests is enough visible to make it possible to predict result and achieve some response from it.
So, question about code testability refers to application architecture. When you break main Object-oriented design principles, create tight coupled structures or program for implementation not for an interface, you make your code less testable. And this is the “saturation point” when unit tests creation takes much more time than functionality itself.
Accordingly, let’s now consider main points which do your code less testable and therefore less flexible and immobile.
1. Don’t work in Constructor
Simply do not do any bussiness work here. It’s a very-very bad practice.
Firstly, code which is located in Constructor almost untestable. Of course, using mocks, reflections you can check if some methods do what they need to. But it’s not possible in any case. Let’s see an example of putting bussiness logic in costructor:
class Document { private $html; public function __construct($url) { $client = new HtmlClient(); $this->html = $client->get($url); } }
How would you test this? Putting new operator in Constructor is a bad practice. You have no choice to avoid calling methods in Constuctor , at least, try to exclude new operator doing like this:
class Document { private $html; public function __construct(HtmlClient $client, $url) { $this->html = $client->get($url); } }
This code is testable, but at the same time it’s a little bizarre when our Document class is aware about how to create or how to get $html object. Our course, the best option is to use injection:
class Document { private $html; public function __construct(Html $html) { $this->html = $html; } }
Secondly and more important, every time when you run tests for each method of class with code in Constructor you need to mock all functionality inside Constructor. Every time you need do extra work. And the biggest problem occur when Constructor contains initialization code with Singleton. Using global state in Constructor in some cases make all class untestable at all.
2. Separate objects creation and consider new operator like harmful
There is no bad in new operator. But initiating objects within your business logic you tight them and make methods closed to injecting test data. If no way to put test data in method than no way to check method’s behavior. It’s simply. Use dependency injection or creational patterns for separation creational logic from other parts of your application.
3. Try to avoid using of global states, particularly, Singletons
Using of global state leads to the following problems:
– multiple tests execution may produce different results;
– order of tests matter;
– can not run test in parallel
All these problems refer to the point that global object using by different parts of application and all of them are able to change its state. To familiarize with prons and cons of Singleton pattern read – Singleton pattern: light or dark side of the Force.
4. Law of Demeter violation
Law of Demeter violation causes visual disappearance of method’s complication. You don’t see how you combine multiple methods in one.
Law of Demeter, also known as a principle of least knowledge is a coding principle, which says that a module should not know about the inner details of the objects it manipulates. If a code depends upon internal details of a particular object, there is good chance that it will break as soon as internal of that object changes.
On surface breaking Law of Demeter looks pretty good, but as soon as you think about principle of least knowledge, you start seeing the real picture of tight coupling.
Also, Law of Demeter violation is a bad thing for testability. In the following example, we will see it:
class Goods { private $accountReceiver; public function makePurchase(Customer $customer) { $money = $customer->getWallet()->getMoney(); $this->accountReceiver->recordSale($this, $money); } }
In the example above to make a purchase we have injected Customer object and got the Wallet object, and after that from Wallet object we got the Money object. Now, look at unit test for this simple method:
public function testMakePurchaseWhenItBreaksLoD() { $accountReceiver = $this->createMock(AccountReceiver::class); $goods = new Goods($accountReceiver); $money = new Money(100, Currency::USD); $password = PassMaker::getBcryptPassword(); $user = new User('testName', $password, 'test@gmail.com'); $wallet = new Wallet($user, $money); $customer = new Customer($wallet); $goods->makePurchase($customer); $this->assert... }
To create a customer we need to initialize a bunch of different objects around it. The worse option occurs when this dependencies also have a lot of object dependencies. In that case we have to initialize “the whole” application for one simple test or mock a large range of dependencies.
On the other hand, really, in test method we need a Money object, not a Customer object. So, remaking test method this way, we get better testability:
class Goods { private $accountReceiver; public function makePurchase(Money $money) { $this->accountReceiver->recordSale($this, $money); } }
public function testMakePurchaseWhenItBreaksLoD() { $accountReceiver = $this->createMock(AccountReceiver::class); $goods = new Goods($accountReceiver); $money = new Money(100, Currency::USD); $goods->makePurchase($money); $this->assert... }
As you can see, this unit test looks better and easier to implement.
These were the basic principles that you must adhere to. Be faithful to them and testing will be a pleasant deal for you!