I am aware that one can add custom assertion by extending the default TestCase
, e.g. I added assertions to check that an array may only contain one specific value:
<?php
namespace KopernikusTimrReportManager;
use PHPUnitFrameworkTestCase;
abstract class ArrayContainsValueTestCase extends TestCase
{
public static function assertArrayOnlyContainsTrue(array $haystack): void
{
static::assertArrayOnlyContainsSameValue(true, $haystack);
}
public static function assertArrayOnlyContainsSameValue(mixed $expectedValue, array $haystack): void
{
$haystack = array_unique($haystack);
static::assertTrue(static::areOnlySameValuesInArray($expectedValue, $haystack), message: 'The array contains of different values, yet sameness was expected');
}
private static function areOnlySameValuesInArray(mixed $expectedValue, array $haystack): bool
{
$haystack = array_unique($haystack);
if (count($haystack) !== 1) {
return false;
}
return reset($haystack) === $expectedValue;
}
public static function assertArrayOnlyContainsFalse(array $haystack)
{
static::assertArrayOnlyContainsSameValue(false, $haystack);
}
}
This is an assertion that is very agnostic to the domain.
Yet other custom assertions I want build are much more bound to the domain of the project, e.g. I created an assertion to check that a custom TimeEntry
value object contains excatly one a specific numeric ticket id, and that one should live in its own class:
<?php
namespace KopernikusTimrReportManager;
use KopernikusTimrReportManagerDtoTimeEntry;
use PHPUnitFrameworkTestCase;
abstract class TimeEntryTestCase extends TestCase
{
public static function assertTimeEntryHasTicketId(TimeEntry $timeEntry, int $ticketNumber)
{
$ticketNumberHashtag = '#' . (string)$ticketNumber;
$count = substr_count($timeEntry->description, '#');
static::assertSame(1, $count, 'the time entry must only contain one hashtag for the ticket id');
static::assertSame($timeEntry->ticket, $ticketNumberHashtag);
}
}
Assume I have a test class, MyTestThatRequiredBothAssertions
. I could achieve that via:
ArrayContainsValueTestCase extends TestCase
TimeEntryTestCase extends ArrayContainsValueTestCase
MyTestThatRequiredBothAssertions extends TimeEntryTestCase
Yet not every actual TimeEntryTestCase
would need the assertions provided by ArrayContainsValueTestCase
.
I furthermore plan to create a couple of custom assertions, not only two, so the inheritance tree seems likely to get out of hand.
I would rather do:
abstract class ArrayContainsValueTestCase extends TestCase
abstract class TimeEntryTestCase extends TestCase
and would like to use them in a specific testcase like this:
class MyTestThatRequiredBothAssertions extends ArrayContainsValueTestCase, TimeEntryTestCase
and add further TestCases on demand, yet php allows only extending one class, so this won’t work.
Is there another solution I am missing to separate the concerns here?
Can I provide my custom assertions differently, while retaining IDE support (auto-completion of the method names and their parameter value) of the methods within a test class?
I want to have multiple classes defining custom assertion, so putting them all in a single file is not something I want to do.
Each of those should at best only extend the default TestCase
-class, yet I want to be able to mix them with one another freely.
Only if a SpecificTestCase
depends on the assertions of another CustomTestCase, I am ok with them depending on one another.
I also thought about using traits, yet a trait cannot extend another class, so this:
trait MyCustomAssertion extends TestCase {
}
is also not allowed.
2
Answers
PHPUnit defines its Assertions as public static classes.
Hence, your own custom Assertions can call its assertions via static access:
TestCase::assertTrue(...)
That means one can use traits as you don't need to extend the
TestCase
anymore. You just define them as such:In your actual TestCase you can then add those traits as needed:
You could even have your custom assertions trait include other traits themselves, just be aware that calling
Trait::somePublicMethod()
is deprecated and may stop working in the future.You have to use them within the trait (making it so that the testcase gets access to the other included assertions of that used trait).
Here is a contrived
TraitUsingOtherTraits
:And it my actual contrived test class I have the method:
which runs just fine:
This is a problem that surely had some consequences in the original design of PhpUnit as well. It is similarly constrained by the single-inheritance rules of PHP and in its earlier versions there were even no traits in PHP that are commonly in use to handle the diamond problem you describe nowadays.
Given the presumption about the extensibility of traits do not finally resolve as an early confusion and you still do not want to use traits here, you can take a look how assertions are implemented in PHP Unit itself.
There is at least a single class per assertion, which is, how I read your question, you strive for.