Move `register_controller` into `base_test`. (#493)

So the controller objects of a class is encapsulatd within the test
class itself. This makes the structure cleaner and gets rid of the
awkward passing of a partial function from test runner to each test
class.
diff --git a/mobly/base_test.py b/mobly/base_test.py
index e64c437..e4139d9 100644
--- a/mobly/base_test.py
+++ b/mobly/base_test.py
@@ -46,6 +46,30 @@
     """Raised for exceptions that occured in BaseTestClass."""
 
 
+def _verify_controller_module(module):
+    """Verifies a module object follows the required interface for
+    controllers.
+
+    Args:
+        module: An object that is a controller module. This is usually
+            imported with import statements or loaded by importlib.
+
+    Raises:
+        ControllerError: if the module does not match the Mobly controller
+            interface, or one of the required members is null.
+    """
+    required_attributes = ('create', 'destroy', 'MOBLY_CONTROLLER_CONFIG_NAME')
+    for attr in required_attributes:
+        if not hasattr(module, attr):
+            raise signals.ControllerError(
+                'Module %s missing required controller module attribute'
+                ' %s.' % (module.__name__, attr))
+        if not getattr(module, attr):
+            raise signals.ControllerError(
+                'Controller interface %s in %s cannot be null.' %
+                (attr, module.__name__))
+
+
 class BaseTestClass(object):
     """Base class for all test classes to inherit from.
 
@@ -77,8 +101,6 @@
             objects.
         user_params: dict, custom parameters from user, to be consumed by
             the test logic.
-        register_controller: func, used by test classes to register
-            controller modules.
     """
 
     TAG = None
@@ -106,12 +128,14 @@
         self.controller_configs = configs.controller_configs
         self.test_bed_name = configs.test_bed_name
         self.user_params = configs.user_params
-        self.register_controller = configs.register_controller
         self.results = records.TestResult()
         self.summary_writer = configs.summary_writer
         # Deprecated, use `self.current_test_info.name`.
         self.current_test_name = None
         self._generated_test_table = collections.OrderedDict()
+        # Controller object management.
+        self._controller_registry = {}
+        self._controller_destructors = {}
 
     def __enter__(self):
         return self
@@ -171,6 +195,157 @@
                 logging.warning('Missing optional user param "%s" in '
                                 'configuration, continue.', name)
 
+    def register_controller(self, module, required=True, min_number=1):
+        """Loads a controller module and returns its loaded devices.
+
+        A Mobly controller module is a Python lib that can be used to control
+        a device, service, or equipment. To be Mobly compatible, a controller
+        module needs to have the following members:
+
+        ```
+        def create(configs):
+            [Required] Creates controller objects from configurations.
+
+            Args:
+                configs: A list of serialized data like string/dict. Each
+                    element of the list is a configuration for a controller
+                    object.
+
+            Returns:
+                A list of objects.
+
+        def destroy(objects):
+            [Required] Destroys controller objects created by the create
+            function. Each controller object shall be properly cleaned up
+            and all the resources held should be released, e.g. memory
+            allocation, sockets, file handlers etc.
+
+            Args:
+                A list of controller objects created by the create function.
+
+        def get_info(objects):
+            [Optional] Gets info from the controller objects used in a test
+            run. The info will be included in test_summary.yaml under
+            the key 'ControllerInfo'. Such information could include unique
+            ID, version, or anything that could be useful for describing the
+            test bed and debugging.
+
+            Args:
+                objects: A list of controller objects created by the create
+                    function.
+
+            Returns:
+                A list of json serializable objects, each represents the
+                info of a controller object. The order of the info object
+                should follow that of the input objects.
+        ```
+
+        Registering a controller module declares a test class's dependency the
+        controller. If the module config exists and the module matches the
+        controller interface, controller objects will be instantiated with
+        corresponding configs. The module should be imported first.
+
+        Args:
+            module: A module that follows the controller module interface.
+            required: A bool. If True, failing to register the specified
+                controller module raises exceptions. If False, the objects
+                failed to instantiate will be skipped.
+            min_number: An integer that is the minimum number of controller
+                objects to be created. Default is one, since you should not
+                register a controller module without expecting at least one
+                object.
+
+        Returns:
+            A list of controller objects instantiated from controller_module, or
+            None if no config existed for this controller and it was not a
+            required controller.
+
+        Raises:
+            ControllerError:
+                * The controller module has already been registered.
+                * The actual number of objects instantiated is less than the
+                * `min_number`.
+                * `required` is True and no corresponding config can be found.
+                * Any other error occurred in the registration process.
+        """
+        _verify_controller_module(module)
+        # Use the module's name as the ref name
+        module_ref_name = module.__name__.split('.')[-1]
+        if module_ref_name in self._controller_registry:
+            raise signals.ControllerError(
+                'Controller module %s has already been registered. It cannot '
+                'be registered again.' % module_ref_name)
+        # Create controller objects.
+        create = module.create
+        module_config_name = module.MOBLY_CONTROLLER_CONFIG_NAME
+        if module_config_name not in self.controller_configs:
+            if required:
+                raise signals.ControllerError(
+                    'No corresponding config found for %s' %
+                    module_config_name)
+            logging.warning(
+                'No corresponding config found for optional controller %s',
+                module_config_name)
+            return None
+        try:
+            # Make a deep copy of the config to pass to the controller module,
+            # in case the controller module modifies the config internally.
+            original_config = self.controller_configs[module_config_name]
+            controller_config = copy.deepcopy(original_config)
+            objects = create(controller_config)
+        except:
+            logging.exception(
+                'Failed to initialize objects for controller %s, abort!',
+                module_config_name)
+            raise
+        if not isinstance(objects, list):
+            raise signals.ControllerError(
+                'Controller module %s did not return a list of objects, abort.'
+                % module_ref_name)
+        # Check we got enough controller objects to continue.
+        actual_number = len(objects)
+        if actual_number < min_number:
+            module.destroy(objects)
+            raise signals.ControllerError(
+                'Expected to get at least %d controller objects, got %d.' %
+                (min_number, actual_number))
+        # Save a shallow copy of the list for internal usage, so tests can't
+        # affect internal registry by manipulating the object list.
+        self._controller_registry[module_ref_name] = copy.copy(objects)
+        # Collect controller information and write to test result.
+        # Implementation of 'get_info' is optional for a controller module.
+        if hasattr(module, 'get_info'):
+            controller_info = module.get_info(copy.copy(objects))
+            logging.debug('Controller %s: %s', module_config_name,
+                          controller_info)
+            self.results.add_controller_info(module_config_name,
+                                             controller_info)
+        else:
+            logging.warning('No optional debug info found for controller %s. '
+                            'To provide it, implement get_info in this '
+                            'controller module.', module_config_name)
+        logging.debug('Found %d objects for controller %s', len(objects),
+                      module_config_name)
+        destroy_func = module.destroy
+        self._controller_destructors[module_ref_name] = destroy_func
+        return objects
+
+    def _unregister_controllers(self):
+        """Destroy controller objects and clear internal registry.
+
+        This will be called after each test class.
+        """
+        # TODO(xpconanfan): actually record these errors instead of just
+        # logging them.
+        for name, destroy in self._controller_destructors.items():
+            try:
+                logging.debug('Destroying %s.', name)
+                destroy(self._controller_registry[name])
+            except:
+                logging.exception('Exception occurred destroying %s.', name)
+        self._controller_registry = {}
+        self._controller_destructors = {}
+
     def _setup_generated_tests(self):
         """Proxy function to guarantee the base implementation of
         setup_generated_tests is called.
@@ -727,6 +902,7 @@
             raise e
         finally:
             self._teardown_class()
+            self._unregister_controllers()
             logging.info('Summary for test class %s: %s', self.TAG,
                          self.results.summary_str())
 
diff --git a/mobly/config_parser.py b/mobly/config_parser.py
index b873af0..2278ca6 100644
--- a/mobly/config_parser.py
+++ b/mobly/config_parser.py
@@ -166,8 +166,6 @@
         controller_configs: dict, configs used for instantiating controller
             objects.
         user_params: dict, all the parameters to be consumed by the test logic.
-        register_controller: func, used by test classes to register controller
-            modules.
         summary_writer: records.TestSummaryWriter, used to write elements to
             the test result summary file.
         test_class_name_suffix: string, suffix to append to the class name for
@@ -180,7 +178,6 @@
         self.test_bed_name = None
         self.controller_configs = None
         self.user_params = None
-        self.register_controller = None
         self.summary_writer = None
         self.test_class_name_suffix = None
 
@@ -192,5 +189,4 @@
     def __str__(self):
         content = dict(self.__dict__)
         content.pop('summary_writer')
-        content.pop('register_controller')
         return pprint.pformat(content)
diff --git a/mobly/records.py b/mobly/records.py
index ad37cad..051ff9c 100644
--- a/mobly/records.py
+++ b/mobly/records.py
@@ -488,10 +488,10 @@
                 setattr(sum_result, name, l_value + r_value)
             elif isinstance(r_value, dict):
                 # '+' operator for TestResult is only valid when multiple
-                # TestResult objs were created in the same test run, which means
-                # the controller info would be the same across all of them.
+                # TestResult objs were created in the same test run, use the
+                # r-value which is more up to date.
                 # TODO(xpconanfan): have a better way to validate this situation.
-                setattr(sum_result, name, l_value)
+                setattr(sum_result, name, r_value)
         return sum_result
 
     def add_record(self, record):
diff --git a/mobly/test_runner.py b/mobly/test_runner.py
index e4fa9d8..9c57715 100644
--- a/mobly/test_runner.py
+++ b/mobly/test_runner.py
@@ -17,8 +17,6 @@
 standard_library.install_aliases()
 
 import argparse
-import copy
-import functools
 import inspect
 import logging
 import os
@@ -180,73 +178,6 @@
         print(name)
 
 
-def verify_controller_module(module):
-    """Verifies a module object follows the required interface for
-    controllers.
-
-    A Mobly controller module is a Python lib that can be used to control
-    a device, service, or equipment. To be Mobly compatible, a controller
-    module needs to have the following members:
-
-        def create(configs):
-            [Required] Creates controller objects from configurations.
-
-            Args:
-                configs: A list of serialized data like string/dict. Each
-                    element of the list is a configuration for a controller
-                    object.
-            Returns:
-                A list of objects.
-
-        def destroy(objects):
-            [Required] Destroys controller objects created by the create
-            function. Each controller object shall be properly cleaned up
-            and all the resources held should be released, e.g. memory
-            allocation, sockets, file handlers etc.
-
-            Args:
-                A list of controller objects created by the create function.
-
-        def get_info(objects):
-            [Optional] Gets info from the controller objects used in a test
-            run. The info will be included in test_summary.yaml under
-            the key 'ControllerInfo'. Such information could include unique
-            ID, version, or anything that could be useful for describing the
-            test bed and debugging.
-
-            Args:
-                objects: A list of controller objects created by the create
-                    function.
-            Returns:
-                A list of json serializable objects, each represents the
-                info of a controller object. The order of the info object
-                should follow that of the input objects.
-
-    Registering a controller module declares a test class's dependency the
-    controller. If the module config exists and the module matches the
-    controller interface, controller objects will be instantiated with
-    corresponding configs. The module should be imported first.
-
-    Args:
-        module: An object that is a controller module. This is usually
-            imported with import statements or loaded by importlib.
-
-    Raises:
-        ControllerError: if the module does not match the Mobly controller
-            interface, or one of the required members is null.
-    """
-    required_attributes = ('create', 'destroy', 'MOBLY_CONTROLLER_CONFIG_NAME')
-    for attr in required_attributes:
-        if not hasattr(module, attr):
-            raise signals.ControllerError(
-                'Module %s missing required controller module attribute'
-                ' %s.' % (module.__name__, attr))
-        if not getattr(module, attr):
-            raise signals.ControllerError(
-                'Controller interface %s in %s cannot be null.' %
-                (attr, module.__name__))
-
-
 class TestRunner(object):
     """The class that instantiates test classes, executes tests, and
     report results.
@@ -288,10 +219,6 @@
         self.results = records.TestResult()
         self._test_run_infos = []
 
-        # Controller management. These members will be updated for each class.
-        self._controller_registry = {}
-        self._controller_destructors = {}
-
         self._log_path = None
 
     def setup_logger(self):
@@ -412,8 +339,6 @@
                 # Set up the test-specific config
                 test_config = test_run_info.config.copy()
                 test_config.log_path = self._log_path
-                test_config.register_controller = functools.partial(
-                    self._register_controller, test_config)
                 test_config.summary_writer = summary_writer
                 test_config.test_class_name_suffix = test_run_info.test_class_name_suffix
                 try:
@@ -425,8 +350,6 @@
                     logging.warning(
                         'Abort all subsequent test classes. Reason: %s', e)
                     raise
-                finally:
-                    self._unregister_controllers()
         finally:
             # Write controller info and summary to summary file.
             summary_writer.dump(self.results.controller_info,
@@ -439,112 +362,3 @@
                 self.results.summary_str())
             logging.info(msg.strip())
             self._teardown_logger()
-
-    def _register_controller(self, config, module, required=True,
-                             min_number=1):
-        """Loads a controller module and returns its loaded devices.
-
-        See the docstring of verify_controller_module() for a description of
-        what API a controller module must implement to be compatible with this
-        method.
-
-        Args:
-            config: A config_parser.TestRunConfig object.
-            module: A module that follows the controller module interface.
-            required: A bool. If True, failing to register the specified
-                controller module raises exceptions. If False, the objects
-                failed to instantiate will be skipped.
-            min_number: An integer that is the minimum number of controller
-                objects to be created. Default is one, since you should not
-                register a controller module without expecting at least one
-                object.
-
-        Returns:
-            A list of controller objects instantiated from controller_module, or
-            None if no config existed for this controller and it was not a
-            required controller.
-
-        Raises:
-            ControllerError:
-                * The controller module has already been registered.
-                * The actual number of objects instantiated is less than the
-                * `min_number`.
-                * `required` is True and no corresponding config can be found.
-                * Any other error occurred in the registration process.
-
-        """
-        verify_controller_module(module)
-        # Use the module's name as the ref name
-        module_ref_name = module.__name__.split('.')[-1]
-        if module_ref_name in self._controller_registry:
-            raise signals.ControllerError(
-                'Controller module %s has already been registered. It cannot '
-                'be registered again.' % module_ref_name)
-        # Create controller objects.
-        create = module.create
-        module_config_name = module.MOBLY_CONTROLLER_CONFIG_NAME
-        if module_config_name not in config.controller_configs:
-            if required:
-                raise signals.ControllerError(
-                    'No corresponding config found for %s' %
-                    module_config_name)
-            logging.warning(
-                'No corresponding config found for optional controller %s',
-                module_config_name)
-            return None
-        try:
-            # Make a deep copy of the config to pass to the controller module,
-            # in case the controller module modifies the config internally.
-            original_config = config.controller_configs[module_config_name]
-            controller_config = copy.deepcopy(original_config)
-            objects = create(controller_config)
-        except:
-            logging.exception(
-                'Failed to initialize objects for controller %s, abort!',
-                module_config_name)
-            raise
-        if not isinstance(objects, list):
-            raise signals.ControllerError(
-                'Controller module %s did not return a list of objects, abort.'
-                % module_ref_name)
-        # Check we got enough controller objects to continue.
-        actual_number = len(objects)
-        if actual_number < min_number:
-            module.destroy(objects)
-            raise signals.ControllerError(
-                'Expected to get at least %d controller objects, got %d.' %
-                (min_number, actual_number))
-        # Save a shallow copy of the list for internal usage, so tests can't
-        # affect internal registry by manipulating the object list.
-        self._controller_registry[module_ref_name] = copy.copy(objects)
-        # Collect controller information and write to test result.
-        # Implementation of 'get_info' is optional for a controller module.
-        if hasattr(module, 'get_info'):
-            controller_info = module.get_info(copy.copy(objects))
-            logging.debug('Controller %s: %s', module_config_name,
-                          controller_info)
-            self.results.add_controller_info(module_config_name,
-                                             controller_info)
-        else:
-            logging.warning('No optional debug info found for controller %s. '
-                            'To provide it, implement get_info in this '
-                            'controller module.', module_config_name)
-        logging.debug('Found %d objects for controller %s', len(objects),
-                      module_config_name)
-        destroy_func = module.destroy
-        self._controller_destructors[module_ref_name] = destroy_func
-        return objects
-
-    def _unregister_controllers(self):
-        """Destroy controller objects and clear internal registry.
-
-        This will be called after each test class.
-        """
-        for name, destroy in self._controller_destructors.items():
-            try:
-                logging.debug('Destroying %s.', name)
-                destroy(self._controller_registry[name])
-            except:
-                logging.exception('Exception occurred destroying %s.', name)
-        self._controller_registry = {}
-        self._controller_destructors = {}
diff --git a/tests/mobly/base_test_test.py b/tests/mobly/base_test_test.py
index d029219..898ab5d 100755
--- a/tests/mobly/base_test_test.py
+++ b/tests/mobly/base_test_test.py
@@ -29,6 +29,7 @@
 from mobly import signals
 
 from tests.lib import utils
+from tests.lib import mock_controller
 
 MSG_EXPECTED_EXCEPTION = "This is an expected exception."
 MSG_EXPECTED_TEST_FAILURE = "This is an expected test failure."
@@ -45,6 +46,15 @@
     """A custom exception class used for tests in this module."""
 
 
+class MockEmptyBaseTest(base_test.BaseTestClass):
+    """Stub used to test functionalities not specific to a class
+    implementation.
+    """
+
+    def test_func(self):
+        pass
+
+
 class BaseTestTest(unittest.TestCase):
     def setUp(self):
         self.tmp_dir = tempfile.mkdtemp()
@@ -52,6 +62,7 @@
         self.summary_file = os.path.join(self.tmp_dir, 'summary.yaml')
         self.mock_test_cls_configs.summary_writer = records.TestSummaryWriter(
             self.summary_file)
+        self.mock_test_cls_configs.controller_configs = {}
         self.mock_test_cls_configs.log_path = self.tmp_dir
         self.mock_test_cls_configs.user_params = {"some_param": "hahaha"}
         self.mock_test_cls_configs.reporter = mock.MagicMock()
@@ -1773,6 +1784,113 @@
                 self.assertIsNotNone(c['timestamp'])
         self.assertTrue(hit)
 
+    def test_register_controller_no_config(self):
+        bt_cls = MockEmptyBaseTest(self.mock_test_cls_configs)
+        with self.assertRaisesRegex(signals.ControllerError,
+                                    'No corresponding config found for'):
+            bt_cls.register_controller(mock_controller)
+
+    def test_register_controller_no_config_for_not_required(self):
+        bt_cls = MockEmptyBaseTest(self.mock_test_cls_configs)
+        self.assertIsNone(
+            bt_cls.register_controller(mock_controller, required=False))
+
+    def test_register_controller_dup_register(self):
+        """Verifies correctness of registration, internal tally of controllers
+        objects, and the right error happen when a controller module is
+        registered twice.
+        """
+        mock_test_config = self.mock_test_cls_configs.copy()
+        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+        mock_test_config.controller_configs = {
+            mock_ctrlr_config_name: ['magic1', 'magic2']
+        }
+        bt_cls = MockEmptyBaseTest(mock_test_config)
+        bt_cls.register_controller(mock_controller)
+        registered_name = 'mock_controller'
+        self.assertTrue(registered_name in bt_cls._controller_registry)
+        mock_ctrlrs = bt_cls._controller_registry[registered_name]
+        self.assertEqual(mock_ctrlrs[0].magic, 'magic1')
+        self.assertEqual(mock_ctrlrs[1].magic, 'magic2')
+        self.assertTrue(bt_cls._controller_destructors[registered_name])
+        expected_msg = 'Controller module .* has already been registered.'
+        with self.assertRaisesRegex(signals.ControllerError, expected_msg):
+            bt_cls.register_controller(mock_controller)
+
+    def test_register_controller_no_get_info(self):
+        mock_test_config = self.mock_test_cls_configs.copy()
+        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+        get_info = getattr(mock_controller, 'get_info')
+        delattr(mock_controller, 'get_info')
+        try:
+            mock_test_config.controller_configs = {
+                mock_ctrlr_config_name: ['magic1', 'magic2']
+            }
+            bt_cls = MockEmptyBaseTest(mock_test_config)
+            bt_cls.register_controller(mock_controller)
+            self.assertEqual(bt_cls.results.controller_info, {})
+        finally:
+            setattr(mock_controller, 'get_info', get_info)
+
+    def test_register_controller_return_value(self):
+        mock_test_config = self.mock_test_cls_configs.copy()
+        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+        mock_test_config.controller_configs = {
+            mock_ctrlr_config_name: ['magic1', 'magic2']
+        }
+        bt_cls = MockEmptyBaseTest(mock_test_config)
+        magic_devices = bt_cls.register_controller(mock_controller)
+        self.assertEqual(magic_devices[0].magic, 'magic1')
+        self.assertEqual(magic_devices[1].magic, 'magic2')
+
+    def test_register_controller_change_return_value(self):
+        mock_test_config = self.mock_test_cls_configs.copy()
+        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+        mock_test_config.controller_configs = {
+            mock_ctrlr_config_name: ['magic1', 'magic2']
+        }
+        bt_cls = MockEmptyBaseTest(mock_test_config)
+        magic_devices = bt_cls.register_controller(mock_controller)
+        magic1 = magic_devices.pop(0)
+        self.assertIs(magic1,
+                      bt_cls._controller_registry['mock_controller'][0])
+        self.assertEqual(
+            len(bt_cls._controller_registry['mock_controller']), 2)
+
+    def test_register_controller_less_than_min_number(self):
+        mock_test_config = self.mock_test_cls_configs.copy()
+        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+        mock_test_config.controller_configs = {
+            mock_ctrlr_config_name: ['magic1', 'magic2']
+        }
+        bt_cls = MockEmptyBaseTest(mock_test_config)
+        expected_msg = 'Expected to get at least 3 controller objects, got 2.'
+        with self.assertRaisesRegex(signals.ControllerError, expected_msg):
+            bt_cls.register_controller(mock_controller, min_number=3)
+
+    def test_verify_controller_module(self):
+        base_test._verify_controller_module(mock_controller)
+
+    def test_verify_controller_module_null_attr(self):
+        try:
+            tmp = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+            mock_controller.MOBLY_CONTROLLER_CONFIG_NAME = None
+            msg = 'Controller interface .* in .* cannot be null.'
+            with self.assertRaisesRegex(signals.ControllerError, msg):
+                base_test._verify_controller_module(mock_controller)
+        finally:
+            mock_controller.MOBLY_CONTROLLER_CONFIG_NAME = tmp
+
+    def test_verify_controller_module_missing_attr(self):
+        try:
+            tmp = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+            delattr(mock_controller, 'MOBLY_CONTROLLER_CONFIG_NAME')
+            msg = 'Module .* missing required controller module attribute'
+            with self.assertRaisesRegex(signals.ControllerError, msg):
+                base_test._verify_controller_module(mock_controller)
+        finally:
+            setattr(mock_controller, 'MOBLY_CONTROLLER_CONFIG_NAME', tmp)
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/tests/mobly/test_runner_test.py b/tests/mobly/test_runner_test.py
index b369231..d86f60c 100755
--- a/tests/mobly/test_runner_test.py
+++ b/tests/mobly/test_runner_test.py
@@ -16,7 +16,6 @@
 import logging
 import mock
 import os
-import platform
 import re
 import shutil
 import tempfile
@@ -56,93 +55,6 @@
     def tearDown(self):
         shutil.rmtree(self.tmp_dir)
 
-    def test_register_controller_no_config(self):
-        tr = test_runner.TestRunner(self.log_dir, self.test_bed_name)
-        with self.assertRaisesRegex(signals.ControllerError,
-                                    'No corresponding config found for'):
-            tr._register_controller(self.base_mock_test_config,
-                                    mock_controller)
-
-    def test_register_controller_no_config_no_register(self):
-        tr = test_runner.TestRunner(self.log_dir, self.test_bed_name)
-        self.assertIsNone(
-            tr._register_controller(
-                self.base_mock_test_config, mock_controller, required=False))
-
-    def test_register_controller_dup_register(self):
-        """Verifies correctness of registration, internal tally of controllers
-        objects, and the right error happen when a controller module is
-        registered twice.
-        """
-        mock_test_config = self.base_mock_test_config.copy()
-        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
-        mock_test_config.controller_configs = {
-            mock_ctrlr_config_name: ['magic1', 'magic2']
-        }
-        tr = test_runner.TestRunner(self.log_dir, self.test_bed_name)
-        tr._register_controller(mock_test_config, mock_controller)
-        registered_name = 'mock_controller'
-        self.assertTrue(registered_name in tr._controller_registry)
-        mock_ctrlrs = tr._controller_registry[registered_name]
-        self.assertEqual(mock_ctrlrs[0].magic, 'magic1')
-        self.assertEqual(mock_ctrlrs[1].magic, 'magic2')
-        self.assertTrue(tr._controller_destructors[registered_name])
-        expected_msg = 'Controller module .* has already been registered.'
-        with self.assertRaisesRegex(signals.ControllerError, expected_msg):
-            tr._register_controller(mock_test_config, mock_controller)
-
-    def test_register_controller_no_get_info(self):
-        mock_test_config = self.base_mock_test_config.copy()
-        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
-        get_info = getattr(mock_controller, 'get_info')
-        delattr(mock_controller, 'get_info')
-        try:
-            mock_test_config.controller_configs = {
-                mock_ctrlr_config_name: ['magic1', 'magic2']
-            }
-            tr = test_runner.TestRunner(self.log_dir, self.test_bed_name)
-            tr._register_controller(mock_test_config, mock_controller)
-            self.assertEqual(tr.results.controller_info, {})
-        finally:
-            setattr(mock_controller, 'get_info', get_info)
-
-    def test_register_controller_return_value(self):
-        mock_test_config = self.base_mock_test_config.copy()
-        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
-        mock_test_config.controller_configs = {
-            mock_ctrlr_config_name: ['magic1', 'magic2']
-        }
-        tr = test_runner.TestRunner(self.log_dir, self.test_bed_name)
-        magic_devices = tr._register_controller(mock_test_config,
-                                                mock_controller)
-        self.assertEqual(magic_devices[0].magic, 'magic1')
-        self.assertEqual(magic_devices[1].magic, 'magic2')
-
-    def test_register_controller_change_return_value(self):
-        mock_test_config = self.base_mock_test_config.copy()
-        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
-        mock_test_config.controller_configs = {
-            mock_ctrlr_config_name: ['magic1', 'magic2']
-        }
-        tr = test_runner.TestRunner(self.log_dir, self.test_bed_name)
-        magic_devices = tr._register_controller(mock_test_config,
-                                                mock_controller)
-        magic1 = magic_devices.pop(0)
-        self.assertIs(magic1, tr._controller_registry['mock_controller'][0])
-        self.assertEqual(len(tr._controller_registry['mock_controller']), 2)
-
-    def test_register_controller_less_than_min_number(self):
-        mock_test_config = self.base_mock_test_config.copy()
-        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
-        mock_test_config.controller_configs = {
-            mock_ctrlr_config_name: ['magic1', 'magic2']
-        }
-        tr = test_runner.TestRunner(self.log_dir, self.test_bed_name)
-        expected_msg = 'Expected to get at least 3 controller objects, got 2.'
-        with self.assertRaisesRegex(signals.ControllerError, expected_msg):
-            tr._register_controller(
-                mock_test_config, mock_controller, min_number=3)
-
     def test_run_twice(self):
         """Verifies that:
         1. Repeated run works properly.
@@ -162,13 +74,9 @@
         tr = test_runner.TestRunner(self.log_dir, self.test_bed_name)
         tr.add_test_class(mock_test_config, integration_test.IntegrationTest)
         tr.run()
-        self.assertFalse(tr._controller_registry)
-        self.assertFalse(tr._controller_destructors)
         self.assertTrue(
             mock_test_config.controller_configs[mock_ctrlr_config_name][0])
         tr.run()
-        self.assertFalse(tr._controller_registry)
-        self.assertFalse(tr._controller_destructors)
         results = tr.results.summary_dict()
         self.assertEqual(results['Requested'], 2)
         self.assertEqual(results['Executed'], 2)
@@ -253,8 +161,6 @@
         tr.add_test_class(mock_test_config, integration2_test.Integration2Test)
         tr.add_test_class(mock_test_config, integration_test.IntegrationTest)
         tr.run()
-        self.assertFalse(tr._controller_registry)
-        self.assertFalse(tr._controller_destructors)
         results = tr.results.summary_dict()
         self.assertEqual(results['Requested'], 2)
         self.assertEqual(results['Executed'], 2)
@@ -342,29 +248,6 @@
         with self.assertRaisesRegex(test_runner.Error, 'No tests to execute.'):
             tr.run()
 
-    def test_verify_controller_module(self):
-        test_runner.verify_controller_module(mock_controller)
-
-    def test_verify_controller_module_null_attr(self):
-        try:
-            tmp = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
-            mock_controller.MOBLY_CONTROLLER_CONFIG_NAME = None
-            msg = 'Controller interface .* in .* cannot be null.'
-            with self.assertRaisesRegex(signals.ControllerError, msg):
-                test_runner.verify_controller_module(mock_controller)
-        finally:
-            mock_controller.MOBLY_CONTROLLER_CONFIG_NAME = tmp
-
-    def test_verify_controller_module_missing_attr(self):
-        try:
-            tmp = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
-            delattr(mock_controller, 'MOBLY_CONTROLLER_CONFIG_NAME')
-            msg = 'Module .* missing required controller module attribute'
-            with self.assertRaisesRegex(signals.ControllerError, msg):
-                test_runner.verify_controller_module(mock_controller)
-        finally:
-            setattr(mock_controller, 'MOBLY_CONTROLLER_CONFIG_NAME', tmp)
-
     @mock.patch(
         'mobly.test_runner._find_test_class',
         return_value=type('SampleTest', (), {}))
diff --git a/tests/mobly/test_suite_test.py b/tests/mobly/test_suite_test.py
index 7e15f25..9b1efc1 100755
--- a/tests/mobly/test_suite_test.py
+++ b/tests/mobly/test_suite_test.py
@@ -55,30 +55,24 @@
     def tearDown(self):
         shutil.rmtree(self.tmp_dir)
 
-    def test_controller_object_not_persistent_across_classes_in_the_same_run(
-            self):
-        self.foo_test_controller_obj_id = None
-        self.bar_test_controller_obj_id = None
+    def test_controller_object_not_persistent_across_classes(self):
         test_run_config = self.base_mock_test_config.copy()
         test_run_config.controller_configs = {'MagicDevice': [{'serial': 1}]}
 
         class FooTest(base_test.BaseTestClass):
-            def setup_class(cls):
-                cls.controller = cls.register_controller(mock_controller)[0]
-                self.foo_test_controller_obj_id = id(cls.controller)
+            def setup_class(cls1):
+                self.controller1 = cls1.register_controller(mock_controller)[0]
 
         class BarTest(base_test.BaseTestClass):
-            def setup_class(cls):
-                cls.controller = cls.register_controller(mock_controller)[0]
-                self.bar_test_controller_obj_id = id(cls.controller)
+            def setup_class(cls2):
+                self.controller2 = cls2.register_controller(mock_controller)[0]
 
         tr = test_runner.TestRunner(self.tmp_dir,
                                     test_run_config.test_bed_name)
         tr.add_test_class(test_run_config, FooTest)
         tr.add_test_class(test_run_config, BarTest)
         tr.run()
-        self.assertNotEqual(self.foo_test_controller_obj_id,
-                            self.bar_test_controller_obj_id)
+        self.assertIsNot(self.controller1, self.controller2)
 
 
 if __name__ == "__main__":