【问题标题】:Set up Django DiscoverRunner to always recreate database on testing with radish设置 Django DiscoverRunner 以始终在使用萝卜进行测试时重新创建数据库
【发布时间】:2017-11-01 21:28:03
【问题描述】:

我正在使用带有 selenium 的萝卜 bdd 来测试我的 django 应用程序,但是有时 django 要求删除数据库,因为它已经存在于数据库中。这是我的terrain.py:

import os

import django
from django.test.runner import DiscoverRunner
from django.test import LiveServerTestCase
from radish import before, after
from selenium import webdriver


os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tangorblog.settings.features')
BASE_URL = os.environ.get('BASE_URL', 'http://localhost:8000')


@before.each_scenario
def setup_django_test(scenario):
    django.setup()
    scenario.context.test_runner = DiscoverRunner()
    scenario.context.test_runner.setup_test_environment()
    scenario.context.old_db_config =\
        scenario.context.test_runner.setup_databases()
    scenario.context.base_url = BASE_URL
    scenario.context.test_case = LiveServerTestCase()
    scenario.context.test_case.setUpClass()
    scenario.context.browser = webdriver.Chrome()


@after.each_scenario
def teardown_django(scenario):
    scenario.context.browser.quit()
    scenario.context.test_case.tearDownClass()
    del scenario.context.test_case
    scenario.context.test_runner.teardown_databases(
        scenario.context.old_db_config)
    scenario.context.test_runner.teardown_test_environment()

我认为,我可以通过某种方式改变它

scenario.context.old_db_config =\
            scenario.context.test_runner.setup_databases()

但我不知道怎么做。有什么帮助吗?

【问题讨论】:

    标签: python django unit-testing selenium bdd


    【解决方案1】:

    在我看来,为每个场景重新创建数据库最终会变得非常低效(而且超级慢)。每次测试运行时只需要创建一次数据库,然后在最后删除它。

    我想出了一个我认为可以更好地与 Django 集成的解决方案。它允许您使用manage.py test 运行测试,每次测试运行只创建/删除一次数据库,并在测试每个功能后清除数据库表。

    请注意,默认情况下,这会同时运行 Django 单元测试和萝卜测试。要仅运行萝卜测试,您可以执行RADISH_ONLY=1 manage.py test。此外,要使实时服务器/Selenium 测试正常工作,您必须先运行 manage.py collectstatic。

    # package/settings.py
    TEST_RUNNER = 'package.test.runner.RadishTestRunner'
    
    # package/test/runner
    import os
    
    from django.test.runner import DiscoverRunner
    
    import radish.main
    
    class RadishTestRunner(DiscoverRunner):
    
        def run_suite(self, suite, **kwargs):
            # Run unit tests
            if os.getenv('RADISH_ONLY') == '1':
                result = None
            else:
                result = super().run_suite(suite, **kwargs)
            # Run radish behavioral tests
            self._radish_result = radish.main.main(['features'])
            return result
    
        def suite_result(self, suite, result, **kwargs):
            if result is not None:
                # Django unit tests were run
                result = super().suite_result(suite, result, **kwargs)
            else:
                result = 0
            result += self._radish_result
            return result
    
    # radish/world.py
    from django.db import connections
    from django.test.testcases import LiveServerThread, _StaticFilesHandler
    from django.test.utils import modify_settings
    
    from radish import pick
    
    from selenium import webdriver
    
    @pick
    def get_browser():
        return webdriver.Chrome()
    
    @pick
    def get_live_server():
        live_server = LiveServer()
        live_server.start()
        return live_server
    
    class LiveServer:
    
        host = 'localhost'
        port = 0
        server_thread_class = LiveServerThread
        static_handler = _StaticFilesHandler
    
        def __init__(self):
            connections_override = {}
            for conn in connections.all():
                if conn.vendor == 'sqlite' and conn.is_in_memory_db():
                    conn.allow_thread_sharing = True
                    connections_override[conn.alias] = conn
    
            self.modified_settings = modify_settings(ALLOWED_HOSTS={'append': self.host})
    
            self.server_thread = self.server_thread_class(
                self.host,
                self.static_handler,
                connections_override=connections_override,
                port=self.port,
            )
            self.server_thread.daemon = True
    
        @property
        def url(self):
            self.server_thread.is_ready.wait()
            return 'http://{self.host}:{self.server_thread.port}'.format(self=self)
    
        def start(self):
            self.modified_settings.enable()
            self.server_thread.start()
            self.server_thread.is_ready.wait()
            if self.server_thread.error:
                self.stop()
                raise self.server_thread.error
    
        def stop(self):
            if hasattr(self, 'server_thread'):
                self.server_thread.terminate()
    
            for conn in connections.all():
                if conn.vendor == 'sqlite' and conn.is_in_memory_db():
                    conn.allow_thread_sharing = False
    
            self.modified_settings.disable()
    
    # radish/terrain.py
    from django.db import connections, transaction
    
    from radish import world, before, after
    
    @before.all
    def set_up(features, marker):
        world.get_live_server()
    
    @after.all
    def tear_down(features, marker):
        browser = world.get_browser()
        live_server = world.get_live_server()
        browser.quit()
        live_server.stop()
    
    @before.each_scenario
    def set_up_scenario(scenario):
        live_server = world.get_live_server()
        scenario.context.base_url = live_server.url
        scenario.context.browser = world.get_browser()
    
        # XXX: Only works with the default database
        # XXX: Assumes the default database supports transactions
        scenario.context.transaction = transaction.atomic(using='default')
        scenario.context.transaction.__enter__()
    
    @after.each_scenario
    def tear_down_scenario(scenario):
        transaction.set_rollback(True, using='default')
        scenario.context.transaction.__exit__(None, None, None)
        for connection in connections.all():
            connection.close()
    

    【讨论】:

    • 我明白,但这会导致数据库不独立于每个场景。
    • 这很容易修复,无需为每个场景创建和删除数据库。
    • 我可以将它与硒一起使用吗?启动测试命令是否也适用于在浏览器上测试的 selenium?
    • 我编辑了代码,所以现在应该在每个场景之后刷新数据库。
    • 在我看来,通过 radish 运行 Selenium 测试似乎不太合适。直接使用LiveServerTestCase 似乎是运行Selenium 测试的更好方法(如this article 所示)。
    【解决方案2】:

    @Wyatt,我再次修改你的答案。我已经尝试运行您的解决方案,但是它并没有设法使每个场景独立,当我尝试在场景中创建模型对象时,我什至遇到了完整性错误。不管我仍然使用您的解决方案(尤其是RadishTestRunner,因为这个想法来自您。我对其进行了修改,以便可以将django unittest 与radish 分开运行。我直接使用LiveServerTestCase 并删除LiveServer,因为我注意到了两者之间的相似之处,除了LiveServerTestCase 继承自TransactionTestCase 并且还内置了LiveServerThread 和_StaticFilesHandler。它是这样的:

    # package/test/runner.py
    import os
    from django.test.runner import DiscoverRunner
    import radish.main
    
    
    class RadishTestRunner(DiscoverRunner):
        radish_features = ['features']
        def run_suite(self, suite, **kwargs):
            # run radish test
            return radish.main.main(self.radish_features)
    
        def suite_result(self, suite, result, **kwargs):
            return result
    
        def set_radish_features(self, features):
            self.radish_features = features
    
    # radish/world.py
    from django.test import LiveServerTestCase
    
    
    from radish import pick
    
    from selenium import webdriver
    
    
    @pick
    def get_browser():
        return webdriver.Chrome()
    
    
    @pick
    def get_live_server():
        live_server = LiveServerTestCase
        live_server.setUpClass()
        return live_server
    
    # radish/terrain.py
    from radish import world, before, after
    from selenium import webdriver
    
    
    @before.all
    def set_up(features, marker):
        world.get_live_server()
    
    
    @after.all
    def tear_down(features, marker):
        live_server = world.get_live_server()
        live_server.tearDownClass()
    
    
    @before.each_scenario
    def set_up_scenario(scenario):
        live_server = world.get_live_server()
    
        scenario.context.browser = webdriver.Chrome()
        scenario.context.base_url = live_server.live_server_url
    
        scenario.context.test_case = live_server()
        scenario.context.test_case._pre_setup()
    
    
    @after.each_scenario
    def tear_down_scenario(scenario):
        scenario.context.test_case._post_teardown()
        scenario.context.browser.quit()
    

    就是这样。这也解决了 PostgreSQL 在您指出的另一个问题上的问题。我还在每个场景中打开和退出浏览器,因为它让我可以更好地控制场景中的浏览器。非常感谢您为我指明正确的方向而付出的努力。

    最后我回到 PostgreSQL。 就速度而言,PostgreSQL 似乎比 MySQL 更好。它大大减少了运行测试的时间。

    哦,我需要在django 设置文件中指定STATIC_ROOT 后首先运行./manage.py collectstatic。

    我还修改了RadishTestRunner,所以我可以用python manage.py radish /path/to/features/file 运行它,而不是用RADISH_ONLY=1 运行。这是我的萝卜命令:

    # package.management.commands.radish
    from __future__ import absolute_import
    import sys
    from django.core.management.base import BaseCommand, CommandError
    from package.test.runner import RadishTestRunner
    
    
    class Command(BaseCommand):
        def add_arguments(self, parser):
            parser.add_argument('features', nargs='+', type=str)
    
        def handle(self, *args, **options):
            test_runner = RadishTestRunner(interactive=False)
            if options['features']:
                test_runner.set_radish_features(options['features'])
            result = test_runner.run_suite(None)
            if result:
                sys.exit(result)
    

    通过 django 管理命令使用 radish,我们可以控制要运行的功能文件。

    【讨论】:

    • 为每个功能创建一个LiveServerTestCase 在后台随机端口上运行一个新服务器,但这些服务器从未使用过。您所有的 Selenium 测试都使用非测试开发服务器。
    • 你是对的,这比它应该占用的资源更多。
    猜你喜欢
    • 1970-01-01
    • 2018-03-25
    • 1970-01-01
    • 2016-01-18
    • 1970-01-01
    • 1970-01-01
    • 2015-12-15
    • 2020-11-17
    • 1970-01-01
    相关资源
    最近更新 更多