Edit: After reading one of the comments, I realized I misspoke pretty badly in the original post, I've made some edits to clarify. Essentially, I need to check the results but am struggling with that in traits.
I usually use dependency injection (DI), but want to get better with traits since they seem like they can fulfill a similar role while still using composition instead of inheritance. The issue I'm running into is I cannot figure out the best way to make sure that the code under test (CUT) is working as expected when using a trait.
The issue is I'm a bit stuck with how to do this with traits without adding extra code to the actual implementation of the trait.
For example, let's pretend we have a trait:
trait CanLog
{
protected function log($level, $message)
{
// implementation goes here
}
}
class ClassThatWillLog
{
use CanLog;
public function foobar()
{
// do some stuff
$this->log("INFO", "I finished successfully!");
}
}
I want to assert that the log contains some specially formatted INFO level message that also has "I finished successfully". The problem is that I need to inherently make this into an integration test (and check the place the actual CanLog implementation logs to) because I can't currently substitute in the method by which the CanLog trait logs... If that makes sense.
When doing DI, I create an interface and then a test double that implements the interface/contract. You can then check that the fake implementation contains the data it should during the assertion phase. The above could easily be tested using the below:
class FakeLogger implements LoggerInterface
{
private array $log = [];
protected function log($level, $message)
{
$this->log[] = "$level: $message\n";
}
public function getLogMessages(): array
{
return $this->log;
}
}
class ClassThatWillLog
{
private LoggerInterface $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function foobar()
{
// do some stuff
$this->logger->log("INFO", "I finished successfully!");
}
}
I could create a similar thing in CanLog as I did in FakeLogger but that feels wrong and adds code that is completely unnecessary for production. I'm just not sure how to replicate the DI equivalent when using a trait instead.
Traits are "compiler-assisted copy and paste" - once the class is compiled (yes, PHP has a compile stage, don't let anyone tell you otherwise), the code of ClassThatWillLog
contains the log
method directly in the body. The fact that it came from a trait is completely invisible.
Since it's a protected method, you can override it on child instance, but there's nothing you can do to "recompile" the class with a different trait.
This makes traits a poor choice for this kind of dependency; using an interface and injection is the easy to go.
Traits are useful in the rarer cases where you know that multiple classes will want the same code, but you don't want any relationship between them and you're sure that code will never need to be substituted by a different implementation.