From f9100ec85d0289483f3cee84bbcbc821289839b8 Mon Sep 17 00:00:00 2001 From: Liam Young Date: Sat, 29 Oct 2016 14:05:25 +0200 Subject: [PATCH] Add unit tests Added unit tests and tidy up calls to initialise Keystone client. Closes-Bug: 1628491 Change-Id: I14ea09539e5781ded75bcfa47108f10e27c816df --- src/lib/charm/openstack/tempest.py | 50 ++- .../{handlers.py => tempest_handlers.py} | 0 test-requirements.txt | 10 +- unit_tests/__init__.py | 49 ++- unit_tests/tempest_output.py | 21 + .../test_lib_charm_openstack_tempest.py | 358 ++++++++++++++++++ unit_tests/test_noop.py | 13 - unit_tests/test_tempest_handlers.py | 34 ++ 8 files changed, 494 insertions(+), 41 deletions(-) rename src/reactive/{handlers.py => tempest_handlers.py} (100%) create mode 100644 unit_tests/tempest_output.py create mode 100644 unit_tests/test_lib_charm_openstack_tempest.py delete mode 100644 unit_tests/test_noop.py create mode 100644 unit_tests/test_tempest_handlers.py diff --git a/src/lib/charm/openstack/tempest.py b/src/lib/charm/openstack/tempest.py index c84b4fb..0719e28 100644 --- a/src/lib/charm/openstack/tempest.py +++ b/src/lib/charm/openstack/tempest.py @@ -65,15 +65,24 @@ class TempestAdminAdapter(adapters.OpenStackRelationAdapter): """Collection keystone information from keystone relation""" return self.relation.credentials() - def init_keystone_client(self): - """Initialise keystone client""" - if self.kc: - return - self.keystone_auth_url = '{}://{}:{}/v2.0'.format( + @property + def ks_client(self): + if not self.kc: + self.init_keystone_client() + return self.kc + + @property + def keystone_auth_url(self): + return '{}://{}:{}/v2.0'.format( 'http', self.keystone_info['service_hostname'], self.keystone_info['service_port'] ) + + def init_keystone_client(self): + """Initialise keystone client""" + if self.kc: + return auth = { 'username': self.keystone_info['service_username'], 'password': self.keystone_info['service_password'], @@ -92,14 +101,15 @@ class TempestAdminAdapter(adapters.OpenStackRelationAdapter): @returns {'access_token' token1, 'secret_token': token2} """ - self.init_keystone_client() - if not self.kc: + if not self.ks_client: return {} - current_creds = self.kc.ec2.list(self.kc.user_id) + current_creds = self.ks_client.ec2.list(self.ks_client.user_id) if current_creds: creds = current_creds[0] else: - creds = self.kc.ec2.create(self.kc.user_id, self.kc.tenant_id) + creds = self.ks_client.ec2.create( + self.ks_client.user_id, + self.ks_client.tenant_id) return {'access_token': creds.access, 'secret_token': creds.secret} @property @@ -108,14 +118,13 @@ class TempestAdminAdapter(adapters.OpenStackRelationAdapter): @returns {'image_id' id1, 'image_alt_id': id2} """ - self.init_keystone_client() image_info = {} try: - glance_endpoint = self.kc.service_catalog.url_for( + glance_endpoint = self.ks_client.service_catalog.url_for( service_type='image', endpoint_type='publicURL') glance_client = glanceclient.Client( - '2', glance_endpoint, token=self.kc.auth_token) + '2', glance_endpoint, token=self.ks_client.auth_token) for image in glance_client.images.list(): if self.uconfig.get('glance-image-name') == image.name: image_info['image_id'] = image.id @@ -138,15 +147,14 @@ class TempestAdminAdapter(adapters.OpenStackRelationAdapter): @returns {'public_network_id' id1, 'router_id': id2} """ - self.init_keystone_client() network_info = {} try: - neutron_ep = self.kc.service_catalog.url_for( + neutron_ep = self.ks_client.service_catalog.url_for( service_type='network', endpoint_type='publicURL') neutron_client = neutronclient.Client( endpoint_url=neutron_ep, - token=self.kc.auth_token) + token=self.ks_client.auth_token) routers = neutron_client.list_routers( name=self.uconfig['router-name']) if len(routers['routers']) == 0: @@ -178,10 +186,9 @@ class TempestAdminAdapter(adapters.OpenStackRelationAdapter): @returns {'flavor_id' id1, 'flavor_alt_id': id2} """ - self.init_keystone_client() compute_info = {} try: - nova_ep = self.kc.service_catalog.url_for( + nova_ep = self.ks_client.service_catalog.url_for( service_type='compute', endpoint_type='publicURL' ) @@ -210,8 +217,9 @@ class TempestAdminAdapter(adapters.OpenStackRelationAdapter): @returns [svc1, svc2, ...]: List of registered services """ - self.init_keystone_client() - services = [svc.name for svc in self.kc.services.list() if svc.enabled] + services = [svc.name + for svc in self.ks_client.services.list() + if svc.enabled] return services @property @@ -338,7 +346,7 @@ class TempestCharm(charm.OpenStackCharm): env['https_proxy'] = conf['https-proxy'] cmd = ['tox', '-e', tox_target] f = open(logfile, "w") - subprocess.call(cmd, cwd=run_dir, stdout=f, stderr=f) + subprocess.call(cmd, cwd=run_dir, stdout=f, stderr=f, env=env) def get_tempest_files(self, branch_name): """Prepare tempest files and directories @@ -357,7 +365,7 @@ class TempestCharm(charm.OpenStackCharm): @return dict: Dictonary of summary data """ summary = {} - with open(logfile) as tempest_log: + with open(logfile, 'r') as tempest_log: summary_line = False for line in tempest_log: if line.strip() == "Totals": diff --git a/src/reactive/handlers.py b/src/reactive/tempest_handlers.py similarity index 100% rename from src/reactive/handlers.py rename to src/reactive/tempest_handlers.py diff --git a/test-requirements.txt b/test-requirements.txt index 9a0bed8..a16d678 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,7 +1,11 @@ -# Unit test requirements -flake8>=2.2.4,<=2.4.1 +# Lint and unit test requirements +flake8 os-testr>=0.4.1 charms.reactive mock>=1.2 coverage>=3.6 -git+https://github.com/openstack/charms.openstack#egg=charms.openstack +requests +python-glanceclient +python-neutronclient +python-novaclient +git+https://github.com/openstack/charms.openstack.git#egg=charms-openstack diff --git a/unit_tests/__init__.py b/unit_tests/__init__.py index d20acca..12469eb 100644 --- a/unit_tests/__init__.py +++ b/unit_tests/__init__.py @@ -1,5 +1,46 @@ -# By design, this unit_tests dir is outside the src charm (layer), -# and it will not be included in the resultant built charm asset. +# Copyright 2016 Canonical Ltd # -# Include unit tests here which are intended to be executable -# from the source charm but not the built charm. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import mock + +sys.path.append('src') +sys.path.append('src/lib') + +# Mock out charmhelpers so that we can test without it. +# also stops sideeffects from occuring. +charmhelpers = mock.MagicMock() +apt_pkg = mock.MagicMock() +sys.modules['apt_pkg'] = apt_pkg +sys.modules['charmhelpers'] = charmhelpers +sys.modules['charmhelpers.core'] = charmhelpers.core +sys.modules['charmhelpers.core.decorators'] = charmhelpers.core.decorators +sys.modules['charmhelpers.core.hookenv'] = charmhelpers.core.hookenv +sys.modules['charmhelpers.core.host'] = charmhelpers.core.host +sys.modules['charmhelpers.core.unitdata'] = charmhelpers.core.unitdata +sys.modules['charmhelpers.core.templating'] = charmhelpers.core.templating +sys.modules['charmhelpers.contrib'] = charmhelpers.contrib +sys.modules['charmhelpers.contrib.openstack'] = charmhelpers.contrib.openstack +sys.modules['charmhelpers.contrib.openstack.utils'] = ( + charmhelpers.contrib.openstack.utils) +sys.modules['charmhelpers.contrib.openstack.templating'] = ( + charmhelpers.contrib.openstack.templating) +sys.modules['charmhelpers.contrib.network'] = charmhelpers.contrib.network +sys.modules['charmhelpers.contrib.network.ip'] = ( + charmhelpers.contrib.network.ip) +sys.modules['charmhelpers.fetch'] = charmhelpers.fetch +sys.modules['charmhelpers.cli'] = charmhelpers.cli +sys.modules['charmhelpers.contrib.hahelpers'] = charmhelpers.contrib.hahelpers +sys.modules['charmhelpers.contrib.hahelpers.cluster'] = ( + charmhelpers.contrib.hahelpers.cluster) diff --git a/unit_tests/tempest_output.py b/unit_tests/tempest_output.py new file mode 100644 index 0000000..13af30f --- /dev/null +++ b/unit_tests/tempest_output.py @@ -0,0 +1,21 @@ +TEMPEST_OUT = """ +====== +Totals +====== +Ran: 62 tests in 64.8226 sec. + - Passed: 21 + - Skipped: 41 + - Expected Fail: 0 + - Unexpected Success: 0 + - Failed: 0 +Sum of execute time for each test: 13.7436 sec. + +============== +Worker Balance +============== + - Worker 0 (62 tests) => 0:00:59.719541 +___________________________________ summary +____________________________________ + smoke: commands succeeded + congratulations :) +""" diff --git a/unit_tests/test_lib_charm_openstack_tempest.py b/unit_tests/test_lib_charm_openstack_tempest.py new file mode 100644 index 0000000..1383e2d --- /dev/null +++ b/unit_tests/test_lib_charm_openstack_tempest.py @@ -0,0 +1,358 @@ +# Copyright 2016 Canonical Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import +from __future__ import print_function + +import io +import mock + +import charms_openstack.test_utils as test_utils +import charm.openstack.tempest as tempest +import unit_tests.tempest_output + + +class Helper(test_utils.PatchHelper): + + def setUp(self): + super().setUp() + self.patch_release(tempest.TempestCharm.release) + + +class TestTempestAdminAdapter(test_utils.PatchHelper): + + def test_init(self): + self.patch_object(tempest.hookenv, 'config') + self.patch_object(tempest.TempestAdminAdapter, 'init_keystone_client') + self.patch_object( + tempest.adapters.OpenStackRelationAdapter, '__init__') + tempest.TempestAdminAdapter('rel2') + self.init_keystone_client.assert_called_once_with() + self.__init__.assert_called_once_with('rel2') + + def test_init_keystone_client(self): + ks_info = { + 'service_hostname': 'kshost', + 'service_port': '5001', + 'service_username': 'user1', + 'service_password': 'pass1', + 'service_tenant_name': 'svc', + 'service_region': 'reg1'} + self.patch_object(tempest.keystoneclient.client, 'Client') + self.patch_object(tempest.hookenv, 'config') + self.patch_object( + tempest.adapters.OpenStackRelationAdapter, '__init__') + self.patch_object( + tempest.TempestAdminAdapter, + 'keystone_info', + new=ks_info) + a = tempest.TempestAdminAdapter('rel2') + a.init_keystone_client() + self.Client.assert_called_once_with( + auth_url='http://kshost:5001/v2.0', + password='pass1', + region_name='reg1', + tenant_name='svc', + username='user1') + + def test_ec2_creds(self): + self.patch_object(tempest.hookenv, 'config') + self.patch_object(tempest.TempestAdminAdapter, 'init_keystone_client') + self.patch_object( + tempest.adapters.OpenStackRelationAdapter, '__init__') + kc = mock.MagicMock() + creds = mock.MagicMock() + creds.access = 'ac2' + creds.secret = 'st2' + kc.user_id = 'bob' + kc.ec2.list = lambda x: [creds] + self.patch_object(tempest.TempestAdminAdapter, 'ks_client', new=kc) + a = tempest.TempestAdminAdapter('rel2') + self.assertEqual(a.ec2_creds, {'access_token': 'ac2', + 'secret_token': 'st2'}) + + def test_image_info(self): + self.patch_object(tempest.hookenv, 'config') + self.patch_object(tempest.TempestAdminAdapter, 'init_keystone_client') + self.patch_object( + tempest.adapters.OpenStackRelationAdapter, '__init__') + self.patch_object(tempest.TempestAdminAdapter, 'ks_client') + self.patch_object(tempest.glanceclient, 'Client') + self.config.return_value = { + 'glance-image-name': 'img1', + 'glance-alt-image-name': 'altimg', + } + kc = mock.MagicMock() + kc.service_catalog.url_for = \ + lambda service_type=None, endpoint_type=None: 'http://glance' + self.ks_client.return_value = kc + img1 = mock.MagicMock() + img1.name = 'img1' + img1.id = 'img1_id' + img2 = mock.MagicMock() + img2.name = 'img2' + img2.id = 'img2_id' + gc = mock.MagicMock() + gc.images.list = lambda: [img1, img2] + self.Client.return_value = gc + a = tempest.TempestAdminAdapter('rel2') + self.assertEqual(a.image_info, {'image_id': 'img1_id'}) + + def test_network_info(self): + self.patch_object(tempest.hookenv, 'config') + self.patch_object(tempest.TempestAdminAdapter, 'init_keystone_client') + self.patch_object( + tempest.adapters.OpenStackRelationAdapter, '__init__') + self.patch_object(tempest.TempestAdminAdapter, 'ks_client') + self.patch_object(tempest.neutronclient, 'Client') + router1 = {'id': '16'} + net1 = {'id': 'pubnet1'} + kc = mock.MagicMock() + kc.service_catalog.url_for = \ + lambda service_type=None, endpoint_type=None: 'http://neutron' + self.ks_client.return_value = kc + self.config.return_value = { + 'router-name': 'route1', + 'network-name': 'net1'} + nc = mock.MagicMock() + nc.list_routers = lambda name=None: {'routers': [router1]} + nc.list_networks = lambda name=None: {'networks': [net1]} + self.Client.return_value = nc + a = tempest.TempestAdminAdapter('rel2') + self.assertEqual( + a.network_info, + {'public_network_id': 'pubnet1', 'router_id': '16'}) + + def test_compute_info(self): + self.patch_object(tempest.hookenv, 'config') + self.patch_object(tempest.TempestAdminAdapter, 'init_keystone_client') + self.patch_object( + tempest.adapters.OpenStackRelationAdapter, '__init__') + ki = { + 'service_username': 'user', + 'service_password': 'pass', + 'service_tenant_name': 'ten', + } + self.patch_object( + tempest.TempestAdminAdapter, + 'keystone_info', + new=ki) + self.patch_object( + tempest.TempestAdminAdapter, + 'keystone_auth_url', + new='auth_url') + self.config.return_value = { + 'flavor-name': 'm3.huuge'} + kc = mock.MagicMock() + kc.service_catalog.url_for = \ + lambda service_type=None, endpoint_type=None: 'http://nova:999/bob' + self.patch_object(tempest.TempestAdminAdapter, 'ks_client', new=kc) + self.patch_object(tempest.novaclient.client, 'Client') + _flavor1 = mock.MagicMock() + _flavor1.name = 'm3.huuge' + _flavor1.id = 'id1' + nc = mock.MagicMock() + nc.flavors.list = lambda: [_flavor1] + self.Client.return_value = nc + a = tempest.TempestAdminAdapter('rel2') + self.assertEqual( + a.compute_info, + { + 'flavor_id': 'id1', + 'nova_base': 'http://nova', + 'nova_endpoint': 'http://nova:999/bob'}) + + def test_get_present_services(self): + self.patch_object(tempest.TempestAdminAdapter, 'init_keystone_client') + self.patch_object( + tempest.adapters.OpenStackRelationAdapter, '__init__') + kc = mock.MagicMock() + svc1 = mock.Mock() + svc2 = mock.Mock() + svc3 = mock.Mock() + svc1.name = 'compute' + svc1.enabled = True + svc2.name = 'image' + svc2.enabled = False + svc3.name = 'network' + svc3.enabled = True + svcs = [svc1, svc2, svc3] + kc.services.list = lambda: svcs + self.patch_object(tempest.TempestAdminAdapter, 'ks_client', new=kc) + a = tempest.TempestAdminAdapter('rel2') + self.assertEqual( + a.get_present_services(), + ['compute', 'network']) + + def test_service_info(self): + self.patch_object(tempest.TempestAdminAdapter, 'init_keystone_client') + self.patch_object( + tempest.adapters.OpenStackRelationAdapter, '__init__') + self.patch_object(tempest.TempestAdminAdapter, 'get_present_services') + self.get_present_services.return_value = ['cinder', 'glance'] + self.patch_object(tempest.hookenv, 'action_get') + self.action_get.return_value = { + 'service-whitelist': 'swift glance'} + a = tempest.TempestAdminAdapter('rel2') + self.assertEqual( + a.service_info, + { + 'ceilometer': 'false', + 'cinder': 'false', + 'glance': 'true', + 'heat': 'false', + 'horizon': 'false', + 'ironic': 'false', + 'neutron': 'false', + 'nova': 'false', + 'sahara': 'false', + 'swift': 'true', + 'trove': 'false', + 'zaqar': 'false', + 'neutron': 'false'}) + + def test_service_info_auto(self): + self.patch_object(tempest.TempestAdminAdapter, 'init_keystone_client') + self.patch_object( + tempest.adapters.OpenStackRelationAdapter, '__init__') + self.patch_object(tempest.TempestAdminAdapter, 'get_present_services') + self.get_present_services.return_value = ['cinder', 'glance'] + self.patch_object(tempest.hookenv, 'action_get') + self.action_get.return_value = { + 'service-whitelist': 'auto'} + a = tempest.TempestAdminAdapter('rel2') + self.assertEqual( + a.service_info, + { + 'ceilometer': 'false', + 'cinder': 'true', + 'glance': 'true', + 'heat': 'false', + 'horizon': 'false', + 'ironic': 'false', + 'neutron': 'false', + 'nova': 'false', + 'sahara': 'false', + 'swift': 'false', + 'trove': 'false', + 'zaqar': 'false', + 'neutron': 'false'}) + + +class TestTempestCharm(Helper): + + def test_setup_directories(self): + self.patch_object(tempest.os.path, 'exists') + self.patch_object(tempest.os, 'mkdir') + self.exists.return_value = False + c = tempest.TempestCharm() + c.setup_directories() + calls = [ + mock.call('/var/lib/tempest'), + mock.call('/var/lib/tempest/logs') + ] + self.mkdir.assert_has_calls(calls) + + def test_setup_git(self): + self.patch_object(tempest.hookenv, 'config') + self.patch_object(tempest.os.path, 'exists') + self.patch_object(tempest.os, 'symlink') + self.patch_object(tempest.fetch, 'install_remote') + self.config.return_value = {'tempest-source': 'git_url'} + self.exists.return_value = False + c = tempest.TempestCharm() + c.setup_git('git_branch', 'git_dir') + self.install_remote.assert_called_once_with( + 'git_url', + branch='git_branch', + depth='1', + dest='git_dir') + self.symlink.assert_called_once_with( + '/var/lib/tempest/tempest.conf', + 'git_dir/tempest/etc/tempest.conf') + + def test_setup_git_noop(self): + self.patch_object(tempest.hookenv, 'config') + self.patch_object(tempest.os.path, 'exists') + self.config.return_value = {'tempest-source': 'git_url'} + self.patch_object(tempest.os, 'symlink') + self.patch_object(tempest.fetch, 'install_remote') + self.exists.return_value = True + c = tempest.TempestCharm() + c.setup_git('git_branch', 'git_dir') + self.assertFalse(self.install_remote.called) + self.assertFalse(self.symlink.called) + + def test_execute_tox(self): + # XXX env seems unused + self.patch_object(tempest.hookenv, 'config') + self.patch_object(tempest.os.environ, 'copy') + self.patch_object(tempest.subprocess, 'call') + os_env = mock.MagicMock() + self.copy.return_value = os_env + self.config.return_value = { + 'http-proxy': 'http://proxy', + 'https-proxy': 'https://proxy', + } + with mock.patch("builtins.open", return_value="fhandle"): + c = tempest.TempestCharm() + c.execute_tox('/tmp/run', '/tmp/t.log', 'py38') + self.call.assert_called_with( + ['tox', '-e', 'py38'], + cwd='/tmp/run', + stderr='fhandle', + stdout='fhandle', + env=os_env) + + def test_get_tempest_files(self): + self.patch_object(tempest.time, 'strftime') + self.strftime.return_value = 'teatime' + c = tempest.TempestCharm() + self.assertEqual( + c.get_tempest_files('br1'), + ('/var/lib/tempest/tempest-br1', + '/var/lib/tempest/logs/run_teatime.log', + '/var/lib/tempest/tempest-br1/tempest')) + + def test_parse_tempest_log(self): + _log_contents = io.StringIO(unit_tests.tempest_output.TEMPEST_OUT) + expect = { + 'expected-fail': '0', + 'failed': '0', + 'passed': '21', + 'skipped': '41', + 'unexpected-success': '0'} + with mock.patch("builtins.open", return_value=_log_contents): + c = tempest.TempestCharm() + self.assertEqual(c.parse_tempest_log("logfile"), expect) + + def test_run_test(self): + self.patch_object(tempest.hookenv, 'action_set') + self.patch_object(tempest.hookenv, 'action_get') + self.action_get.return_value = { + 'branch': 'br1'} + self.patch_object(tempest.TempestCharm, 'get_tempest_files') + self.patch_object(tempest.TempestCharm, 'setup_directories') + self.patch_object(tempest.TempestCharm, 'setup_git') + self.patch_object(tempest.TempestCharm, 'execute_tox') + self.patch_object(tempest.TempestCharm, 'parse_tempest_log') + self.get_tempest_files.return_value = ( + 'git_dir1', + '/var/log/t.log', + '/var/tempest/run') + self.parse_tempest_log.return_value = {'run_info': 'OK'} + c = tempest.TempestCharm() + c.run_test('py39') + self.action_set.assert_called_once_with( + {'run_info': 'OK', 'tempest-logfile': '/var/log/t.log'}) diff --git a/unit_tests/test_noop.py b/unit_tests/test_noop.py deleted file mode 100644 index 37f61c6..0000000 --- a/unit_tests/test_noop.py +++ /dev/null @@ -1,13 +0,0 @@ -import unittest - - -class TestNoOp(unittest.TestCase): - """Placeholder - Write Me!""" - # XXX (beisner): with the charm.openstack vs lib/charm/openstack/tempest - # module namespace collision, and with the hard requirement to have some - # sort of unit test passing, here is a temporary inert noop test. After - # charms_openstack module is completed, and this tempest charm is - # refactored to use it, revisit this and add actual unit tests. - def test_noop(self): - """Test Nothing""" - pass diff --git a/unit_tests/test_tempest_handlers.py b/unit_tests/test_tempest_handlers.py new file mode 100644 index 0000000..7813475 --- /dev/null +++ b/unit_tests/test_tempest_handlers.py @@ -0,0 +1,34 @@ +# Copyright 2016 Canonical Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import +from __future__ import print_function + +import reactive.tempest_handlers as handlers +import charms_openstack.test_utils as test_utils + + +class TestRegisteredHooks(test_utils.TestRegisteredHooks): + + def test_hooks(self): + defaults = [] + hook_set = { + 'when': { + 'install_packages': ('charm.installed',), + 'assess_status': ('charm.installed',), + } + } + # test that the hooks were registered via the + # reactive.barbican_handlers + self.registered_hooks_test_helper(handlers, hook_set, defaults)