将 Django DiscoverRunner 设置为始终在使用萝卜进行测试时重新创建数据库

Set up Django DiscoverRunner to always recreate database on testing with radish

我正在使用带有 selenium 的 radish 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()

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

@Wyatt,我又要修改你的答案了。我已经尝试 运行 你的解决方案,但是它没有设法使每个场景独立,当我尝试在场景中创建模型对象时,我什至遇到了完整性错误。不管怎样我仍然使用你的解决方案(特别是 RadishTestRunner,因为这个想法来自你。我修改了它所以我可以 运行 django unittestradish 分开。我使用 LiveServerTestCase 直接删除 LiveServer 因为我注意到两者之间的相似性,除了 LiveServerTestCase 继承自 TransactionTestCase 并且它还构建了 LiveServerThread_StaticFilesHandler in. 是这样的:

# 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 在您指出的我的其他问题上的问题。我还在每个场景中打开和退出浏览器,因为它让我可以更好地控制场景中的浏览器。非常感谢您为我指出正确的方向所做的努力。

最后我return 到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 管理命令的萝卜,我们可以控制我们想要的特征文件 运行。

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

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

请注意,此 运行 默认情况下同时进行 Django 单元测试和萝卜测试。要运行 只是萝卜测试,你可以RADISH_ONLY=1 manage.py test。此外,要使实时 server/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()