最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

php - How to groupreusemix custom assertions classes for phpunit testcases? - Stack Overflow

programmeradmin0浏览0评论

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.

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.

Share Improve this question asked Jan 18 at 17:54 k0pernikusk0pernikus 66.5k77 gold badges240 silver badges359 bronze badges 1
  • 1 Who marked this as needing details or clarity? This is a phenomenal question, and is incredibly detailed and clear. – maiorano84 Commented Jan 19 at 4:33
Add a comment  | 

2 Answers 2

Reset to default 1

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:

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.

发布评论

评论列表(0)

  1. 暂无评论