Skip to content

Commit

Permalink
Merge pull request #29 from avsd/simple-tests-support
Browse files Browse the repository at this point in the history
Simple tests support
  • Loading branch information
mixxorz authored Mar 17, 2017
2 parents d64d153 + 0272ffa commit 2670e49
Show file tree
Hide file tree
Showing 11 changed files with 156 additions and 13 deletions.
30 changes: 21 additions & 9 deletions behave_django/environment.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
from behave.runner import ModelRunner
from behave.runner import ModelRunner, Context
from django.core.management import call_command
from django.shortcuts import resolve_url


class PatchedContext(Context):

@property
def base_url(self):
assert hasattr(self.test, 'live_server_url'), (
'Web browser automation is not available.'
' This scenario step can not be run'
' with the --simple or -S flag.')
return self.test.live_server_url

def get_url(self, to=None, *args, **kwargs):
return self.base_url + (
resolve_url(to, *args, **kwargs) if to else '')


class BehaveHooksMixin(object):
"""
Provides methods that run during test execution
Expand All @@ -17,18 +32,15 @@ def before_scenario(self, context):
Sets up the test case, base_url, and the get_url() utility function.
"""
context.__class__ = PatchedContext
# Simply setting __class__ directly doesn't work
# because behave.runner.Context.__setattr__ is implemented wrongly.
object.__setattr__(context, '__class__', PatchedContext)

context.test = self.testcase_class()
context.test.setUpClass()
context.test()

context.base_url = context.test.live_server_url

def get_url(to=None, *args, **kwargs):
return context.base_url + (
resolve_url(to, *args, **kwargs) if to else '')

context.get_url = get_url

def load_fixtures(self, context):
"""
Method that runs immediately after behave's before_scenario function
Expand Down
21 changes: 20 additions & 1 deletion behave_django/management/commands/behave.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@

from behave_django.environment import monkey_patch_behave
from behave_django.runner import (BehaviorDrivenTestRunner,
ExistingDatabaseTestRunner)
ExistingDatabaseTestRunner,
SimpleTestRunner)


def add_command_arguments(parser):
Expand All @@ -26,6 +27,13 @@ def add_command_arguments(parser):
default=False,
help="Preserves the test DB between runs.",
)
parser.add_argument(
'-S', '--simple',
action='store_true',
default=False,
help="Use simple test runner that supports Django's"
" testing client only (no web browser automation)"
)


def add_behave_arguments(parser): # noqa
Expand All @@ -40,6 +48,8 @@ def add_behave_arguments(parser): # noqa
'-c',
'-k',
'-v',
'-S',
'--simple',
]

parser.add_argument(
Expand Down Expand Up @@ -90,9 +100,18 @@ def add_arguments(self, parser):

def handle(self, *args, **options):

# Check the flags
if options['use_existing_database'] and options['simple']:
self.stderr.write(self.style.WARNING(
'--simple flag has no effect'
' together with --use-existing-database'
))

# Configure django environment
if options['dry_run'] or options['use_existing_database']:
django_test_runner = ExistingDatabaseTestRunner()
elif options['simple']:
django_test_runner = SimpleTestRunner()
else:
django_test_runner = BehaviorDrivenTestRunner()

Expand Down
11 changes: 10 additions & 1 deletion behave_django/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

from behave_django.environment import BehaveHooksMixin
from behave_django.testcase import (BehaviorDrivenTestCase,
ExistingDatabaseTestCase)
ExistingDatabaseTestCase,
DjangoSimpleTestCase)


class BehaviorDrivenTestRunner(DiscoverRunner, BehaveHooksMixin):
Expand All @@ -27,3 +28,11 @@ def setup_databases(self, **kwargs):

def teardown_databases(self, old_config, **kwargs):
pass


class SimpleTestRunner(DiscoverRunner, BehaveHooksMixin):
"""
Test runner that uses DjangoSimpleTestCase with atomic
transaction management and no support of web browser automation.
"""
testcase_class = DjangoSimpleTestCase
20 changes: 20 additions & 0 deletions behave_django/testcase.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from django.test.testcases import TestCase


class BehaviorDrivenTestCase(StaticLiveServerTestCase):
Expand All @@ -24,3 +25,22 @@ def _fixture_setup(self):

def _fixture_teardown(self):
pass


class DjangoSimpleTestCase(TestCase):
"""
Test case attached to the context during behave execution
This test case uses `transaction.atomic()` to achieve test isolation
instead of flushing the entire database. As a result, tests run much
quicker and have no issues with altered DB state after all tests ran
when `--keepdb` is used.
As a side effect, this test case does not support web browser automation.
Use Django's testing client instead to test requests and responses.
Also, it prevents the regular tests from running.
"""

def runTest(self):
pass
4 changes: 3 additions & 1 deletion docs/contribute.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ Install the dev dependencies
$ pip install -r requirements-dev.txt
Make sure the tests pass. The ``@failing`` tag is used for tests that are
supposed to fail.
supposed to fail. The ``@requires-live-http`` tag is used for tests that can't run
with ``--simple`` flag.

.. code:: bash
$ python manage.py behave --tags=~@failing # skip failing tests
$ python manage.py behave --simple --tags=~@failing --tags=~@requires-live-http
$ py.test
Start your topic branch
Expand Down
22 changes: 22 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ testing client.
# save response in context for next step
context.response = context.test.client.get(url)
Simple testing
--------------

If you only use Django's testing client then behave tests can run
much quicker with the ``--simple`` command line option. In this case
transaction rollback is used for test automation instead of flushing
the database after each scenario, just like in Django's standard
``TestCase``.

No HTTP server is started during the simple testing, so you can't
use web browser automation. Accessing ``context.base_url``
or calling ``context.get_url()`` will raise an exception.

unittest + Django assert library
--------------------------------

Expand Down Expand Up @@ -162,6 +176,14 @@ recreating it each time you run the test. This flag enables
``manage.py behave --keepdb`` to take advantage of that feature.
|keepdb docs|_.

``--simple``
************

Use Django's simple ``TestCase`` which rolls back the database transaction after
each scenario instead of flushing the entire database. Tests run much quicker,
however HTTP server is not started and therefore web browser automation is
not available.

Behave configuration file
-------------------------

Expand Down
1 change: 1 addition & 0 deletions features/context-urlhelper.feature
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@requires-live-http
Feature: URL helpers are available in behave's context
In order to ensure that url helpers are available as documented
As the Maintainer
Expand Down
1 change: 1 addition & 0 deletions features/live-test-server.feature
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@requires-live-http
Feature: Live server
In order to prove that the live server works
As the Maintainer
Expand Down
16 changes: 15 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def run_silently(command):
command_args, stdout=PIPE, stderr=PIPE, stdin=PIPE)
stdout, stderr = process.communicate()
output = (stdout.decode('UTF-8') + os.linesep +
stderr.decode('UTF-8')).strip()
stderr.decode('UTF-8')).strip() + os.linesep
return process.returncode, output


Expand All @@ -25,6 +25,8 @@ def test_additional_management_command_options(self):
os.linesep + ' --use-existing-database' + os.linesep) in output
assert (
os.linesep + ' -k, --keepdb') in output
assert (
os.linesep + ' -S, --simple') in output

def test_should_accept_behave_arguments(self):
from behave_django.management.commands.behave import Command
Expand Down Expand Up @@ -99,3 +101,15 @@ def test_conflicting_options_should_get_prefixed(self):
argv=['manage.py', 'behave', '--behave-k', '--behave-version'])

assert args == ['-k', '--version']

def test_simple_and_use_existing_database_flags_raise_a_warning(self):
exit_status, output = run_silently(
'python manage.py behave'
' --simple --use-existing-database --tags=@skip-all'
)
assert exit_status == 0
assert (
os.linesep +
'--simple flag has no effect ' +
'together with --use-existing-database' +
os.linesep) in output
42 changes: 42 additions & 0 deletions tests/test_simple_testcase.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
try:
import unittest.mock as mock
except ImportError:
import mock

from .util import DjangoSetupMixin
from behave_django.runner import SimpleTestRunner
from behave.runner import Context, Runner
from django.test.testcases import TestCase
import pytest


class TestSimpleTestCase(DjangoSetupMixin):

@mock.patch('behave_django.management.commands.behave.behave_main', return_value=0) # noqa
@mock.patch('behave_django.management.commands.behave.SimpleTestRunner') # noqa
def test_use_simple_test_runner(self,
mock_simple_test_runner,
mock_behave_main):
self.run_management_command('behave', simple=True)
mock_behave_main.assert_called_once_with(args=[])
mock_simple_test_runner.assert_called_once_with()

def test_simple_test_runner_uses_simple_testcase(self):
runner = mock.MagicMock()
context = Context(runner)
SimpleTestRunner().before_scenario(context)
assert isinstance(context.test, TestCase)

def test_simple_testcase_fails_when_accessing_base_url(self):
runner = Runner(mock.MagicMock())
runner.context = Context(runner)
SimpleTestRunner().before_scenario(runner.context)
with pytest.raises(AssertionError):
assert runner.context.base_url == 'should raise an exception!'

def test_simple_testcase_fails_when_calling_get_url(self):
runner = Runner(mock.MagicMock())
runner.context = Context(runner)
SimpleTestRunner().before_scenario(runner.context)
with pytest.raises(AssertionError):
runner.context.get_url()
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ deps =
py27: mock
commands =
py.test
{envpython} manage.py behave --simple --tags=~@failing --tags=~@requires-live-http --format=progress
{envpython} manage.py behave --tags=~@failing --format=progress

[testenv:docs]
Expand Down

0 comments on commit 2670e49

Please sign in to comment.