Skip to content

Commit

Permalink
Switch to pytest for unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jmbowman committed Sep 22, 2017
1 parent 990a8cb commit ca97e94
Show file tree
Hide file tree
Showing 30 changed files with 298 additions and 205 deletions.
2 changes: 2 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ omit =
cms/djangoapps/contentstore/views/dev.py
cms/djangoapps/*/migrations/*
cms/djangoapps/*/features/*
cms/lib/*/migrations/*
lms/debug/*
lms/envs/*
lms/djangoapps/*/migrations/*
Expand All @@ -25,6 +26,7 @@ omit =
common/djangoapps/*/migrations/*
openedx/core/djangoapps/*/migrations/*
openedx/core/djangoapps/debug/*
openedx/features/*/migrations/*

concurrency=multiprocessing

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ conf/locale/messages.mo
.testids/
.noseids
nosetests.xml
.cache/
.coverage
.coverage.*
coverage.xml
Expand Down
44 changes: 44 additions & 0 deletions cms/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""
Studio unit test configuration and fixtures.
This module needs to exist because the pytest.ini in the cms package stops
pytest from looking for the conftest.py module in the parent directory when
only running cms tests.
"""

from __future__ import absolute_import, unicode_literals

import importlib
import os
import contracts
import pytest


def pytest_configure(config):
"""
Do core setup operations from manage.py before collecting tests.
"""
if config.getoption('help'):
return
enable_contracts = os.environ.get('ENABLE_CONTRACTS', False)
if not enable_contracts:
contracts.disable_all()
settings_module = os.environ.get('DJANGO_SETTINGS_MODULE')
startup_module = 'cms.startup' if settings_module.startswith('cms') else 'lms.startup'
startup = importlib.import_module(startup_module)
startup.run()


@pytest.fixture(autouse=True, scope='function')
def _django_clear_site_cache():
"""
pytest-django uses this fixture to automatically clear the Site object
cache by replacing it with a new dictionary. edx-django-sites-extensions
grabs the cache dictionary at startup, and uses that one for all lookups
from then on. Our CacheIsolationMixin class tries to clear the cache by
grabbing the current dictionary from the site models module and clearing
it. Long story short: if you use this all together, neither cache
clearing mechanism actually works. So override this fixture to not mess
with what has been working for us so far.
"""
pass
6 changes: 6 additions & 0 deletions cms/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[pytest]
DJANGO_SETTINGS_MODULE = cms.envs.test
addopts = --nomigrations --reuse-db --durations=20 -p no:randomly
norecursedirs = envs
python_classes =
python_files = tests.py test_*.py *_tests.py
8 changes: 8 additions & 0 deletions common/lib/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.conf import settings


def pytest_configure():
"""
Use Django's default settings for tests in common/lib.
"""
settings.configure()
5 changes: 5 additions & 0 deletions common/lib/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[pytest]
addopts = --nomigrations --reuse-db --durations=20
norecursedirs = .cache
python_classes =
python_files = tests.py test_*.py tests_*.py *_tests.py __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,6 @@
import ddt
from contracts import contract
from nose.plugins.attrib import attr
# For the cache tests to work, we need to be using the Django default
# settings (not our usual cms or lms test settings) and they need to
# be configured before importing from django.core.cache
from django.conf import settings
if not settings.configured:
settings.configure()
from django.core.cache import caches, InvalidCacheBackendError

from openedx.core.lib import tempdir
Expand Down
3 changes: 3 additions & 0 deletions common/test/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[pytest]
addopts = -p no:randomly --durations=20
norecursedirs = .cache
10 changes: 10 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
Default unit test configuration and fixtures.
"""

from __future__ import absolute_import, unicode_literals

# Import hooks and fixture overrides from the cms package to
# avoid duplicating the implementation

from cms.conftest import _django_clear_site_cache, pytest_configure # pylint: disable=unused-import
72 changes: 37 additions & 35 deletions docs/testing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -130,14 +130,15 @@ however, run any acceptance tests.

Note -
`paver` is a scripting tool. To get information about various options, you can run the this command.

::
paver -h

paver -h

Running Python Unit tests
-------------------------

We use `nose <https://nose.readthedocs.org/en/latest/>`__ through the
`django-nose plugin <https://pypi.python.org/pypi/django-nose>`__ to run
the test suite.
We use `pytest <https://pytest.org/>`__ to run the test suite.

For example, this command runs all the python test scripts.

Expand Down Expand Up @@ -194,7 +195,7 @@ To run a single django test class use this command.

::

paver test_system -t lms/djangoapps/courseware/tests/tests.py:ActivateLoginTest
paver test_system -t lms/djangoapps/courseware/tests/tests.py::ActivateLoginTest

When developing tests, it is often helpful to be able to really just run
one single test without the overhead of PIP installs, UX builds, etc. In
Expand All @@ -204,23 +205,23 @@ the time of this writing, the command is the following.

::

python ./manage.py lms test --verbosity=1 lms/djangoapps/courseware/tests/test_courses.py --traceback --settings=test
pytest lms/djangoapps/courseware/tests/test_courses.py


To run a single test format the command like this.

::

paver test_system -t lms/djangoapps/courseware/tests/tests.py:ActivateLoginTest.test_activate_login
paver test_system -t lms/djangoapps/courseware/tests/tests.py::ActivateLoginTest::test_activate_login

The ``lms`` suite of tests runs with randomized order, by default.
You can override these by using ``--no-randomize`` to disable randomization.
You can use ``--randomize`` to randomize the test case sequence. In the
short term, this is likely to reveal bugs in our test setup and teardown;
please fix (or at least file tickets for) any such issues you encounter.

You can also enable test concurrency with the ``--processes=N`` flag (where ``N``
is the number of processes to run tests with, and ``-1`` means one process per
available core). Note, however, that when running concurrently, breakpoints may
not work correctly, and you will not be able to run single test methods (only
single test classes).
not work correctly.

For example:

Expand All @@ -239,44 +240,44 @@ To re-run all failing django tests from lms or cms, use the
paver test_system -s lms --failed
paver test_system -s cms --failed

There is also a ``--fail_fast``, ``-x`` option that will stop nosetests
There is also a ``--exitfirst``, ``-x`` option that will stop pytest
after the first failure.

common/lib tests are tested with the ``test_lib`` task, which also
accepts the ``--failed`` and ``--fail_fast`` options.
accepts the ``--failed`` and ``--exitfirst`` options.

::

paver test_lib -l common/lib/calc
paver test_lib -l common/lib/xmodule --failed

For example, this command runs a single nose test file.
For example, this command runs a single python unit test file.

::

nosetests common/lib/xmodule/xmodule/tests/test_stringify.py
pytest common/lib/xmodule/xmodule/tests/test_stringify.py

This command runs a single nose test within a specified file.
This command runs a single python unit test within a specified file.

::

nosetests common/lib/xmodule/xmodule/tests/test_stringify.py:test_stringify
pytest common/lib/xmodule/xmodule/tests/test_stringify.py::test_stringify


This is an example of how to run a single test and get stdout, with proper env config.
This is an example of how to run a single test and get stdout shown immediately, with proper env config.

::

python manage.py cms --settings test test contentstore.tests.test_import_nostatic -s
pytest cms/djangoapps/contentstore/tests/test_import.py -s

These are examples of how to run a single test and get stdout and get coverage.
These are examples of how to run a single test and get coverage.

::

python -m coverage run which ./manage.py cms --settings test test --traceback --logging-clear-handlers --liveserver=localhost:8000-9000 contentstore.tests.test_import_nostatic -s # cms example
python -m coverage run which ./manage.py lms --settings test test --traceback --logging-clear-handlers --liveserver=localhost:8000-9000 courseware.tests.test_module_render -s # lms example
pytest cms/djangoapps/contentstore/tests/test_import.py --cov # cms example
pytest lms/djangoapps/courseware/tests/test_module_render.py --cov # lms example

Use this command to generate coverage report.
Use this command to generate a coverage report.

::

Expand All @@ -297,31 +298,32 @@ you can run one of these commands.
::

paver test_system -s cms -t common/djangoapps/terrain/stubs/tests/test_youtube_stub.py
python -m coverage run `which ./manage.py` cms --settings test test --traceback common/djangoapps/terrain/stubs/tests/test_youtube_stub.py
pytest common/djangoapps/terrain/stubs/tests/test_youtube_stub.py

Very handy: if you pass the ``--pdb`` flag to a paver test function, or
uncomment the ``pdb=1`` line in ``setup.cfg``, the test runner
will drop you into pdb on error. This lets you go up and down the stack
and see what the values of the variables are. Check out `the pdb
documentation <http://docs.python.org/library/pdb.html>`__
documentation <http://docs.python.org/library/pdb.html>`__ Note that this
only works if you aren't collecting coverage statistics (pdb and coverage.py
use the same mechanism to trace code execution).

Use this command to put a temporary debugging breakpoint in a test.
If you check this in, your tests will hang on jenkins.

::

from nose.tools import set_trace; set_trace()

import pdb; pdb.set_trace()

Note: More on the ``--failed`` functionality
Note: More on the ``--failed`` functionality:

* In order to use this, you must run the tests first. If you haven't already
run the tests, or if no tests failed in the previous run, then using the
``--failed`` switch will result in **all** of the tests being run. See more
about this in the `nose documentation
<http://nose.readthedocs.org/en/latest/plugins/testid.html#looping-over-failed-tests>`__.
about this in the `pytest documentation
<https://docs.pytest.org/en/latest/cache.html>`__.

* Note that ``paver test_python`` calls nosetests separately for cms and lms.
* Note that ``paver test_python`` calls pytest separately for cms and lms.
This means that if tests failed only in lms on the previous run, then calling
``paver test_python --failed`` will run **all of the tests for cms** in
addition to the previously failing lms tests. If you want it to run only the
Expand Down Expand Up @@ -501,7 +503,7 @@ To run all the bok choy accessibility tests use this command.

paver test_a11y

To run specific tests, use the ``-t`` flag to specify a nose-style test spec
To run specific tests, use the ``-t`` flag to specify a pytest-style test spec
relative to the ``common/test/acceptance/tests`` directory. This is an example for it.

::
Expand Down Expand Up @@ -565,7 +567,7 @@ Note if setup has already been done, you can run::
You must run BOTH `--testsonly` and `--fasttest`.

3. When done, you can kill your servers in the first terminal/ssh session with
Control-C. *Warning*: Only hit Control-C one time so the nose test framework can
Control-C. *Warning*: Only hit Control-C one time so the pytest framework can
properly clean up.

Running Lettuce Acceptance Tests
Expand Down Expand Up @@ -644,7 +646,7 @@ Running Tests on Paver Scripts

To run tests on the scripts that power the various Paver commands, use the following command::

nosetests pavelib
pytest pavelib


Testing internationalization with dummy translations
Expand Down Expand Up @@ -814,7 +816,7 @@ To view JavaScript code style quality run this command.

::

paver run_eslint --limit=50000
paver run_eslint --limit=50000



Expand Down
9 changes: 5 additions & 4 deletions lms/djangoapps/certificates/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,16 +207,17 @@ def test_with_downloadable_web_cert(self):
)

@ddt.data(
(False, datetime.now(pytz.UTC) + timedelta(days=2), False),
(False, datetime.now(pytz.UTC) - timedelta(days=2), True),
(True, datetime.now(pytz.UTC) + timedelta(days=2), True)
(False, timedelta(days=2), False),
(False, -timedelta(days=2), True),
(True, timedelta(days=2), True)
)
@ddt.unpack
@patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True})
def test_cert_api_return(self, self_paced, cert_avail_date, cert_downloadable_status):
def test_cert_api_return(self, self_paced, cert_avail_delta, cert_downloadable_status):
"""
Test 'downloadable status'
"""
cert_avail_date = datetime.now(pytz.UTC) + cert_avail_delta
self.course.self_paced = self_paced
self.course.certificate_available_date = cert_avail_date
self.course.save()
Expand Down
4 changes: 1 addition & 3 deletions lms/djangoapps/certificates/tests/test_tasks.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
from unittest import TestCase

import ddt
from django.test import TestCase
from mock import call, patch
from opaque_keys.edx.keys import CourseKey
from nose.tools import assert_true

from lms.djangoapps.certificates.tasks import generate_certificate
from student.tests.factories import UserFactory
Expand Down
1 change: 1 addition & 0 deletions lms/djangoapps/courseware/field_overrides.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ def default(self, block, name):

class OverrideModulestoreFieldData(OverrideFieldData):
"""Apply field data overrides at the modulestore level. No student context required."""
provider_classes = None

@classmethod
def wrap(cls, block, field_data): # pylint: disable=arguments-differ
Expand Down
5 changes: 5 additions & 0 deletions lms/djangoapps/courseware/tests/test_module_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ def setUp(self):
Set up the course and user context
"""
super(ModuleRenderTestCase, self).setUp()
OverrideFieldData.provider_classes = None

self.mock_user = UserFactory()
self.mock_user.id = 1
Expand All @@ -154,6 +155,10 @@ def setUp(self):
)
)

def tearDown(self):
OverrideFieldData.provider_classes = None
super(ModuleRenderTestCase, self).tearDown()

def test_get_module(self):
self.assertEqual(
None,
Expand Down
Loading

0 comments on commit ca97e94

Please sign in to comment.