Merge "Create API to allow content handlers to handle structured data definitions"
[lhc/web/wiklou.git] / tests / phpunit / MediaWikiTestCase.php
index d5192ac..8dfe628 100644 (file)
@@ -2,12 +2,23 @@
 use MediaWiki\Logger\LegacySpi;
 use MediaWiki\Logger\LoggerFactory;
 use MediaWiki\Logger\MonologSpi;
+use MediaWiki\MediaWikiServices;
 use Psr\Log\LoggerInterface;
 
 /**
  * @since 1.18
  */
 abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
+
+       /**
+        * The service locator created by prepareServices(). This service locator will
+        * be restored after each test. Tests that pollute the global service locator
+        * instance should use overrideMwServices() to isolate the test.
+        *
+        * @var MediaWikiServices|null
+        */
+       private static $serviceLocator = null;
+
        /**
         * $called tracks whether the setUp and tearDown method has been called.
         * class extending MediaWikiTestCase usually override setUp and tearDown
@@ -29,6 +40,8 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
        public static $users;
 
        /**
+        * Primary database
+        *
         * @var DatabaseBase
         * @since 1.18
         */
@@ -106,22 +119,245 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                }
        }
 
-       public function run( PHPUnit_Framework_TestResult $result = null ) {
+       public static function setUpBeforeClass() {
+               parent::setUpBeforeClass();
+
+               // NOTE: Usually, PHPUnitMaintClass::finalSetup already called this,
+               // but let's make doubly sure.
+               self::prepareServices( new GlobalVarConfig() );
+       }
+
+       /**
+        * Convenience method for getting an immutable test user
+        *
+        * @since 1.28
+        *
+        * @param string[] $groups Groups the test user should be in.
+        * @return TestUser
+        */
+       public static function getTestUser( $groups = [] ) {
+               return TestUserRegistry::getImmutableTestUser( $groups );
+       }
+
+       /**
+        * Convenience method for getting a mutable test user
+        *
+        * @since 1.28
+        *
+        * @param string[] $groups Groups the test user should be added in.
+        * @return TestUser
+        */
+       public static function getMutableTestUser( $groups = [] ) {
+               return TestUserRegistry::getMutableTestUser( __CLASS__, $groups );
+       }
+
+       /**
+        * Convenience method for getting an immutable admin test user
+        *
+        * @since 1.28
+        *
+        * @param string[] $groups Groups the test user should be added to.
+        * @return TestUser
+        */
+       public static function getTestSysop() {
+               return self::getTestUser( [ 'sysop', 'bureaucrat' ] );
+       }
+
+       /**
+        * Prepare service configuration for unit testing.
+        *
+        * This calls MediaWikiServices::resetGlobalInstance() to allow some critical services
+        * to be overridden for testing.
+        *
+        * prepareServices() only needs to be called once, but should be called as early as possible,
+        * before any class has a chance to grab a reference to any of the global services
+        * instances that get discarded by prepareServices(). Only the first call has any effect,
+        * later calls are ignored.
+        *
+        * @note This is called by PHPUnitMaintClass::finalSetup.
+        *
+        * @see MediaWikiServices::resetGlobalInstance()
+        *
+        * @param Config $bootstrapConfig The bootstrap config to use with the new
+        *        MediaWikiServices. Only used for the first call to this method.
+        */
+       public static function prepareServices( Config $bootstrapConfig ) {
+               static $servicesPrepared = false;
+
+               if ( $servicesPrepared ) {
+                       return;
+               } else {
+                       $servicesPrepared = true;
+               }
+
+               self::resetGlobalServices( $bootstrapConfig );
+       }
+
+       /**
+        * Reset global services, and install testing environment.
+        * This is the testing equivalent of MediaWikiServices::resetGlobalInstance().
+        * This should only be used to set up the testing environment, not when
+        * running unit tests. Use overrideMwServices() for that.
+        *
+        * @see MediaWikiServices::resetGlobalInstance()
+        * @see prepareServices()
+        * @see overrideMwServices()
+        *
+        * @param Config|null $bootstrapConfig The bootstrap config to use with the new
+        *        MediaWikiServices.
+        */
+       protected static function resetGlobalServices( Config $bootstrapConfig = null ) {
+               $oldServices = MediaWikiServices::getInstance();
+               $oldConfigFactory = $oldServices->getConfigFactory();
+
+               $testConfig = self::makeTestConfig( $bootstrapConfig );
+
+               MediaWikiServices::resetGlobalInstance( $testConfig );
+
+               self::$serviceLocator = MediaWikiServices::getInstance();
+               self::installTestServices(
+                       $oldConfigFactory,
+                       self::$serviceLocator
+               );
+       }
+
+       /**
+        * Create a config suitable for testing, based on a base config, default overrides,
+        * and custom overrides.
+        *
+        * @param Config|null $baseConfig
+        * @param Config|null $customOverrides
+        *
+        * @return Config
+        */
+       private static function makeTestConfig(
+               Config $baseConfig = null,
+               Config $customOverrides = null
+       ) {
+               $defaultOverrides = new HashConfig();
+
+               if ( !$baseConfig ) {
+                       $baseConfig = MediaWikiServices::getInstance()->getBootstrapConfig();
+               }
+
                /* Some functions require some kind of caching, and will end up using the db,
                 * which we can't allow, as that would open a new connection for mysql.
                 * Replace with a HashBag. They would not be going to persist anyway.
                 */
-               ObjectCache::$instances[CACHE_DB] = new HashBagOStuff;
+               $hashCache = [ 'class' => 'HashBagOStuff' ];
+               $objectCaches = [
+                               CACHE_DB => $hashCache,
+                               CACHE_ACCEL => $hashCache,
+                               CACHE_MEMCACHED => $hashCache,
+                               'apc' => $hashCache,
+                               'xcache' => $hashCache,
+                               'wincache' => $hashCache,
+                       ] + $baseConfig->get( 'ObjectCaches' );
+
+               $defaultOverrides->set( 'ObjectCaches', $objectCaches );
+               $defaultOverrides->set( 'MainCacheType', CACHE_NONE );
+
+               // Use a fast hash algorithm to hash passwords.
+               $defaultOverrides->set( 'PasswordDefault', 'A' );
+
+               $testConfig = $customOverrides
+                       ? new MultiConfig( [ $customOverrides, $defaultOverrides, $baseConfig ] )
+                       : new MultiConfig( [ $defaultOverrides, $baseConfig ] );
+
+               return $testConfig;
+       }
+
+       /**
+        * @param ConfigFactory $oldConfigFactory
+        * @param MediaWikiServices $newServices
+        *
+        * @throws MWException
+        */
+       private static function installTestServices(
+               ConfigFactory $oldConfigFactory,
+               MediaWikiServices $newServices
+       ) {
+               // Use bootstrap config for all configuration.
+               // This allows config overrides via global variables to take effect.
+               $bootstrapConfig = $newServices->getBootstrapConfig();
+               $newServices->resetServiceForTesting( 'ConfigFactory' );
+               $newServices->redefineService(
+                       'ConfigFactory',
+                       self::makeTestConfigFactoryInstantiator(
+                               $oldConfigFactory,
+                               [ 'main' =>  $bootstrapConfig ]
+                       )
+               );
+       }
+
+       /**
+        * @param ConfigFactory $oldFactory
+        * @param Config[] $configurations
+        *
+        * @return Closure
+        */
+       private static function makeTestConfigFactoryInstantiator(
+               ConfigFactory $oldFactory,
+               array $configurations
+       ) {
+               return function( MediaWikiServices $services ) use ( $oldFactory, $configurations ) {
+                       $factory = new ConfigFactory();
+
+                       // clone configurations from $oldFactory that are not overwritten by $configurations
+                       $namesToClone = array_diff(
+                               $oldFactory->getConfigNames(),
+                               array_keys( $configurations )
+                       );
 
-               // Sandbox APC by replacing with in-process hash instead.
-               // Ensures values are removed between tests.
-               ObjectCache::$instances['apc'] =
-               ObjectCache::$instances['xcache'] =
-               ObjectCache::$instances['wincache'] = new HashBagOStuff;
+                       foreach ( $namesToClone as $name ) {
+                               $factory->register( $name, $oldFactory->makeConfig( $name ) );
+                       }
+
+                       foreach ( $configurations as $name => $config ) {
+                               $factory->register( $name, $config );
+                       }
+
+                       return $factory;
+               };
+       }
+
+       /**
+        * Resets some well known services that typically have state that may interfere with unit tests.
+        * This is a lightweight alternative to resetGlobalServices().
+        *
+        * @note There is no guarantee that no references remain to stale service instances destroyed
+        * by a call to doLightweightServiceReset().
+        *
+        * @throws MWException if called outside of PHPUnit tests.
+        *
+        * @see resetGlobalServices()
+        */
+       private function doLightweightServiceReset() {
+               global $wgRequest;
+
+               JobQueueGroup::destroySingletons();
+               ObjectCache::clear();
+               FileBackendGroup::destroySingleton();
+
+               // TODO: move global state into MediaWikiServices
+               RequestContext::resetMain();
+               MediaHandler::resetCache();
+               if ( session_id() !== '' ) {
+                       session_write_close();
+                       session_id( '' );
+               }
+
+               $wgRequest = new FauxRequest();
+               MediaWiki\Session\SessionManager::resetCache();
+       }
+
+       public function run( PHPUnit_Framework_TestResult $result = null ) {
+               // Reset all caches between tests.
+               $this->doLightweightServiceReset();
 
                $needsResetDB = false;
 
-               if ( $this->needsDB() ) {
+               if ( !self::$dbSetup || $this->needsDB() ) {
                        // set up a DB connection for this test to use
 
                        self::$useTemporaryTables = !$this->getCliArg( 'use-normal-tables' );
@@ -132,14 +368,21 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                        $this->checkDbIsSupported();
 
                        if ( !self::$dbSetup ) {
-                               // switch to a temporary clone of the database
-                               self::setupTestDB( $this->db, $this->dbPrefix() );
+                               $this->setupAllTestDBs();
+                               $this->addCoreDBData();
 
                                if ( ( $this->db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
-                                       $this->resetDB();
+                                       $this->resetDB( $this->db, $this->tablesUsed );
                                }
                        }
-                       $this->addCoreDBData();
+
+                       // TODO: the DB setup should be done in setUpBeforeClass(), so the test DB
+                       // is available in subclass's setUpBeforeClass() and setUp() methods.
+                       // This would also remove the need for the HACK that is oncePerClass().
+                       if ( $this->oncePerClass() ) {
+                               $this->addDBDataOnce();
+                       }
+
                        $this->addDBData();
                        $needsResetDB = true;
                }
@@ -147,10 +390,26 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                parent::run( $result );
 
                if ( $needsResetDB ) {
-                       $this->resetDB();
+                       $this->resetDB( $this->db, $this->tablesUsed );
                }
        }
 
+       /**
+        * @return bool
+        */
+       private function oncePerClass() {
+               // Remember current test class in the database connection,
+               // so we know when we need to run addData.
+
+               $class = static::class;
+
+               $first = !isset( $this->db->_hasDataForTestClass )
+                       || $this->db->_hasDataForTestClass !== $class;
+
+               $this->db->_hasDataForTestClass = $class;
+               return $first;
+       }
+
        /**
         * @since 1.21
         *
@@ -222,6 +481,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                }
 
                DeferredUpdates::clearPendingUpdates();
+               ObjectCache::getMainWANInstance()->clearProcessCache();
 
                ob_start( 'MediaWikiTestCase::wfResetOutputBuffersBarrier' );
        }
@@ -263,6 +523,12 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                }
                $this->mwGlobals = [];
                $this->restoreLoggers();
+
+               if ( self::$serviceLocator && MediaWikiServices::getInstance() !== self::$serviceLocator ) {
+                       MediaWikiServices::forceGlobalInstance( self::$serviceLocator );
+               }
+
+               // TODO: move global state into MediaWikiServices
                RequestContext::resetMain();
                MediaHandler::resetCache();
                if ( session_id() !== '' ) {
@@ -271,6 +537,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                }
                $wgRequest = new FauxRequest();
                MediaWiki\Session\SessionManager::resetCache();
+               MediaWiki\Auth\AuthManager::resetCache();
 
                $phpErrorLevel = intval( ini_get( 'error_reporting' ) );
 
@@ -298,6 +565,30 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                );
        }
 
+       /**
+        * Sets a service, maintaining a stashed version of the previous service to be
+        * restored in tearDown
+        *
+        * @since 1.27
+        *
+        * @param string $name
+        * @param object $object
+        */
+       protected function setService( $name, $object ) {
+               // If we did not yet override the service locator, so so now.
+               if ( MediaWikiServices::getInstance() === self::$serviceLocator ) {
+                       $this->overrideMwServices();
+               }
+
+               MediaWikiServices::getInstance()->disableService( $name );
+               MediaWikiServices::getInstance()->redefineService(
+                       $name,
+                       function () use ( $object ) {
+                               return $object;
+                       }
+               );
+       }
+
        /**
         * Sets a global, maintaining a stashed version of the previous global to be
         * restored in tearDown
@@ -328,6 +619,9 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
         * @param mixed $value Value to set the global to (ignored
         *  if an array is given as first argument).
         *
+        * @note To allow changes to global variables to take effect on global service instances,
+        *       call overrideMwServices().
+        *
         * @since 1.21
         */
        protected function setMwGlobals( $pairs, $value = null ) {
@@ -342,6 +636,29 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                }
        }
 
+       /**
+        * Check if we can back up a value by performing a shallow copy.
+        * Values which fail this test are copied recursively.
+        *
+        * @param mixed $value
+        * @return bool True if a shallow copy will do; false if a deep copy
+        *  is required.
+        */
+       private static function canShallowCopy( $value ) {
+               if ( is_scalar( $value ) || $value === null ) {
+                       return true;
+               }
+               if ( is_array( $value ) ) {
+                       foreach ( $value as $subValue ) {
+                               if ( !is_scalar( $subValue ) && $subValue !== null ) {
+                                       return false;
+                               }
+                       }
+                       return true;
+               }
+               return false;
+       }
+
        /**
         * Stashes the global, will be restored in tearDown()
         *
@@ -355,6 +672,10 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
         * @param array|string $globalKeys Key to the global variable, or an array of keys.
         *
         * @throws Exception When trying to stash an unset global
+        *
+        * @note To allow changes to global variables to take effect on global service instances,
+        *       call overrideMwServices().
+        *
         * @since 1.23
         */
        protected function stashMwGlobals( $globalKeys ) {
@@ -373,13 +694,22 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                                // NOTE: we serialize then unserialize the value in case it is an object
                                // this stops any objects being passed by reference. We could use clone
                                // and if is_object but this does account for objects within objects!
-                               try {
-                                       $this->mwGlobals[$globalKey] = unserialize( serialize( $GLOBALS[$globalKey] ) );
-                               }
-                                       // NOTE; some things such as Closures are not serializable
-                                       // in this case just set the value!
-                               catch ( Exception $e ) {
+                               if ( self::canShallowCopy( $GLOBALS[$globalKey] ) ) {
                                        $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
+                               } elseif (
+                                       // Many MediaWiki types are safe to clone. These are the
+                                       // ones that are most commonly stashed.
+                                       $GLOBALS[$globalKey] instanceof Language ||
+                                       $GLOBALS[$globalKey] instanceof User ||
+                                       $GLOBALS[$globalKey] instanceof FauxRequest
+                               ) {
+                                       $this->mwGlobals[$globalKey] = clone $GLOBALS[$globalKey];
+                               } else {
+                                       try {
+                                               $this->mwGlobals[$globalKey] = unserialize( serialize( $GLOBALS[$globalKey] ) );
+                                       } catch ( Exception $e ) {
+                                               $this->mwGlobals[$globalKey] = $GLOBALS[$globalKey];
+                                       }
                                }
                        }
                }
@@ -395,6 +725,9 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
         *
         * @throws MWException If the designated global is not an array.
         *
+        * @note To allow changes to global variables to take effect on global service instances,
+        *       call overrideMwServices().
+        *
         * @since 1.21
         */
        protected function mergeMwGlobalArrayValue( $name, $values ) {
@@ -415,6 +748,79 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                $this->setMwGlobals( $name, $merged );
        }
 
+       /**
+        * Stashes the global instance of MediaWikiServices, and installs a new one,
+        * allowing test cases to override settings and services.
+        * The previous instance of MediaWikiServices will be restored on tearDown.
+        *
+        * @since 1.27
+        *
+        * @param Config $configOverrides Configuration overrides for the new MediaWikiServices instance.
+        * @param callable[] $services An associative array of services to re-define. Keys are service
+        *        names, values are callables.
+        *
+        * @return MediaWikiServices
+        * @throws MWException
+        */
+       protected function overrideMwServices( Config $configOverrides = null, array $services = [] ) {
+               if ( !$configOverrides ) {
+                       $configOverrides = new HashConfig();
+               }
+
+               $oldInstance = MediaWikiServices::getInstance();
+               $oldConfigFactory = $oldInstance->getConfigFactory();
+
+               $testConfig = self::makeTestConfig( null, $configOverrides );
+               $newInstance = new MediaWikiServices( $testConfig );
+
+               // Load the default wiring from the specified files.
+               // NOTE: this logic mirrors the logic in MediaWikiServices::newInstance.
+               $wiringFiles = $testConfig->get( 'ServiceWiringFiles' );
+               $newInstance->loadWiringFiles( $wiringFiles );
+
+               // Provide a traditional hook point to allow extensions to configure services.
+               Hooks::run( 'MediaWikiServices', [ $newInstance ] );
+
+               foreach ( $services as $name => $callback ) {
+                       $newInstance->redefineService( $name, $callback );
+               }
+
+               self::installTestServices(
+                       $oldConfigFactory,
+                       $newInstance
+               );
+               MediaWikiServices::forceGlobalInstance( $newInstance );
+
+               return $newInstance;
+       }
+
+       /**
+        * @since 1.27
+        * @param string|Language $lang
+        */
+       public function setUserLang( $lang ) {
+               RequestContext::getMain()->setLanguage( $lang );
+               $this->setMwGlobals( 'wgLang', RequestContext::getMain()->getLanguage() );
+       }
+
+       /**
+        * @since 1.27
+        * @param string|Language $lang
+        */
+       public function setContentLang( $lang ) {
+               if ( $lang instanceof Language ) {
+                       $langCode = $lang->getCode();
+                       $langObj = $lang;
+               } else {
+                       $langCode = $lang;
+                       $langObj = Language::factory( $langCode );
+               }
+               $this->setMwGlobals( [
+                       'wgLanguageCode' => $langCode,
+                       'wgContLang' => $langObj,
+               ] );
+       }
+
        /**
         * Sets the logger for a specified channel, for the duration of the test.
         * @since 1.27
@@ -422,6 +828,9 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
         * @param LoggerInterface $logger
         */
        protected function setLogger( $channel, LoggerInterface $logger ) {
+               // TODO: Once loggers are managed by MediaWikiServices, use
+               //       overrideMwServices() to set loggers.
+
                $provider = LoggerFactory::getProvider();
                $wrappedProvider = TestingAccessWrapper::newFromObject( $provider );
                $singletons = $wrappedProvider->singletons;
@@ -510,7 +919,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
        protected function insertPage( $pageName, $text = 'Sample page for unit test.' ) {
                $title = Title::newFromText( $pageName, 0 );
 
-               $user = User::newFromName( 'UTSysop' );
+               $user = static::getTestSysop()->getUser();
                $comment = __METHOD__ . ': Sample page for unit test.';
 
                // Avoid memory leak...?
@@ -527,8 +936,29 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
        }
 
        /**
-        * Stub. If a test needs to add additional data to the database, it should
-        * implement this method and do so
+        * Stub. If a test suite needs to add additional data to the database, it should
+        * implement this method and do so. This method is called once per test suite
+        * (i.e. once per class).
+        *
+        * Note data added by this method may be removed by resetDB() depending on
+        * the contents of $tablesUsed.
+        *
+        * To add additional data between test function runs, override prepareDB().
+        *
+        * @see addDBData()
+        * @see resetDB()
+        *
+        * @since 1.27
+        */
+       public function addDBDataOnce() {
+       }
+
+       /**
+        * Stub. Subclasses may override this to prepare the database.
+        * Called before every test run (test function or data set).
+        *
+        * @see addDBDataOnce()
+        * @see resetDB()
         *
         * @since 1.18
         */
@@ -540,38 +970,33 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
 
                        # Insert 0 user to prevent FK violations
                        # Anonymous user
-                       $this->db->insert( 'user', [
-                               'user_id' => 0,
-                               'user_name' => 'Anonymous' ], __METHOD__, [ 'IGNORE' ] );
+                       if ( !$this->db->selectField( 'user', '1', [ 'user_id' => 0 ] ) ) {
+                               $this->db->insert( 'user', [
+                                       'user_id' => 0,
+                                       'user_name' => 'Anonymous' ], __METHOD__, [ 'IGNORE' ] );
+                       }
 
                        # Insert 0 page to prevent FK violations
                        # Blank page
-                       $this->db->insert( 'page', [
-                               'page_id' => 0,
-                               'page_namespace' => 0,
-                               'page_title' => ' ',
-                               'page_restrictions' => null,
-                               'page_is_redirect' => 0,
-                               'page_is_new' => 0,
-                               'page_random' => 0,
-                               'page_touched' => $this->db->timestamp(),
-                               'page_latest' => 0,
-                               'page_len' => 0 ], __METHOD__, [ 'IGNORE' ] );
+                       if ( !$this->db->selectField( 'page', '1', [ 'page_id' => 0 ] ) ) {
+                               $this->db->insert( 'page', [
+                                       'page_id' => 0,
+                                       'page_namespace' => 0,
+                                       'page_title' => ' ',
+                                       'page_restrictions' => null,
+                                       'page_is_redirect' => 0,
+                                       'page_is_new' => 0,
+                                       'page_random' => 0,
+                                       'page_touched' => $this->db->timestamp(),
+                                       'page_latest' => 0,
+                                       'page_len' => 0 ], __METHOD__, [ 'IGNORE' ] );
+                       }
                }
 
                User::resetIdByNameCache();
 
                // Make sysop user
-               $user = User::newFromName( 'UTSysop' );
-
-               if ( $user->idForName() == 0 ) {
-                       $user->addToDatabase();
-                       TestUser::setPasswordForUser( $user, 'UTSysopPassword' );
-               }
-
-               // Always set groups, because $this->resetDB() wipes them out
-               $user->addGroup( 'sysop' );
-               $user->addGroup( 'bureaucrat' );
+               $user = static::getTestSysop()->getUser();
 
                // Make 1 page with 1 revision
                $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
@@ -618,6 +1043,52 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                self::$dbSetup = false;
        }
 
+       /**
+        * Setups a database with the given prefix.
+        *
+        * If reuseDB is true and certain conditions apply, it will just change the prefix.
+        * Otherwise, it will clone the tables and change the prefix.
+        *
+        * Clones all tables in the given database (whatever database that connection has
+        * open), to versions with the test prefix.
+        *
+        * @param DatabaseBase $db Database to use
+        * @param string $prefix Prefix to use for test tables
+        * @return bool True if tables were cloned, false if only the prefix was changed
+        */
+       protected static function setupDatabaseWithTestPrefix( DatabaseBase $db, $prefix ) {
+               $tablesCloned = self::listTables( $db );
+               $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix );
+               $dbClone->useTemporaryTables( self::$useTemporaryTables );
+
+               if ( ( $db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
+                       CloneDatabase::changePrefix( $prefix );
+
+                       return false;
+               } else {
+                       $dbClone->cloneTableStructure();
+                       return true;
+               }
+       }
+
+       /**
+        * Set up all test DBs
+        */
+       public function setupAllTestDBs() {
+               global $wgDBprefix;
+
+               self::$oldTablePrefix = $wgDBprefix;
+
+               $testPrefix = $this->dbPrefix();
+
+               // switch to a temporary clone of the database
+               self::setupTestDB( $this->db, $testPrefix );
+
+               if ( self::isUsingExternalStoreDB() ) {
+                       self::setupExternalStoreTestDBs( $testPrefix );
+               }
+       }
+
        /**
         * Creates an empty skeleton of the wiki database by cloning its structure
         * to equivalent tables using the given $prefix. Then sets MediaWiki to
@@ -640,8 +1111,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
         * @throws MWException If the database table prefix is already $prefix
         */
        public static function setupTestDB( DatabaseBase $db, $prefix ) {
-               global $wgDBprefix;
-               if ( $wgDBprefix === $prefix ) {
+               if ( $db->tablePrefix() === $prefix ) {
                        throw new MWException(
                                'Cannot run unit tests, the database prefix is already "' . $prefix . '"' );
                }
@@ -650,51 +1120,128 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                        return;
                }
 
-               $tablesCloned = self::listTables( $db );
-               $dbClone = new CloneDatabase( $db, $tablesCloned, $prefix );
-               $dbClone->useTemporaryTables( self::$useTemporaryTables );
+               // TODO: the below should be re-written as soon as LBFactory, LoadBalancer,
+               // and DatabaseBase no longer use global state.
 
                self::$dbSetup = true;
-               self::$oldTablePrefix = $wgDBprefix;
-
-               if ( ( $db->getType() == 'oracle' || !self::$useTemporaryTables ) && self::$reuseDB ) {
-                       CloneDatabase::changePrefix( $prefix );
 
+               if ( !self::setupDatabaseWithTestPrefix( $db, $prefix ) ) {
                        return;
-               } else {
-                       $dbClone->cloneTableStructure();
                }
 
+               // Assuming this isn't needed for External Store database, and not sure if the procedure
+               // would be available there.
                if ( $db->getType() == 'oracle' ) {
                        $db->query( 'BEGIN FILL_WIKI_INFO; END;' );
                }
        }
 
+       /**
+        * Clones the External Store database(s) for testing
+        *
+        * @param string $testPrefix Prefix for test tables
+        */
+       protected static function setupExternalStoreTestDBs( $testPrefix ) {
+               $connections = self::getExternalStoreDatabaseConnections();
+               foreach ( $connections as $dbw ) {
+                       // Hack: cloneTableStructure sets $wgDBprefix to the unit test
+                       // prefix,.  Even though listTables now uses tablePrefix, that
+                       // itself is populated from $wgDBprefix by default.
+
+                       // We have to set it back, or we won't find the original 'blobs'
+                       // table to copy.
+
+                       $dbw->tablePrefix( self::$oldTablePrefix );
+                       self::setupDatabaseWithTestPrefix( $dbw, $testPrefix );
+               }
+       }
+
+       /**
+        * Gets master database connections for all of the ExternalStoreDB
+        * stores configured in $wgDefaultExternalStore.
+        *
+        * @return array Array of DatabaseBase master connections
+        */
+
+       protected static function getExternalStoreDatabaseConnections() {
+               global $wgDefaultExternalStore;
+
+               $externalStoreDB = ExternalStore::getStoreObject( 'DB' );
+               $defaultArray = (array) $wgDefaultExternalStore;
+               $dbws = [];
+               foreach ( $defaultArray as $url ) {
+                       if ( strpos( $url, 'DB://' ) === 0 ) {
+                               list( $proto, $cluster ) = explode( '://', $url, 2 );
+                               $dbw = $externalStoreDB->getMaster( $cluster );
+                               $dbws[] = $dbw;
+                       }
+               }
+
+               return $dbws;
+       }
+
+       /**
+        * Check whether ExternalStoreDB is being used
+        *
+        * @return bool True if it's being used
+        */
+       protected static function isUsingExternalStoreDB() {
+               global $wgDefaultExternalStore;
+               if ( !$wgDefaultExternalStore ) {
+                       return false;
+               }
+
+               $defaultArray = (array) $wgDefaultExternalStore;
+               foreach ( $defaultArray as $url ) {
+                       if ( strpos( $url, 'DB://' ) === 0 ) {
+                               return true;
+                       }
+               }
+
+               return false;
+       }
+
        /**
         * Empty all tables so they can be repopulated for tests
+        *
+        * @param DatabaseBase $db|null Database to reset
+        * @param array $tablesUsed Tables to reset
         */
-       private function resetDB() {
-               if ( $this->db ) {
-                       if ( $this->db->getType() == 'oracle' ) {
-                               if ( self::$useTemporaryTables ) {
-                                       wfGetLB()->closeAll();
-                                       $this->db = wfGetDB( DB_MASTER );
+       private function resetDB( $db, $tablesUsed ) {
+               if ( $db ) {
+                       $userTables = [ 'user', 'user_groups', 'user_properties' ];
+                       $coreDBDataTables = array_merge( $userTables, [ 'page', 'revision' ] );
+
+                       // If any of the user tables were marked as used, we should clear all of them.
+                       if ( array_intersect( $tablesUsed, $userTables ) ) {
+                               $tablesUsed = array_unique( array_merge( $tablesUsed, $userTables ) );
+                               TestUserRegistry::clear();
+                       }
+
+                       $truncate = in_array( $db->getType(), [ 'oracle', 'mysql' ] );
+                       foreach ( $tablesUsed as $tbl ) {
+                               // TODO: reset interwiki table to its original content.
+                               if ( $tbl == 'interwiki' ) {
+                                       continue;
+                               }
+
+                               if ( $truncate ) {
+                                       $db->query( 'TRUNCATE TABLE ' . $db->tableName( $tbl ), __METHOD__ );
                                } else {
-                                       foreach ( $this->tablesUsed as $tbl ) {
-                                               if ( $tbl == 'interwiki' ) {
-                                                       continue;
-                                               }
-                                               $this->db->query( 'TRUNCATE TABLE ' . $this->db->tableName( $tbl ), __METHOD__ );
-                                       }
+                                       $db->delete( $tbl, '*', __METHOD__ );
                                }
-                       } else {
-                               foreach ( $this->tablesUsed as $tbl ) {
-                                       if ( $tbl == 'interwiki' || $tbl == 'user' ) {
-                                               continue;
-                                       }
-                                       $this->db->delete( $tbl, '*', __METHOD__ );
+
+                               if ( $tbl === 'page' ) {
+                                       // Forget about the pages since they don't
+                                       // exist in the DB.
+                                       LinkCache::singleton()->clear();
                                }
                        }
+
+                       if ( array_intersect( $tablesUsed, $coreDBDataTables ) ) {
+                               // Re-add core DB data that was deleted
+                               $this->addCoreDBData();
+                       }
                }
        }
 
@@ -729,10 +1276,8 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                $this->assertTrue( $value == '', $msg );
        }
 
-       private static function unprefixTable( $tableName ) {
-               global $wgDBprefix;
-
-               return substr( $tableName, strlen( $wgDBprefix ) );
+       private static function unprefixTable( &$tableName, $ind, $prefix ) {
+               $tableName = substr( $tableName, strlen( $prefix ) );
        }
 
        private static function isNotUnittest( $table ) {
@@ -747,16 +1292,15 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
         * @return array
         */
        public static function listTables( $db ) {
-               global $wgDBprefix;
-
-               $tables = $db->listTables( $wgDBprefix, __METHOD__ );
+               $prefix = $db->tablePrefix();
+               $tables = $db->listTables( $prefix, __METHOD__ );
 
                if ( $db->getType() === 'mysql' ) {
                        # bug 43571: cannot clone VIEWs under MySQL
-                       $views = $db->listViews( $wgDBprefix, __METHOD__ );
+                       $views = $db->listViews( $prefix, __METHOD__ );
                        $tables = array_diff( $tables, $views );
                }
-               $tables = array_map( [ __CLASS__, 'unprefixTable' ], $tables );
+               array_walk( $tables, [ __CLASS__, 'unprefixTable' ], $prefix );
 
                // Don't duplicate test tables from the previous fataled run
                $tables = array_filter( $tables, [ __CLASS__, 'isNotUnittest' ] );
@@ -1106,32 +1650,6 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                }
        }
 
-       /**
-        * Check whether we have the 'gzip' commandline utility, will skip
-        * the test whenever "gzip -V" fails.
-        *
-        * Result is cached at the process level.
-        *
-        * @return bool
-        *
-        * @since 1.21
-        */
-       protected function checkHasGzip() {
-               static $haveGzip;
-
-               if ( $haveGzip === null ) {
-                       $retval = null;
-                       wfShellExec( 'gzip -V', $retval );
-                       $haveGzip = ( $retval === 0 );
-               }
-
-               if ( !$haveGzip ) {
-                       $this->markTestSkipped( "Skip test, requires the gzip utility in PATH" );
-               }
-
-               return $haveGzip;
-       }
-
        /**
         * Check if $extName is a loaded PHP extension, will skip the
         * test whenever it is not loaded.
@@ -1149,33 +1667,6 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                return $loaded;
        }
 
-       /**
-        * Asserts that an exception of the specified type occurs when running
-        * the provided code.
-        *
-        * @since 1.21
-        * @deprecated since 1.22 Use setExpectedException
-        *
-        * @param callable $code
-        * @param string $expected
-        * @param string $message
-        */
-       protected function assertException( $code, $expected = 'Exception', $message = '' ) {
-               $pokemons = null;
-
-               try {
-                       call_user_func( $code );
-               } catch ( Exception $pokemons ) {
-                       // Gotta Catch 'Em All!
-               }
-
-               if ( $message === '' ) {
-                       $message = 'An exception of type "' . $expected . '" should have been thrown';
-               }
-
-               $this->assertInstanceOf( $expected, $pokemons, $message );
-       }
-
        /**
         * Asserts that the given string is a valid HTML snippet.
         * Wraps the given string in the required top level tags and
@@ -1283,4 +1774,5 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
        public static function wfResetOutputBuffersBarrier( $buffer ) {
                return $buffer;
        }
+
 }