@ediar asked me on Twitter if I still think a constructor should not be tested. It depends on the type of object you’re working with, so I think it’ll be useful to elaborate here.
Would you test the constructor of a service that just gets some dependencies injected? No. You’ll test the behavior of the service by calling one of its public methods. The injected dependencies are collaborating services and the service as a whole won’t work if anything went wrong in the constructor.
Would you test the constructor of a DTO? No. A DTO is by definition a simple object with no behavior, which allows you to pass data in a structured way. The data will be copied to the DTO and used in some other place, so when something goes wrong with assigning values to properties, you will know it when you test the service that uses the DTO.
Would you test the constructor of an entity? Yes, but in very specific ways, none of which is this one:
public function testConstructor(): void
{ $user = new User('Name', 'M'); self::assertSame('Name', $user->getName()); self::assertSame('M', $user->getGender());
}
(“Why do we need to know the user’s gender?” Great question. We don’t. We’ll get back to it.)
I like to call a test like this: “you can take out what you put in”, or: “it assigns the values to the right properties”. It’s a bad test and I’ll explain why.
Common constructor problems will be caught by your static analyzer
If you want to make sure the provided constructor arguments end up being assigned to the right properties, then just rely on static analyzers like PHPStan or Psalm (or even PhpStorm). They will point out that something is wrong, e.g. when you assign a value of the wrong type, or you rely on it to be non-optional, and so on. You don’t need to test these low-level things about your code.
Exposing state breaks encapsulation
If you still want to verify that a constructor assigns the right values to the right properties, then you either have to use reflection to peek into the private properties of the object, or you have to add public getters to the object. Both options break encapsulation. The object can no longer keep its internal data structures and inner workings to itself.
If you choose reflection, the test becomes fragile: it breaks whenever you change anything about the properties.
If you choose getters, you end up with methods that are initially only introduced for testing purposes, but eventually clients will start using those methods and relying on them, even if they weren’t meant to have access to all those values. These getters are not intention-revealing methods anyway, which causes the entity to have a very confusing API with too many public methods that don’t serve any real use case, except testing.
The test doesn’t explain why you need the property assignments
Even if you would allow breaking encapsulation by exposing too much state, the constructor test is still a bad test. It doesn’t answer why you need those property assignments. Do you need them so the getter can return the assigned value? That would be a circular argument. Take the constructor test away and you don’t need them anymore.
Asking “why” means we have to look at a higher abstraction level. Why do you need that property assignment?
- Because that property will be mapped to a database column and you need that data to be in your database.
This answer needs some further soul searching. Why do you need this value to be in your database?
- Because you want to select the value and show it on some page.
- Because a legacy system relies on that particular value to be in that particular column.
- Because one of the stakeholders has told you to collect and save that data.
Replace the constructor unit test with some higher-level test
The rather unhelpful unit test that we have for the constructor doesn’t mention any of these reasons. Which is how knowledge about a system gets lost over time. Codifying all the reasons as tests, test descriptions, or possibly just comments in tests, would be a great way to preserve knowledge. It also helps us refrain from testing the implementation details of a class. Instead, we’ll be focussing on the behavior of the system as a whole, as we should. For example, providing a high-level test for reason 1:
Given the user registers themself as "Matthias
Truncated by Planet PHP, read more at the original (another 3686 bytes)