【问题标题】: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 unittestradish 分开运行。我直接使用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
    相关资源
    最近更新 更多