1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
|
<?php
declare( strict_types=1 );
namespace MediaWiki\Tests;
use MediaWikiIntegrationTestCase;
use Psr\Container\ContainerInterface;
use ReflectionClass;
use ReflectionMethod;
use ReflectionNamedType;
use ReflectionType;
/**
* Base class for testing ExtensionServices classes.
*
* Such classes are used in many extensions to access services more easily.
* They usually have one method like this for each service they register:
*
* ```php
* public static function getService1( ContainerInterface $services = null ): Service1 {
* return ( $services ?: MediaWikiServices::getInstance() )
* ->get( 'ExtensionName.Service1' );
* }
* ```
*
* To test an ExtensionServices class,
* create a subclass of this test base class and specify $className and $serviceNamePrefix.
*
* @license GPL-2.0-or-later
*/
abstract class ExtensionServicesTestBase extends MediaWikiIntegrationTestCase {
/**
* @var string The name of the ExtensionServices class.
* (A fully qualified name, usually specified via ::class syntax.)
*/
protected string $className;
/**
* @var string The prefix of the services in the service wiring.
* Usually something like 'ExtensionName.'.
* @see ExtensionJsonTestBase::$serviceNamePrefix
*/
protected string $serviceNamePrefix;
/**
* @var string[] An optional list of service names that,
* despite starting with the {@link self::$serviceNamePrefix},
* have no corresponding getter method on the ExtensionServices class.
* This can be used to temporarily support the old name of a renamed service
* for backwards compatibility with other extensions.
*/
protected array $serviceNamesWithoutMethods = [];
/** @dataProvider provideMethods */
public function testMethodSignature( ReflectionMethod $method ): void {
$this->assertTrue( $method->isPublic(),
'service accessor must be public' );
$this->assertTrue( $method->isStatic(),
'service accessor must be static' );
$this->assertStringStartsWith( 'get', $method->getName(),
'service accessor must be a getter' );
$this->assertTrue( $method->hasReturnType(),
'service accessor must declare return type' );
}
/** @dataProvider provideMethods */
public function testMethodWithDefaultServiceContainer( ReflectionMethod $method ): void {
$methodName = $method->getName();
$serviceName = $this->serviceNamePrefix . substr( $methodName, strlen( 'get' ) );
$expectedService = $this->createValue( $method->getReturnType() );
$this->setService( $serviceName, $expectedService );
$actualService = $this->className::$methodName();
$this->assertSame( $expectedService, $actualService,
'should return service from MediaWikiServices' );
}
/** @dataProvider provideMethods */
public function testMethodWithCustomServiceContainer( ReflectionMethod $method ): void {
$methodName = $method->getName();
$serviceName = $this->serviceNamePrefix . substr( $methodName, strlen( 'get' ) );
$expectedService = $this->createValue( $method->getReturnType() );
$services = $this->createMock( ContainerInterface::class );
$services->expects( $this->once() )
->method( 'get' )
->with( $serviceName )
->willReturn( $expectedService );
$actualService = $this->className::$methodName( $services );
$this->assertSame( $expectedService, $actualService,
'should return service from injected container' );
}
public function provideMethods(): iterable {
$reflectionClass = new ReflectionClass( $this->className );
$methods = $reflectionClass->getMethods();
foreach ( $methods as $method ) {
if ( $method->isConstructor() ) {
continue;
}
yield $method->getName() => [ $method ];
}
}
private function createValue( ReflectionType $type ) {
// (in PHP 8.0, account for $type being a ReflectionUnionType here)
$this->assertInstanceOf( ReflectionNamedType::class, $type );
/** @var ReflectionNamedType $type */
if ( $type->allowsNull() ) {
return null;
}
if ( $type->isBuiltin() ) {
switch ( $type->getName() ) {
case 'bool':
return true;
case 'int':
return 0;
case 'float':
return 0.0;
case 'string':
return '';
case 'array':
case 'iterable':
return [];
case 'callable':
return 'is_null';
default:
$this->fail( "unknown builtin type {$type->getName()}" );
}
}
return $this->createMock( $type->getName() );
}
public function testMethodsExist(): void {
if ( $this->serviceNamePrefix === '' ) {
return;
}
$reflectionClass = new ReflectionClass( $this->className );
foreach ( $this->getServiceContainer()->getServiceNames() as $serviceName ) {
if ( in_array( $serviceName, $this->serviceNamesWithoutMethods, true ) ) {
continue;
}
if ( str_starts_with( $serviceName, $this->serviceNamePrefix ) ) {
$serviceNameSuffix = substr( $serviceName, strlen( $this->serviceNamePrefix ) );
$_ = $reflectionClass->getMethod( 'get' . $serviceNameSuffix ); // should not throw
}
}
$this->assertTrue( true, 'test did not throw' );
}
}
|