django/

python: Django-like testrunner

Django-like testrunner for non-django projects:

#!/usr/bin/env python

import argparse
import collections
import unittest
import sys


def main():
    parser = argparse.ArgumentParser(description='Django-like testrunner')
    parser.add_argument('specific_test', metavar='', type=str, nargs='?', help='Specifc test')
    parser.add_argument("--failfast", action="store_true")
    parser.add_argument("--verbosity", type=int, default=1)

    args = parser.parse_args()

    loader = unittest.TestLoader()
    all_tests = loader.discover('.', top_level_dir="./")
    suite = unittest.TestSuite()

    if args.specific_test:

        def walk_tests(tests):
            if isinstance(tests, collections.Iterable):
                for item in tests:
                    walk_tests(item)
                return
            if tests.id().startswith(args.specific_test):
                suite.addTest(tests)
            elif not str(tests).startswith("test"):
                sys.exit("Error in file %s" % tests)

        walk_tests(all_tests)
    else:
        suite.addTests(all_tests)

    unittest.TextTestRunner(verbosity=args.verbosity, failfast=args.failfast).run(suite)


if __name__ == "__main__":
    main()
| 1 Mar 2018

Test context of the rendered template with RequestFactory in Django

When writing tests for the Django’s view functions I prefer to use RequestFactory instead of the django test client. Tests that use RequestFactory are easier to understand and they are faster. For example, you need to test a simple view function:

def incoming_queue(request):
    if request.method == "POST":
        pass
    else:
        incoming_queue_list = UsageHistory.objects.filter(company=request.user.agent.company, queued=True)
        context = {
            "incoming_queue_list": incoming_queue_list,
            "page": "queue",
            "agent_available_url": reverse("agent_available")
        }
        return render(request, 'agents/incoming_queue.html', context)

If you want to test the context of the rendered template, it is easy to do with django test client as it attaches it to the response. If you want to use RequestFactory for that purpose, there is no easy way to do it. It would be comfortable to have something like assertTemplateContextIn(expected_context) method, that checks that expected_context was used during the template rendering. The following code was inspired by assertTemplateUsed method:

from copy import copy

from django.db.models.query import QuerySet
from django.test.signals import template_rendered
from django.test.utils import ContextList


class TemplateContextTestCase(object):
    def assertTemplateContextIn(self, expected_context=None):
        """
        Asserts that the template was rendered with the context that includes
        expected_context. Example:
            with self.assertTemplateContextIn(expected_context):
                vuew_function(self.request)
        """
        return _AssertTemplateContext(self, expected_context)


class _AssertTemplateContext(object):
    def __init__(self, test_case, expected_context):
        self.test_case = test_case
        self.expected_context = expected_context
        self.context = ContextList()

    def on_template_render(self, sender, signal, template, context, **kwargs):
        self.context.append(copy(context))

    def test(self):
        missing = []
        mismatched = []
        for key in self.expected_context.keys():
            if key in self.context:
                if self.context[key] != self.expected_context[key]:
                    match = False
                    # Compare QuerySets
                    if isinstance(self.context[key], QuerySet) and isinstance(self.expected_context[key], QuerySet):
                        if self.context[key].model == self.expected_context[key].model:
                            if list(self.context[key].values_list("id", flat=True)) == \
                               list(self.expected_context[key].values_list("id", flat=True)):
                                match = True
                    if not match:
                        mismatched.append(key)
            else:
                missing.append(key)
        return missing, mismatched

    def __enter__(self):
        template_rendered.connect(self.on_template_render)
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        template_rendered.disconnect(self.on_template_render)
        if exc_type is not None:
            return

        missing, mismatched = self.test()
        message = ""
        if missing:
            message = message + "Missing keys: " + ", ".join(x for x in missing) + "\n"
        if mismatched:
            for item in mismatched:
                message = message + "Context key mismatch '%s': %s != %s \n" % (
                    item, self.expected_context[item], self.context[item]
                )
        if message:
            self.test_case.fail(message)

We often pass querysets instances to the context of the rendered template. The code above checks if 2 QuerySets are the same by comparing their models and ids of the elements. TemplateContextTestCase class can be used to extend TestCase class:

from django.test import TestCase, RequestFactory

from yourapp.tests.helpers import TemplateContextTestCase
from django.core.urlresolvers import reverse


class TestIncomingQueue(TestCase, TemplateContextTestCase):
    def test_context(self):
        agent = AgentFactory()
        self.request.user = agent.user
        expected = {
            "page": "queue",
            "agent_available_url": reverse("agent_available"),
            "incoming_queue_list": UsageHistory.objects.none()
        }
        with self.assertTemplateContextIn(expected):
            incoming_queue(self.request)
| 1 Mar 2017