0

I am using radish bdd with selenium to test my django app, however sometimes django ask to delete database because it's already exists in database. here's my 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()

I think that, I can somehow could alter this on

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

But I don't know how. Any help?

tangorboyz
  • 39
  • 8

2 Answers2

1

@Wyatt, again I'm just gonna modify your answer. I have try and run your solution, however it didn't manage to make each scenario independent, I even encounter an Integrity error when I try to make model object inside scenario. Regardless I still use your solution (especially RadishTestRunner, as the idea comes from you. I modified it so I could run django unittest separately from radish. I use LiveServerTestCase directly and remove LiveServer as I notice the similirity between the two, except that LiveServerTestCase inherits from TransactionTestCase and it also has the LiveServerThread and _StaticFilesHandler built in. Here's how it is:

# 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()

That's it. This also fix the problem with PostgreSQL on my other question that you point out. I also open and quit browser on each scenario, as it gives me more control over the browser inside scenario. Thank you so much for you effort to point me at the right direction.

Finally I return to PostgreSQL. PostgreSQL seem to better than MySQL in terms of speed. It greatly reduced the time to run test.

And oh ya, I need to run ./manage.py collectstatic first after specify STATIC_ROOT in django settings file.

I also modify RadishTestRunner, so instead of running with RADISH_ONLY=1, I could run it with python manage.py radish /path/to/features/file. Here's my radish command:

# 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)

By using radish with django management command, we have control over which feature file we want to run.

tangorboyz
  • 39
  • 8
  • Creating a `LiveServerTestCase` for every feature runs a new server in the background on a random port, but those servers are never used. All of your Selenium tests are using the non-test dev server. –  Nov 03 '17 at 16:44
  • You are right, that take more resource than it should be. – tangorboyz Nov 04 '17 at 14:43
1

It seems to me that recreating the database for every scenario would end up being highly inefficient (and super slow). It should only be necessary to create the database once per test run and then drop it at the end.

I've come up with a solution I think integrates better with Django. It allows you to run the tests with manage.py test, only creates/drops the database once per test run, and clears the database tables after every feature is tested.

Note that this runs both the Django unit tests and radish tests by default. To run just the radish tests, you can do RADISH_ONLY=1 manage.py test. Also, for the live server/Selenium tests to work, you have to run manage.py collectstatic first.

# 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()
  • I understand, but that would cause the database not independent on each scenario. – tangorboyz Nov 02 '17 at 21:50
  • That would be easy enough to fix without creating and dropping the database for every single scenario. –  Nov 02 '17 at 21:59
  • Can I use that with selenium? Would launch test command also works with selenium, which test on browser? – tangorboyz Nov 02 '17 at 22:11
  • I edited the code so the database should be flushed after every scenario now. –  Nov 02 '17 at 22:17
  • In my opinion, running Selenium tests via radish doesn't seem like a good fit. Using `LiveServerTestCase` directly seems like a better way to run Selenium tests (as show in [this article](https://medium.com/@unary/django-views-automated-testing-with-selenium-d9df95bdc926)). –  Nov 02 '17 at 22:24
  • I am working on it. I'll let you know, If I hit something. – tangorboyz Nov 04 '17 at 09:35
  • This works for me with Postgres (tested by creating the same record in two different scenarios). It might not work with MySQL. –  Nov 04 '17 at 17:06
  • Ya, I've test both. – tangorboyz Nov 04 '17 at 17:19