File: ExtensionServicesTestBase.php

package info (click to toggle)
mediawiki 1%3A1.43.3%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 417,464 kB
  • sloc: php: 1,062,949; javascript: 664,290; sql: 9,714; python: 5,458; xml: 3,489; sh: 1,131; makefile: 64
file content (158 lines) | stat: -rw-r--r-- 5,025 bytes parent folder | download
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' );
	}

}