jlh
jlh

Reputation: 4677

phpunit: Avoid printing very long output

I have a PHPUnit test which checks that a rendered HTML output does not contain a certain string, I use:

public function testSomething() {
    $htmlOutput = ...;
    self::assertDoesNotMatchRegularExpression(
        '/...pattern to detect a certain error.../',
        $htmlOutput,
        'HTML response contained a certain error',
    );
}

When the test fails, PHPUnit prints an extremly long output:

There was 1 failure:

1) MyTest::testSomething
HTML response contained a certain error
Failed asserting that '<!DOCTYPE html>\r\n
<html lang="en">\r\n
<head>\r\n
...
... hundreds and hundreds of lines
....
</body>\r\n
</html>' does not match PCRE pattern "/...pattern to detect a certain error.../".

This is very annoying because all pieces of important information have now scrolled up in my terminal way beyond reach, which is the name of the failing test and the actual message "HTML response contained a certain error". Of course the exact string can potentially be important to figure out what went wrong, but in half of the cases the message is good enough.

What's the recommended approach here?

Upvotes: 4

Views: 385

Answers (2)

jlh
jlh

Reputation: 4677

As Sebastian Bergmann noted, in general HTML and XML validation should not be done with regexes. I found that PHP's XML parser with xpath queries can be useful. Also frameworks often contain useful extensions to PHPUnit (for example symfony).

That said, I did found a solution that works nicely even for non-HTML content, for example long plain text output. It involves writing a custom PHPUnit constraint:

use PHPUnit\Framework\Constraint\Constraint;

/**
 * Class RegularExpressionForLongString is a variant of PHPUnit's RegularExpression that
 * does not print the entire string on failure, which makes it useful for testing very
 * long strings.  Instead it prints the snippet where the regex first matched.
 */
class RegularExpressionForLongString extends Constraint {
    /**
     * Maximum length to print
     */
    private const MAX_LENGTH = 127;

    /**
     * @var string
     */
    private $pattern;

    /**
     * @var array|null
     */
    private $lastMatch = null;

    /**
     * RegularExpressionForLongString constructor.
     *
     * @param string $pattern
     */
    public function __construct(string $pattern) {
        $this->pattern = $pattern;
    }

    /**
     * @inheritDoc
     */
    public function toString(): string {
        return sprintf(
            'matches PCRE pattern "%s"',
            $this->pattern
        );
    }

    /**
     * @inheritDoc
     */
    protected function matches($other): bool {
        return preg_match($this->pattern, $other, $this->lastMatch, PREG_OFFSET_CAPTURE) > 0;
    }

    /**
     * @inheritDoc
     */
    protected function failureDescription($other): string {
        if (!is_string($other)) {
            return parent::failureDescription($other);
        }

        $strlen = strlen($other);
        $from = $this->lastMatch[0][1];
        $to = $from + strlen($this->lastMatch[0][0]);
        $context = max(0, intdiv(self::MAX_LENGTH - ($to - $from), 2));
        $from -= $context;
        $to += $context;
        if ($from <= 0) {
            $from = 0;
            $prefix = '';
        } else {
            $prefix = "\u{2026}";
        }
        if ($to >= $strlen) {
            $to = $strlen;
            $suffix = '';
        } else {
            $suffix = "\u{2026}";
        }

        $substr = substr($other, $from, $to - $from);
        return $prefix . $this->exporter()->export($substr) . $suffix . ' ' . $this->toString();
    }
}

Then in a new base class for the tests:

use PHPUnit\Framework\Constraint\LogicalNot;

/**
 * Class MyTestCase
 */
class MyTestCase extends TestCase {
    /**
     * Asserts that a string does not match a given regular expression.  But don't be so verbose
     * about it.
     *
     * @param string $pattern
     * @param string $string
     * @param string $message
     */
    public static function assertDoesNotMatchRegularExpressionForLongString(string $pattern, string $string, string $message = ''): void {
        static::assertThat(
            $string,
            new LogicalNot(new RegularExpressionForLongString($pattern)),
            $message,
        );
    }
}

Here's an example of how to use it:

self::assertDoesNotMatchRegularExpressionForLongString('/\{[A-Z_]+\}/', $content, "Response contains placeholders that weren't substituted");

Here's a sample output of it failing:

There was 1 failure:

1) <namespace>\SomeClassTest::testFunc
Response contains placeholders that weren't substituted
Failed asserting that …'re will be context printed here\r\n
    {CLIENT_FIRST_NAME}\r\n
    Some other text here.\r\n
    '… does not match PCRE pattern "/\{[A-Z_]+\}/".

Upvotes: 0

Sebastian Bergmann
Sebastian Bergmann

Reputation: 8326

I am afraid that this is what it is when assertDoesNotMatchRegularExpression() is used. That being said, I would suggest not to use regular expressions for verifying HTML or XML. Use specialized assertions that use CSS selectors or XPath expressions instead.

Upvotes: 3

Related Questions