Reputation: 66430
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 Kopernikus\TimrReportManager;
use PHPUnit\Framework\TestCase;
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 Kopernikus\TimrReportManager;
use Kopernikus\TimrReportManager\Dto\TimeEntry;
use PHPUnit\Framework\TestCase;
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.
Upvotes: 0
Views: 62
Reputation: 66430
PHPUnit defines its Assertions as public static methods.
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:
<?php
namespace Kopernikus\TimrReportManager;
use PHPUnit\Framework\TestCase;
trait ArrayContainsValueTrait
{
public static function assertArrayOnlyContainsTrue(array $haystack): void
{
static::assertArrayOnlyContainsSameValue(true, $haystack);
}
public static function assertArrayOnlyContainsSameValue(mixed $expectedValue, array $haystack): void
{
$haystack = array_unique($haystack);
TestCase::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);
}
}
<?php
namespace Kopernikus\TimrReportManager;
use Kopernikus\TimrReportManager\Dto\TimeEntry;
use PHPUnit\Framework\TestCase;
trait TimeEntryAssertionsTrait
{
public static function assertTimeEntryHasTicketId(TimeEntry $timeEntry, int $ticketNumber)
{
$ticketNumberHashtag = '#' . (string)$ticketNumber;
$count = substr_count($timeEntry->description, '#');
TestCase::assertSame(1, $count, 'the time entry must only contain one hashtag for the ticket id');
TestCase::assertSame($timeEntry->ticket, $ticketNumberHashtag);
}
}
In your actual TestCase you can then add those traits as needed:
class CsvParserTest extends TestCase
{
use TimeEntryAssertionsTrait;
use ArrayContainsValueTrait;
...
}
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
:
<?php
namespace Kopernikus\TimrReportManager\Services;
use Kopernikus\TimrReportManager\ArrayContainsValueTrait;
use Kopernikus\TimrReportManager\Dto\TimeEntry;
use Kopernikus\TimrReportManager\TimeEntryAssertionsTrait;
trait TraitUsingOtherTraits
{
use ArrayContainsValueTrait;
use TimeEntryAssertionsTrait;
/**
* Contrived dummy assertion, this is no real test
*/
public static function assertUsingOtherTraits(): void
{
static::assertTimeEntryHasTicketId(new TimeEntry('foobar #123', '2024-12-31 13:00', '2024-12-31 15:00'), 123);
static::assertArrayOnlyContainsTrue([true, true, true]);
}
}
And it my actual contrived test class I have the method:
public function testShowcaseTraits()
{
static::assertUsingOtherTraits();
}
which runs just fine:
Upvotes: 1
Reputation: 197499
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.
Upvotes: 0