diff --git a/.gitignore b/.gitignore index 9928c23..2e74b93 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,6 @@ output/*/index.html # Sphinx docs/_build + +# Reports +reports diff --git a/README.rst b/README.rst index c712d85..5378d86 100644 --- a/README.rst +++ b/README.rst @@ -240,6 +240,10 @@ TODO * Make query count configurable * Include Jenkins support out of the box (using `django_jenkins`) +Other features + +* Group same queries and report their number of repetitions + Running Tests ------------- diff --git a/runtests.py b/runtests.py index a68cf32..fbb6ade 100644 --- a/runtests.py +++ b/runtests.py @@ -1,13 +1,12 @@ #!/usr/bin/env python # -*- coding: utf-8 -from __future__ import unicode_literals, absolute_import +from __future__ import absolute_import, unicode_literals import os import sys import django -from django.conf import settings -from django.test.utils import get_runner +from django.test.runner import DiscoverRunner def run_tests(*test_args): @@ -16,9 +15,7 @@ def run_tests(*test_args): os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings' django.setup() - TestRunner = get_runner(settings) - test_runner = TestRunner() - failures = test_runner.run_tests(test_args) + failures = DiscoverRunner().run_tests(test_args) sys.exit(bool(failures)) diff --git a/test_query_counter/management/commands/query_count_report.py b/test_query_counter/management/commands/query_count_report.py new file mode 100644 index 0000000..c428a43 --- /dev/null +++ b/test_query_counter/management/commands/query_count_report.py @@ -0,0 +1,25 @@ +from django.core.management import BaseCommand +from test_query_counter.apps import RequestQueryCountConfig +from test_query_counter.report import Reporter + + +class Command(BaseCommand): + + help = 'Generates an HTML Report file with the query report' + + CURRENT_QUERY_COUNT = 'reports/query_count_detail.json' + + def add_arguments(self, parser): + super().add_arguments(parser) + summary_path = RequestQueryCountConfig.get_setting('SUMMARY_PATH') + parser.add_argument(dest='query_count_file', + help='JSON summary file for current run.', + default=summary_path) + parser.add_argument('--output-dir', '-o', + dest='output', + help='Output directory to generate report', + default='reports') + + def handle(self, *args, **options): + with open(options['query_count_file']) as current_file: + Reporter.process_file(current_file, options['output']) diff --git a/test_query_counter/manager.py b/test_query_counter/manager.py index 4a83d85..8ca427d 100644 --- a/test_query_counter/manager.py +++ b/test_query_counter/manager.py @@ -138,9 +138,9 @@ def wrapped(self, *args, **kwargs): result = func(self, *args, **kwargs) if not RequestQueryCountConfig.enabled(): return result + cls.save_json('SUMMARY_PATH', cls.queries, False) cls.save_json('DETAIL_PATH', cls.queries, True) - cls.queries = None return result return wrapped diff --git a/test_query_counter/middleware.py b/test_query_counter/middleware.py index 020fc6a..b29c98c 100644 --- a/test_query_counter/middleware.py +++ b/test_query_counter/middleware.py @@ -1,6 +1,9 @@ +import json +import traceback +from time import time + from django.core.exceptions import MiddlewareNotUsed -from django.core.signals import request_started -from django.db import DEFAULT_DB_ALIAS, connections, reset_queries +from django.db import DEFAULT_DB_ALIAS, connections from test_query_counter.apps import RequestQueryCountConfig from test_query_counter.manager import RequestQueryCountManager @@ -30,26 +33,109 @@ def __init__(self, *args, **kwargs): self.final_queries = None self.connection = connections[DEFAULT_DB_ALIAS] - def process_request(self, _): + def wrap_cursor(self, connection, request): + if not hasattr(connection, '_drqc_cursor'): + connection._drqc_cursor = connection.cursor + + def cursor(*args, **kwargs): + return CursorProxy( + connection._drqc_cursor(*args, **kwargs), + connection, + request, + RequestQueryCountConfig.stacktraces_enabled(), + self + ) + + connection.cursor = cursor + return cursor + + def on_query(self, request, query_info): + container = RequestQueryCountManager.get_testcase_container() + if container: + container.add(request, [query_info]) + + def unwrap_cursor(self, connection): + if hasattr(connection, '_drqc_cursor'): + del connection._drqc_cursor + del connection.cursor + + def process_request(self, request): if RequestQueryCountManager.get_testcase_container(): # Took from django.test.utils.CaptureQueriesContext - self.force_debug_cursor = self.connection.force_debug_cursor - self.connection.force_debug_cursor = True - self.initial_queries = len(self.connection.queries_log) - self.final_queries = None - request_started.disconnect(reset_queries) + self.wrap_cursor(self.connection, request) def process_response(self, request, response): if RequestQueryCountManager.get_testcase_container(): # Took from django.test.utils.CaptureQueriesContext - self.connection.force_debug_cursor = self.force_debug_cursor - request_started.connect(reset_queries) - final_queries = len(self.connection.queries_log) - captured_queries = self.connection.queries[ - self.initial_queries:final_queries - ] - - query_container = RequestQueryCountManager.get_testcase_container() - query_container.add(request, captured_queries) + self.unwrap_cursor(self.connection) return response + + +class CursorProxy(object): + """ + Wraps a cursor and logs queries. + """ + + def __init__(self, cursor, db, request, enable_stacktraces, logger): + self.cursor = cursor + # Instance of a BaseDatabaseWrapper subclass + self.logger = logger + self.enable_stacktraces = enable_stacktraces + self.request = request + + def _record(self, method, sql, params): + start_time = time() + try: + return method(sql, params) + finally: + stop_time = time() + duration = (stop_time - start_time) * 1000 + if self.enable_stacktraces: + stacktrace = traceback.format_stack() + else: + stacktrace = [] + + try: + _params = json.dumps([self._decode(p) for p in params]) + except: + _params = "" + + alias = getattr(self.db, 'alias', 'default') + conn = self.db.connection + vendor = getattr(conn, 'vendor', 'unknown') + + params = { + 'vendor': vendor, + 'alias': alias, + 'duration': duration, + 'sql': sql, + 'params': _params, + 'stacktrace': stacktrace, + 'start_time': start_time, + 'stop_time': stop_time, + } + + # We keep `sql` to maintain backwards compatibility + self.logger.on_query(self.request, params) + + def callproc(self, procname, params=None): + return self._record(self.cursor.callproc, procname, params) + + def execute(self, sql, params=None): + return self._record(self.cursor.execute, sql, params) + + def executemany(self, sql, param_list): + return self._record(self.cursor.executemany, sql, param_list) + + def __getattr__(self, attr): + return getattr(self.cursor, attr) + + def __iter__(self): + return iter(self.cursor) + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() diff --git a/test_query_counter/query_count.py b/test_query_counter/query_count.py index 56a945a..74b0993 100644 --- a/test_query_counter/query_count.py +++ b/test_query_counter/query_count.py @@ -111,10 +111,6 @@ def add_by_key(self, api_method_key, queries): def add(self, request, queries): """Agregates the queries to the captured queries dict""" - if request in self.recorded_requests: - return - - self.recorded_requests.add(request) key = (request.method, request.path) self.add_by_key(key, queries) diff --git a/test_query_counter/report.py b/test_query_counter/report.py new file mode 100644 index 0000000..2e34f10 --- /dev/null +++ b/test_query_counter/report.py @@ -0,0 +1,40 @@ +import json +import shutil +from contextlib import ExitStack +from os import makedirs, path + + +class Reporter(object): + @classmethod + def ensure_dir(cls, output_dir): + makedirs(output_dir, exist_ok=True) + + @classmethod + def write_index(cls, query_count_file, output_dir): + output_index = path.join(output_dir, 'index.html') + + with ExitStack() as stack: + output = stack.enter_context( + open(output_index, encoding='utf-8', mode='w') + ) + template_path = path.join(path.dirname(__file__), + 'templates', 'report-index.html') + template_file = stack.enter_context( + open(template_path, encoding='utf-8') + ) + report = json.dumps(json.load(query_count_file)) + formatted_file = template_file.read().format(report=report) + output.write(formatted_file) + + @classmethod + def write_assets(cls, output_dir): + shutil.copyfile( + path.join(path.dirname(__file__), 'static', 'app.js'), + path.join(output_dir, 'app.js') + ) + + @classmethod + def process_file(cls, query_count_file, output_dir): + cls.ensure_dir(output_dir) + cls.write_index(query_count_file, output_dir) + cls.write_assets(output_dir) diff --git a/test_query_counter/static/app.js b/test_query_counter/static/app.js new file mode 100644 index 0000000..f880a08 --- /dev/null +++ b/test_query_counter/static/app.js @@ -0,0 +1,102 @@ +'use strict'; +let queries = null; +let tree = null; + +function main() +{ + queries = rawReport.test_cases[0].queries[0].queries; + tree = traceTree(window.rawReport); +} + +function traceTree(queries) +{ + let tree = { trace: '', queries: [], children: {}, total: 0 }; + + queries.forEach((query) => { + query.stacktrace = query.stacktrace.map(getFormattedTrace); + }); + + queries.forEach((query) => { + addToTree(tree, query, 0, query.stacktrace.length); + }); + + tree = squashTree(tree); + return tree; +} + +function squashTree(tree) +{ + let childrenKeys = Object.keys(tree.children); + + var squashedChilds = {}; + childrenKeys.forEach((key) => { + squashedChilds[key] = squashTree(tree.children[key]); + }); + tree.children = squashedChilds; + + if(childrenKeys.length != 1) + { + return tree; + } + else { + let childTree = tree.children[childrenKeys[0]]; + + return { + trace: [tree.trace].concat(childTree.trace), + queries: tree.queries, + total: tree.total, + children: childTree.children + }; + } +} + +function getFormattedTrace(traceElement) +{ + const pathReplacements = [ + [ '/home/igui/envs/profit-tools-django/lib/python3.4/site-packages/', '' ], + [ '/home/igui/src/profit-tools/planning-view/app/', 'app/' ], + ]; + + let filePath = traceElement[0]; + + for(var idx in pathReplacements) + { + filePath = filePath.replace( + pathReplacements[idx][0], + pathReplacements[idx][1] + ); + } + + return filePath + ':' + traceElement[1] + '>' + traceElement[2]; +} + +function formatSQL(sql) +{ + return sql.replace(/\"/g, ''); +} + +function addToTree(tree, query, firstIdx, lastIdx) +{ + if(firstIdx >= lastIdx) + { + return; + } + + let traceElement = query.stacktrace[firstIdx]; + let child = tree.children[traceElement]; + + if(!child) + { + child = { trace: traceElement, queries: [], children: {}, total: 0 }; + tree.children[traceElement] = child; + } + + addToTree(child, query, firstIdx+1, lastIdx); + + const sql = formatSQL(query.sql); + tree.queries.push(sql); + tree.total++; +} + +main(); + diff --git a/test_query_counter/templates/report-index.html b/test_query_counter/templates/report-index.html new file mode 100644 index 0000000..be45a54 --- /dev/null +++ b/test_query_counter/templates/report-index.html @@ -0,0 +1,13 @@ + + + + + Query Report + + + + + + diff --git a/tests/runner.py b/tests/runner.py new file mode 100644 index 0000000..5eb12ab --- /dev/null +++ b/tests/runner.py @@ -0,0 +1,35 @@ +import unittest +from io import StringIO +from unittest import TextTestRunner + +from django.test.runner import DiscoverRunner + + +# Simple class that doesn't output to the standard output +class StringIOTextRunner(TextTestRunner): + def __init__(self, *args, **kwargs): + kwargs['stream'] = StringIO() + super().__init__(*args, **kwargs) + + +class MiniTestRunner(DiscoverRunner): + suite = None + test_runner = StringIOTextRunner + + def setup_test_environment(self, **kwargs): + unittest.installHandler() + + def teardown_test_environment(self, **kwargs): + unittest.removeHandler() + + def setup_databases(self, **kwargs): + pass + + def teardown_databases(self, old_config, **kwargs): + pass + + def run_checks(self): + pass + + def build_suite(self, test_labels=None, extra_tests=None, **kwargs): + return self.suite diff --git a/tests/settings.py b/tests/settings.py index a0e5d9e..932f406 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -38,9 +38,13 @@ 'test_query_counter', ] +TEST_RUNNER = 'tests.runner.MiniTestRunner' + +ALLOWED_HOSTS = 'testserver' + SITE_ID = 1 if django.VERSION >= (1, 10): - MIDDLEWARE = () + MIDDLEWARE = ('test_query_counter.middleware.Middleware',) else: - MIDDLEWARE_CLASSES = () + MIDDLEWARE_CLASSES = ('test_query_counter.middleware.Middleware',) diff --git a/tests/test_app_config.py b/tests/test_app_config.py index 5cb3f39..ef8ae6b 100644 --- a/tests/test_app_config.py +++ b/tests/test_app_config.py @@ -1,5 +1,7 @@ +from unittest import TestCase + from django.conf import settings -from django.test import TestCase, override_settings +from django.test import override_settings from test_query_counter.apps import RequestQueryCountConfig from test_query_counter.manager import RequestQueryCountManager diff --git a/tests/test_middleware.py b/tests/test_middleware.py index e14a557..b91a849 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -1,28 +1,25 @@ import os -from io import StringIO from os import path -from unittest import TestLoader, TextTestRunner, mock +from unittest import TestCase, TestSuite, mock from unittest.mock import MagicMock +import django from django.core.exceptions import MiddlewareNotUsed -from django.test import TestCase, override_settings -from django.test.runner import DiscoverRunner +from django.test import Client, TransactionTestCase, override_settings from test_query_counter.apps import RequestQueryCountConfig from test_query_counter.manager import RequestQueryCountManager from test_query_counter.middleware import Middleware +from tests.runner import MiniTestRunner + class TestMiddleWare(TestCase): + @classmethod + def setUpClass(cls): + django.setup() def setUp(self): - # Simple class that doesn't output to the standard output - class StringIOTextRunner(TextTestRunner): - def __init__(self, *args, **kwargs): - kwargs['stream'] = StringIO() - super().__init__(*args, **kwargs) - - self.test_runner = DiscoverRunner() - self.test_runner.test_runner = StringIOTextRunner + self.test_runner = MiniTestRunner() def tearDown(self): try: @@ -37,33 +34,31 @@ def tearDown(self): def test_middleware_called(self): with mock.patch('test_query_counter.middleware.Middleware', new=MagicMock(wraps=Middleware)) as mocked: - self.client.get('/url-1') + Client().get('/url-1') self.assertEqual(mocked.call_count, 1) def test_case_injected_one_test(self): - class Test(TestCase): + class Test(TransactionTestCase): def test_foo(self): - self.client.get('/url-1') - - self.test_runner.setup_test_environment() - self.test_runner.run_suite(TestLoader().loadTestsFromTestCase( - testCaseClass=Test)) - self.test_runner.teardown_test_environment() + Client().get('/url-1') + test_suite = self.test_runner.suite = TestSuite() + test_suite.addTest(Test('test_foo')) + self.test_runner.run_tests(test_labels='') self.assertEqual(RequestQueryCountManager.queries.total, 1) def test_case_injected_two_tests(self): - class Test(TestCase): + class Test(TransactionTestCase): def test_foo(self): self.client.get('/url-1') def test_bar(self): self.client.get('/url-2') - self.test_runner.run_suite( - TestLoader().loadTestsFromTestCase(testCaseClass=Test) - ) - + test_suite = self.test_runner.suite = TestSuite() + test_suite.addTest(Test('test_foo')) + test_suite.addTest(Test('test_bar')) + self.test_runner.run_tests(test_labels='') self.assertEqual(RequestQueryCountManager.queries.total, 2) @override_settings(TEST_QUERY_COUNTER={'ENABLE': False}) @@ -75,10 +70,10 @@ def test_foo(self): def test_bar(self): self.client.get('/url-2') - self.test_runner.run_tests( - None, - TestLoader().loadTestsFromTestCase(testCaseClass=Test) - ) + test_suite = self.test_runner.suite = TestSuite() + test_suite.addTest(Test('test_foo')) + test_suite.addTest(Test('test_bar')) + self.test_runner.run_tests(test_labels='') self.assertIsNone(RequestQueryCountManager.queries) @override_settings(TEST_QUERY_COUNTER={'ENABLE': False}) @@ -88,17 +83,23 @@ def test_disabled(self): Middleware(mock_get_response) def test_json_exists(self): - class Test(TestCase): + class Test(TransactionTestCase): def test_foo(self): self.client.get('/url-1') self.assertFalse(path.exists( RequestQueryCountConfig.get_setting('DETAIL_PATH')) ) - self.test_runner.run_tests( - None, - TestLoader().loadTestsFromTestCase(testCaseClass=Test) - ) - self.assertTrue(path.exists( - RequestQueryCountConfig.get_setting('DETAIL_PATH')) + + test_suite = self.test_runner.suite = TestSuite() + test_suite.addTest(Test('test_foo')) + self.test_runner.run_tests(test_labels='') + + self.assertTrue( + path.exists( + RequestQueryCountConfig.get_setting('DETAIL_PATH') + ), + 'JSON doesn\'t exists in {}'.format( + RequestQueryCountConfig.get_setting('DETAIL_PATH') + ) ) diff --git a/tests/test_query_count_containers.py b/tests/test_query_count_containers.py index 1cd4f29..e621901 100644 --- a/tests/test_query_count_containers.py +++ b/tests/test_query_count_containers.py @@ -1,4 +1,4 @@ -from django.test import TestCase +from unittest import TestCase from test_query_counter.query_count import (TestCaseQueryContainer, TestResultQueryContainer) diff --git a/tests/test_query_count_evaluator.py b/tests/test_query_count_evaluator.py index 1658ea3..ab45f8f 100644 --- a/tests/test_query_count_evaluator.py +++ b/tests/test_query_count_evaluator.py @@ -1,8 +1,7 @@ import json import re from io import StringIO - -from django.test import TestCase +from unittest import TestCase from test_query_counter.query_count import QueryCountEvaluator diff --git a/tests/test_query_count_runner.py b/tests/test_query_count_runner.py index 3a429e2..5313d07 100644 --- a/tests/test_query_count_runner.py +++ b/tests/test_query_count_runner.py @@ -1,27 +1,20 @@ import os -from io import StringIO +import unittest from os import path -from unittest import TestLoader, TextTestRunner +from unittest import TestSuite -from django.test import TestCase -from django.test.runner import DiscoverRunner +import django.test.testcases as django_testcase from test_query_counter.apps import RequestQueryCountConfig from test_query_counter.manager import RequestQueryCountManager from test_query_counter.query_count import (TestResultQueryContainer, exclude_query_count) +from tests.runner import MiniTestRunner -class TestRunnerTest(TestCase): +class TestRunnerTest(unittest.TestCase): def setUp(self): - # Simple class that doesn't output to the standard output - class StringIOTextRunner(TextTestRunner): - def __init__(self, *args, **kwargs): - kwargs['stream'] = StringIO() - super().__init__(*args, **kwargs) - - self.test_runner = DiscoverRunner() - self.test_runner.test_runner = StringIOTextRunner + self.test_runner = MiniTestRunner() def tearDown(self): try: @@ -34,18 +27,17 @@ def tearDown(self): pass def test_empty_test(self): - class Test(TestCase): + class Test(django_testcase.TestCase): def test_foo(self): pass def test_bar(self): pass - self.test_runner.setup_test_environment() - self.test_runner.run_suite(TestLoader().loadTestsFromTestCase( - testCaseClass=Test) - ) - self.test_runner.teardown_test_environment() + test_suite = self.test_runner.suite = TestSuite() + test_suite.addTest(Test('test_foo')) + test_suite.addTest(Test('test_bar')) + self.test_runner.run_tests(test_labels='') # check for empty tests self.assertIsNotNone(RequestQueryCountManager, 'queries') @@ -74,14 +66,13 @@ def get_id(cls, test_class, method_name): method_name) def test_runner_include_queries(self): - class Test(TestCase): + class Test(django_testcase.TestCase): def test_foo(self): self.client.get('/url-1') - self.test_runner.run_tests( - None, - TestLoader().loadTestsFromTestCase(testCaseClass=Test) - ) + test_suite = self.test_runner.suite = TestSuite() + test_suite.addTest(Test('test_foo')) + self.test_runner.run_tests(test_labels='') # Assert it ran one test self.assertEqual(len(RequestQueryCountManager.queries.queries_by_testcase), 1) @@ -95,7 +86,7 @@ def test_foo(self): ) def test_excluded_test(self): - class Test(TestCase): + class Test(django_testcase.TestCase): @exclude_query_count() def test_foo(self): self.client.get('/url-1') @@ -103,9 +94,11 @@ def test_foo(self): def test_bar(self): self.client.get('/url-1') - self.test_runner.run_suite( - TestLoader().loadTestsFromTestCase(testCaseClass=Test) - ) + test_suite = self.test_runner.suite = TestSuite() + test_suite.addTest(Test('test_foo')) + test_suite.addTest(Test('test_bar')) + self.test_runner.run_tests(test_labels='') + # Assert test_foo has excluded queries self.assertEqual( RequestQueryCountManager.queries.queries_by_testcase[ @@ -121,16 +114,18 @@ def test_bar(self): def test_excluded_class(self): @exclude_query_count() - class Test(TestCase): + class Test(django_testcase.TestCase): def test_foo(self): self.client.get('path-1') def test_bar(self): self.client.get('path-1') - self.test_runner.run_suite( - TestLoader().loadTestsFromTestCase(testCaseClass=Test) - ) + test_suite = self.test_runner.suite = TestSuite() + test_suite.addTest(Test('test_foo')) + test_suite.addTest(Test('test_bar')) + self.test_runner.run_tests(test_labels='') + # Assert test_foo has excluded queries self.assertEqual( RequestQueryCountManager.queries.queries_by_testcase[ @@ -144,7 +139,7 @@ def test_bar(self): ) def test_conditional_exclude(self): - class Test(TestCase): + class Test(django_testcase.TestCase): @exclude_query_count(path='url-2') def test_exclude_path(self): self.client.get('/url-1') @@ -164,9 +159,12 @@ def test_exclude_count(self): self.client.put('/url-3') self.client.put('/url-3') - self.test_runner.run_suite( - TestLoader().loadTestsFromTestCase(testCaseClass=Test) - ) + test_suite = self.test_runner.suite = TestSuite() + test_suite.addTest(Test('test_exclude_path')) + test_suite.addTest(Test('test_exclude_method')) + test_suite.addTest(Test('test_exclude_count')) + self.test_runner.run_tests(test_labels='') + self.assertEqual( RequestQueryCountManager.queries.queries_by_testcase[ self.get_id(Test, 'test_exclude_path')].total, @@ -184,7 +182,7 @@ def test_exclude_count(self): ) def test_nested_method_exclude(self): - class Test(TestCase): + class Test(django_testcase.TestCase): @exclude_query_count(path='url-1') @exclude_query_count(method='post') @exclude_query_count(path='url-3') @@ -193,9 +191,10 @@ def test_foo(self): self.client.post('/url-2') self.client.put('/url-3') - self.test_runner.run_suite( - TestLoader().loadTestsFromTestCase(testCaseClass=Test) - ) + test_suite = self.test_runner.suite = TestSuite() + test_suite.addTest(Test('test_foo')) + self.test_runner.run_tests(test_labels='') + self.assertEqual( RequestQueryCountManager.queries.queries_by_testcase[ self.get_id(Test, 'test_foo')].total, @@ -204,16 +203,17 @@ def test_foo(self): def test_nested_class_method_exclude(self): @exclude_query_count(path='url-1') - class Test(TestCase): + class Test(django_testcase.TestCase): @exclude_query_count(method='post') def test_foo(self): self.client.get('/url-1') self.client.post('/url-2') self.client.put('/url-3') - self.test_runner.run_suite( - TestLoader().loadTestsFromTestCase(testCaseClass=Test) - ) + test_suite = self.test_runner.suite = TestSuite() + test_suite.addTest(Test('test_foo')) + self.test_runner.run_tests(test_labels='') + self.assertEqual( RequestQueryCountManager.queries.queries_by_testcase[ self.get_id(Test, 'test_foo')].total, @@ -221,7 +221,7 @@ def test_foo(self): ) def test_custom_setup_teardown(self): - class Test(TestCase): + class Test(django_testcase.TestCase): def setUp(self): pass @@ -231,9 +231,10 @@ def tearDown(self): def test_foo(self): self.client.get('/url-1') - self.test_runner.run_suite( - TestLoader().loadTestsFromTestCase(testCaseClass=Test) - ) + test_suite = self.test_runner.suite = TestSuite() + test_suite.addTest(Test('test_foo')) + self.test_runner.run_tests(test_labels='') + self.assertIn( self.get_id(Test, 'test_foo'), RequestQueryCountManager.queries.queries_by_testcase