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 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253
|
<?php
/**
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Testing
*/
use MediaWiki\Config\ConfigException;
use MediaWiki\Config\HashConfig;
use MediaWiki\Deferred\DeferredUpdates;
use MediaWiki\Deferred\DeferredUpdatesScopeMediaWikiStack;
use MediaWiki\Deferred\DeferredUpdatesScopeStack;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\Logger\NullSpi;
use MediaWiki\MediaWikiServices;
use MediaWiki\Registration\ExtensionRegistry;
use MediaWiki\Settings\SettingsBuilder;
use PHPUnit\Framework\Exception;
use PHPUnit\Framework\TestCase;
use Wikimedia\ObjectFactory\ObjectFactory;
use Wikimedia\Services\NoSuchServiceException;
/**
* Base class for unit tests.
*
* Extend this class if you are testing classes which use dependency injection and do not access
* global functions, variables, services or a storage backend.
*
* @stable to extend
* @since 1.34
*/
abstract class MediaWikiUnitTestCase extends TestCase {
use MediaWikiCoversValidator;
use MediaWikiTestCaseTrait;
/** @var array */
private static $originalGlobals;
/** @var array */
private static $unitGlobals;
private ?MediaWikiServices $serviceContainer = null;
/**
* @var array<string,object>
*/
private array $services = [];
/**
* List of allowed globals to allow in MediaWikiUnitTestCase.
*
* Please, keep this list to the bare minimum.
*
* @return string[]
*/
private static function getAllowedGlobalsList() {
return [
// The autoloader may change between bootstrap and the first test,
// so (lazily) capture these here instead.
'wgAutoloadClasses',
'wgAutoloadLocalClasses',
// Need for LoggerFactory. Default is NullSpi.
'wgMWLoggerDefaultSpi',
'wgLegalTitleChars',
'wgDevelopmentWarnings',
// Dependency of wfParseUrl()
'wgUrlProtocols',
// For LegacyLogger, injected by DevelopmentSettings.php
'wgDebugLogFile',
'wgDebugLogGroups',
];
}
/**
* The annotation causes this to be called immediately before setUpBeforeClass()
* @beforeClass
*/
final public static function mediaWikiSetUpBeforeClass(): void {
$reflection = new ReflectionClass( static::class );
$dirSeparator = DIRECTORY_SEPARATOR;
if ( stripos( $reflection->getFileName(), "{$dirSeparator}unit{$dirSeparator}" ) === false ) {
self::fail( 'This unit test needs to be in "tests/phpunit/unit"!' );
}
self::$unitGlobals =& TestSetup::$bootstrapGlobals;
foreach ( self::getAllowedGlobalsList() as $global ) {
self::$unitGlobals[ $global ] =& $GLOBALS[ $global ];
}
// Would be nice if we could simply replace $GLOBALS as a whole,
// but un-setting or re-assigning that breaks the reference of this magic
// variable. Thus we have to modify it in place.
self::$originalGlobals = [];
foreach ( $GLOBALS as $key => $_ ) {
// Stash current values
self::$originalGlobals[$key] =& $GLOBALS[$key];
// Remove globals not part of the snapshot (see bootstrap.php, phpunit.php).
if ( $key !== 'GLOBALS' && !array_key_exists( $key, self::$unitGlobals ) ) {
unset( $GLOBALS[$key] );
}
}
// Restore values from the early snapshot
// Not by ref because tests must not be able to modify the snapshot.
foreach ( self::$unitGlobals as $key => $value ) {
$GLOBALS[ $key ] = $value;
}
// Set DeferredUpdates into standalone mode
DeferredUpdates::setScopeStack( new DeferredUpdatesScopeStack() );
MediaWikiServices::disallowGlobalInstanceInUnitTests();
ExtensionRegistry::disableForTest();
SettingsBuilder::disableAccessForUnitTests();
}
/**
* @inheritDoc
*/
protected function runTest() {
try {
// Don't let LoggerFactory::getProvider() access globals or other things we don't want.
LoggerFactory::registerProvider( ObjectFactory::getObjectFromSpec( [
'class' => NullSpi::class
] ) );
return parent::runTest();
} catch ( ConfigException $exception ) {
throw new Exception(
'Config variables must be mocked, they cannot be accessed directly in tests which extend '
. self::class,
$exception->getCode(),
$exception
);
}
}
/**
* The annotation causes this to be called immediately after tearDown()
* @after
*/
final protected function mediaWikiTearDown(): void {
// Quick reset between tests
foreach ( $GLOBALS as $key => $_ ) {
if ( $key !== 'GLOBALS' && !array_key_exists( $key, self::$unitGlobals ) ) {
unset( $GLOBALS[$key] );
}
}
foreach ( self::$unitGlobals as $key => $value ) {
$GLOBALS[ $key ] = $value;
}
}
/**
* The annotation causes this to be called immediately after tearDownAfterClass()
* @afterClass
*/
final public static function mediaWikiTearDownAfterClass(): void {
// Remove globals created by the test
foreach ( $GLOBALS as $key => $_ ) {
if ( $key !== 'GLOBALS' && !array_key_exists( $key, self::$originalGlobals ) ) {
unset( $GLOBALS[$key] );
}
}
// Restore values (including reference!)
foreach ( self::$originalGlobals as $key => &$value ) {
$GLOBALS[ $key ] =& $value;
}
unset( $value );
MediaWikiServices::allowGlobalInstanceAfterUnitTests();
DeferredUpdates::setScopeStack( new DeferredUpdatesScopeMediaWikiStack() );
ExtensionRegistry::enableForTest();
SettingsBuilder::enableAccessAfterUnitTests();
}
/**
* Returns a mock service container.
* To populate the service container with service objects, use setService().
*
* @since 1.43
*/
protected function getServiceContainer(): MediaWikiServices {
if ( !$this->serviceContainer ) {
$this->serviceContainer = $this->getMockBuilder( MediaWikiServices::class )
->setConstructorArgs( [ new HashConfig() ] )
->onlyMethods( [
'getService',
'disableStorage',
'isStorageDisabled',
'redefineService',
'resetServiceForTesting',
'resetChildProcessServices',
'peekService'
] )
->getMock();
$this->serviceContainer
->method( 'getService' )
->willReturnCallback( function ( $name ) {
return $this->getService( $name );
} );
}
return $this->serviceContainer;
}
/**
* Returns a service previously defined with setService().
*
* @param string $name
*
* @return mixed The service instance
*/
protected function getService( string $name ) {
if ( !isset( $this->services[$name] ) ) {
throw new NoSuchServiceException( $name );
}
if ( is_callable( $this->services[$name] ) ) {
$func = $this->services[$name];
$this->services[$name] = $func( $this->serviceContainer );
}
return $this->services[$name];
}
/**
* Register a service object with the service container returned by
* getServiceContainer().
*
* @param string $name
* @param mixed $service
*
* @since 1.43
*/
protected function setService( string $name, $service ) {
$this->services[$name] = $service;
}
}
|