Why write custom assertions in your tests?

I'm refactoring some code on a client project - creating a Repository class to centralise some logic before implementing the next feature.

The repository class is responsible for finding and returning any nodes with a specified field value and some base conditions (it must be the correct node type, published, etc.).

Adding a custom assertion

I'm using PHPUnit's native assertions to check it returns a Collection (I regularly include the illuminate/collections library from Laravel in other projects) and that each item is an instance of a NodeInterface, but there isn't an assertion to check each node is of the correct type.

My initial implementation was to loop over each node and use assertSame on its bundle before refactoring to create an array of unique bundle names and comparing it to my expected names:

self::assertSame(
expected: [$nodeType],
actual: $haystack
->map(fn (NodeInterface $item): string => $item->bundle())
->unique()
->toArray(),
);

Why write a custom assertion?

Whilst this works, it likely won't be clear in the future what it's testing.

My initial thought was to add a comment describing it, but then I decided to wrap it in a custom assertion - assertContainsOnlyNodesOfType - a private static function within my test class that wraps the native assertions.

This approach makes the test more readable now and in the future and more domain-focused by giving it a descriptive name.

It can be easily reused within the same test case or elsewhere.

Although I only perform one assertion in this case, I can combine multiple assertions and perform any other required steps.

Finally, I can contain any implementation details within the custom assertion. Here, I'm matching the result against an array of expected values, not just a single node type which is what I want. This detail can be contained within the assertion, making it easier to read and reuse in the future.