こんにちは。やまゆです。
PHPUnit 9 でモック用の withConsecutive
メソッドが deprecated になり、 10 で削除されました。
このメソッドは、同じメソッドを連続して呼ぶ場合のモック定義として有効でした。
例えば、このような実装コードがあるとします。
<?php
declare(strict_types=1);
/**
* @license Apache-2.0
*/
namespace App;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use RuntimeException;
final readonly class ValidateJsonHeaderMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
// Accept は "application/json" か "application/json charset=utf-8"
if ($request->hasHeader('Accept')) {
$acceptHeader = $request->getHeaderLine('Accept');
if (!\str_starts_with($acceptHeader, 'application/json')) {
throw new RuntimeException('Accept header must be application/json');
}
}
// Content-Type は "application/json" か "application/json; charset=utf-8"
if ($request->hasHeader('Content-Type')) {
$contentTypeHeader = $request->getHeaderLine('Content-Type');
if (!\str_starts_with($contentTypeHeader, 'application/json')) {
throw new RuntimeException('Content-Type header must be application/json');
}
}
return $handler->handle($request);
}
}
これに対して、 PHPUnit 9 では下記のテストコードが有効でした。
<?php
declare(strict_types=1);
/**
* @license Apache-2.0
*/
use App\ValidateJsonHeaderMiddleware;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* @covers \App\ValidateJsonHeaderMiddleware
*/
final class ValidateJsonHeaderMiddlewareTest extends TestCase
{
public function testInvalidAccept(): void
{
$request = $this->createMock(ServerRequestInterface::class);
$request->expects(self::once())
->method('hasHeader')
->with('Accept')
->willReturn(true);
$request->expects(self::once())
->method('getHeaderLine')
->with('Accept')
->willReturn('text/html');
$handler = $this->createMock(RequestHandlerInterface::class);
$handler->expects(self::never())
->method('handle');
$middleware = new ValidateJsonHeaderMiddleware();
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Accept header must be application/json');
$middleware->process($request, $handler);
}
public function testInvalidContentType(): void
{
$request = $this->createMock(ServerRequestInterface::class);
$request->expects(self::exactly(2))
->method('hasHeader')
->withConsecutive(['Accept'], ['Content-Type'])
->willReturnOnConsecutiveCalls(true, true);
$request->expects(self::exactly(2))
->method('getHeaderLine')
->withConsecutive(['Accept'], ['Content-Type'])
->willReturnOnConsecutiveCalls('application/json', 'text/html');
$handler = $this->createMock(RequestHandlerInterface::class);
$handler->expects(self::never())
->method('handle');
$middleware = new ValidateJsonHeaderMiddleware();
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Content-Type header must be application/json');
$middleware->process($request, $handler);
}
public function testValid(): void
{
$request = $this->createMock(ServerRequestInterface::class);
$request->expects(self::exactly(2))
->method('hasHeader')
->withConsecutive(['Accept'], ['Content-Type'])
->willReturnOnConsecutiveCalls(true, true);
$request->expects(self::exactly(2))
->method('getHeaderLine')
->withConsecutive(['Accept'], ['Content-Type'])
->willReturnOnConsecutiveCalls('application/json', 'application/json');
$handler = $this->createMock(RequestHandlerInterface::class);
$handler->expects(self::once())
->method('handle')
->with($request)
->willReturn($expected = $this->createMock(ResponseInterface::class));
$middleware = new ValidateJsonHeaderMiddleware();
$actual = $middleware->process($request, $handler);
self::assertSame($expected, $actual);
}
}
$ vendor/bin/phpunit test
PHPUnit 9.6.20 by Sebastian Bergmann and contributors.
Runtime: PHP 8.3.11
Configuration: /path/to/phpunit.xml
... 3 / 3 (100%)
Time: 00:00.011, Memory: 6.00 MB
OK (3 tests, 11 assertions)
これは、 PHPUnit 10 以降では失敗します。
$ vendor/bin/phpunit test
PHPUnit 10.5.33 by Sebastian Bergmann and contributors.
Runtime: PHP 8.3.11
Configuration: /path/to/phpunit.xml
EE. 3 / 3 (100%)
Time: 00:00.013, Memory: 8.00 MB
There were 2 errors:
1) ValidateJsonHeaderMiddlewareTest::testInvalidContentType
Error: Call to undefined method PHPUnit\Framework\MockObject\Builder\InvocationMocker::withConsecutive()
/path/to/test/ValidateJsonHeaderMiddlewareTest.php:47
2) ValidateJsonHeaderMiddlewareTest::testValid
Error: Call to undefined method PHPUnit\Framework\MockObject\Builder\InvocationMocker::withConsecutive()
/path/to/test/ValidateJsonHeaderMiddlewareTest.php:70
ERRORS!
Tests: 3, Assertions: 4, Errors: 2, PHPUnit Deprecations: 1.
Script phpunit handling the test event returned with error code 2
この StackOverflow などが詳しいですが、公式で代替するメソッドは用意されていないため、何かしらの手法を使ってワークアラウンドする必要があります。
代替案1: CakePHP によって用意された代替メソッドを利用する
CakePHP 5 から PHPUnit 10 を使うようになったため、この代替メソッドが用意されています。
この trait を use することで、 static withConsecutive
メソッドが生えるので、以下のように使用できます。
<?php
declare(strict_types=1);
/**
* @license Apache-2.0
*/
use App\ValidateJsonHeaderMiddleware;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
include 'PHPUnitConsecutiveTrait.php';
/**
* @covers \App\ValidateJsonHeaderMiddleware
*/
final class ValidateJsonHeaderMiddlewareTest extends TestCase
{
use \Cake\TestSuite\PHPUnitConsecutiveTrait;
public function testInvalidAccept(): void
{
$request = $this->createMock(ServerRequestInterface::class);
$request->expects(self::once())
->method('hasHeader')
->with('Accept')
->willReturn(true);
$request->expects(self::once())
->method('getHeaderLine')
->with('Accept')
->willReturn('text/html');
$handler = $this->createMock(RequestHandlerInterface::class);
$handler->expects(self::never())
->method('handle');
$middleware = new ValidateJsonHeaderMiddleware();
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Accept header must be application/json');
$middleware->process($request, $handler);
}
public function testInvalidContentType(): void
{
$request = $this->createMock(ServerRequestInterface::class);
$request->expects(self::exactly(2))
->method('hasHeader')
//->withConsecutive(['Accept'], ['Content-Type'])
->with(...self::withConsecutive(['Accept'], ['Content-Type']))
->willReturnOnConsecutiveCalls(true, true);
$request->expects(self::exactly(2))
->method('getHeaderLine')
//->withConsecutive(['Accept'], ['Content-Type'])
->with(...self::withConsecutive(['Accept'], ['Content-Type']))
->willReturnOnConsecutiveCalls('application/json', 'text/html');
$handler = $this->createMock(RequestHandlerInterface::class);
$handler->expects(self::never())
->method('handle');
$middleware = new ValidateJsonHeaderMiddleware();
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Content-Type header must be application/json');
$middleware->process($request, $handler);
}
public function testValid(): void
{
$request = $this->createMock(ServerRequestInterface::class);
$request->expects(self::exactly(2))
->method('hasHeader')
//->withConsecutive(['Accept'], ['Content-Type'])
->with(...self::withConsecutive(['Accept'], ['Content-Type']))
->willReturnOnConsecutiveCalls(true, true);
$request->expects(self::exactly(2))
->method('getHeaderLine')
//->withConsecutive(['Accept'], ['Content-Type'])
->with(...self::withConsecutive(['Accept'], ['Content-Type']))
->willReturnOnConsecutiveCalls('application/json', 'application/json');
$handler = $this->createMock(RequestHandlerInterface::class);
$handler->expects(self::once())
->method('handle')
->with($request)
->willReturn($expected = $this->createMock(ResponseInterface::class));
$middleware = new ValidateJsonHeaderMiddleware();
$actual = $middleware->process($request, $handler);
self::assertSame($expected, $actual);
}
}
代替案2: Mockery を使用する
PHPUnit の MockObject ではなく、 Mockery という別のモックを使うというテクニックもあります。
$ composer require --dev mockery/mockery
<?php
declare(strict_types=1);
/**
* @license Apache-2.0
*/
use App\ValidateJsonHeaderMiddleware;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
#[CoversClass(ValidateJsonHeaderMiddleware::class)]
final class ValidateJsonHeaderMiddlewareTest extends TestCase
{
use \Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
public function testInvalidAccept(): void
{
$request = \Mockery::mock(ServerRequestInterface::class);
$request->shouldReceive('hasHeader')
->once()
->with('Accept')
->andReturn(true);
$request->shouldReceive('getHeaderLine')
->once()
->with('Accept')
->andReturn('text/html');
$handler = $this->createMock(RequestHandlerInterface::class);
$handler->expects(self::never())
->method('handle');
$middleware = new ValidateJsonHeaderMiddleware();
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Accept header must be application/json');
$middleware->process($request, $handler);
}
public function testInvalidContentType(): void
{
$request = \Mockery::mock(ServerRequestInterface::class);
$request->shouldReceive('hasHeader')
->once()
->with('Accept')
->andReturn(true);
$request->shouldReceive('getHeaderLine')
->once()
->with('Accept')
->andReturn('application/json');
$request->shouldReceive('hasHeader')
->once()
->with('Content-Type')
->andReturn(true);
$request->shouldReceive('getHeaderLine')
->once()
->with('Content-Type')
->andReturn('text/html');
$handler = $this->createMock(RequestHandlerInterface::class);
$handler->expects(self::never())
->method('handle');
$middleware = new ValidateJsonHeaderMiddleware();
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Content-Type header must be application/json');
$middleware->process($request, $handler);
}
public function testValid(): void
{
$request = \Mockery::mock(ServerRequestInterface::class);
$request->shouldReceive('hasHeader')
->once()
->with('Accept')
->andReturn(true);
$request->shouldReceive('getHeaderLine')
->once()
->with('Accept')
->andReturn('application/json');
$request->shouldReceive('hasHeader')
->once()
->with('Content-Type')
->andReturn(true);
$request->shouldReceive('getHeaderLine')
->once()
->with('Content-Type')
->andReturn('application/json');
$handler = $this->createMock(RequestHandlerInterface::class);
$handler->expects(self::once())
->method('handle')
->with($request)
->willReturn($expected = $this->createMock(ResponseInterface::class));
$middleware = new ValidateJsonHeaderMiddleware();
$actual = $middleware->process($request, $handler);
self::assertSame($expected, $actual);
}
}
> phpunit
PHPUnit 11.3.4 by Sebastian Bergmann and contributors.
Runtime: PHP 8.3.11
Configuration: /path/to/phpunit.xml
... 3 / 3 (100%)
Time: 00:00.024, Memory: 10.00 MB
OK (3 tests, 18 assertions)
※ついでに Annotation から Attribute に変えています
Mockery の方が分かりやすくはありますね。