Django: Testing

This post is part of my Django series. You can see an overview of the series along with instruction on how to get all the source code here.

This article assumes you are comfortable creating a Django project, can create apps and register them into the INSTALLED_APPS list of the settings file. If not please read my Django HelloWorld article.

Django uses the Python package unittest. This article assumes that you have a basic knowledge of creating unittests with this package. If this is not the case please see my blog post on unittest.

This article assumes you have a project called DjangoSandBox in an application called testintroduction.

This article tests the various parts we have built up over the series; please see here for any explanations of the code we are testing.

Testing Models

Models are pretty easy to test; our first example is using the HelloWorld class we have created previously. Nothing special, a name and date field, a __str()__ function and a Django get_absolute_url function.

#helloworld/models.py
from django.db import models
from django.core.urlresolvers import reverse

class HelloWorld(models.Model):
    first_name = models.CharField(max_length=20)
    now = models.DateTimeField()

    def __str__(self):
        return "{0} {1}".format(self.first_name, self.now)

    @staticmethod
    def get_absolute_url():
        return reverse('helloworld:model')

Testing these functions is nothing more than creating an instance of the class, calling the require function or property and asserting that our result is as required.

#test_models.py

from datetime import date
from django.test import TestCase
from helloworld.models import HelloWorld

class TestHelloWorld(TestCase):
    def setUp(self):
        self.first_name = "Luke"
        self.now = date.today()

        self.a_helloworld = HelloWorld(first_name=self.first_name, now=self.now)

    def test_str(self):
        self.assertEqual("{0} {1}".format(self.first_name, self.now), str(self.a_helloworld))

    def test_get_absolute_url(self):
        self.assertEqual("/helloworld/model/", self.a_helloworld.get_absolute_url())

Our model could contain validation. We can test validation by creating an instance of the model in an invalid state and calling full_clean. If the model is invalid an exception is raised with all the errors contained within a dictionary upon the message_dict property.

# test_models.py
from datetime import date
from django.core.exceptions import ValidationError
from django.test import TestCase
from modelsadvanced.models import ContactDetails, ProxyChild

class TestModelValidation(TestCase):

    def test_model_validation(self):

        try:
            a_contact = ContactDetails(name="Luke", age=101, contactDate=date(year=2015, month=7, day=2))
            a_contact.full_clean()
        except ValidationError as e:

            self.assertEqual({'name': ['Ensure this value has at least 10 characters (it has 4).', 'Luke is barred'],
                              'age': ['Ensure this value is less than or equal to 100.'],
                              'contactDate': ['2015-07-02 is not a future date.']}, e.message_dict)

If we want to test class meta data we can access this via the _meta property:

#test_models.py
class TestModelMeta(TestCase):
    def test_model_meta(self):
        self.assertEqual(True, ProxyChild()._meta.proxy)

We can use list and map to get a list of all field names contained within the fields property of the meta class.:

#test_models.py
class TestModelFields(TestCase):
    def test_model_meta(self):
        field_names = list(map((lambda x: x.name), ContactDetails._meta.fields))

        self.assertEqual(4, len(ContactDetails._meta.fields))
        self.assertEqual(['id', 'age', 'name', 'contactDate'], field_names)

Testing Views

When we test views we can use the test client; it acts as a dummy Web browser. We access it via the client property of our unit test class.

We call get upon the test client along with the URL.

response = self.client.get("/index")

We can use reverse to access the URL via our routing config. Don’t worry we will explicitly test the routing config later/

response = self.client.get(reverse("home:home"))

The response.status_code can be used to assure our response code is as expected.

The response.resolver_match can be use to ensure that the view function used was as expected.

The function self.assertTemplateUsed can be used to assure that the correct template was used.

We can assert that the html is as expected by calling self.assertContains along with sections of html or strings which should be contained within the rendered HTML.

#test_views.py

from django.core.urlresolvers import reverse
from django.test import TestCase
from home import views as home_views

class TestFunctionView(TestCase):
    def test_function_view(self):
        response = self.client.get(reverse("home:home"))

        self.assertEqual(200, response.status_code)
        self.assertEqual(home_views.home, response.resolver_match.func)
        self.assertTemplateUsed(response, template_name='home/home.html')

        self.assertContains(response, '<a href="/helloworld/index/">Hello World</a>')
        self.assertContains(response, '<a href="/templatesintroduction/index">Templates</a>')
        self.assertContains(response, '<a href="/formsintroduction/basic/">Forms</a>')
        self.assertContains(response, '<a href="/viewsintroduction/contact/list">Views</a>')

We can access the context data which was passed to the view via the context property of the response.

#test_views.py

from django.core.urlresolvers import reverse
from django.test import TestCase
from helloworld import views as helloworld_views

class TestContextData(TestCase):
    def test_context_data(self):
        response = self.client.get(reverse("helloworld:template"))

        self.assertEqual('Obelix', response.context["who"])
        self.assertEqual(False, response.context["islong"])

Testing Class Based Views

In the example two up we used the following code to ensure the correct view function was used:

self.assertEqual(home_views.home, response.resolver_match.func)

If we want to assure that the correct class view was used we need to compare the name of the class against that which was used:

self.assertEqual(PhoneAddressListView.as_view().__name__, response.resolver_match.func.__name__)

Where a base template is used we can simply call the function assertTemplateUsed with the base template name.

In the example below we test the list view with and without records to ensure our “no records here” message is displayed to the user.

When records are returned to our template we have a few ways we can test the results along with the assertContains function we have seen in the previous examples.

  • Check the record count of the object list in the contect view
  • Compare elements
  • Compare the query set. To do this we need to convert the expected query set to a list of strings from the repr function as this is what is contained in the response.
#test_views.py

class TestClassView(TestCase):
    def test_with_no_records(self):
        response = self.client.get(reverse("viewsintroduction:address_list"))

        self.assertEqual(200, response.status_code)
        self.assertEqual(PhoneAddressListView.as_view().__name__, response.resolver_match.func.__name__)

        self.assertTemplateUsed(response, template_name='viewsintroduction/base.html')
        self.assertTemplateUsed(response, template_name='viewsintroduction/phoneaddresslist.html')

        self.assertEqual(0, len(response.context["phoneaddress_list"]))
        self.assertContains(response, '<tr><td colspan="6">There are no addresses</td></tr>')

    def test_with_one_record(self):
        an_address = PhoneAddress(number=1, street_name="A", city="A City")
        an_address.save()

        response = self.client.get(reverse("viewsintroduction:address_list"))

        self.assertEqual(200, response.status_code)
        self.assertEqual(PhoneAddressListView.as_view().__name__, response.resolver_match.func.__name__)

        self.assertTemplateUsed(response, template_name='viewsintroduction/base.html')
        self.assertTemplateUsed(response, template_name='viewsintroduction/phoneaddresslist.html')

        context_addresses = response.context['phoneaddress_list']
        expected_addresses = [repr(r) for r in PhoneAddress.objects.all()]

        self.assertEqual(1, len(context_addresses))
        self.assertEqual(an_address, context_addresses.first())

        self.assertQuerysetEqual(context_addresses, expected_addresses, ordered=False)

        self.assertContains(response, an_address.city)
        self.assertContains(response, an_address.number)
        self.assertContains(response, an_address.street_name)

        self.assertNotContains(response, '<tr><td colspan="6">There are no addresses</td></tr>')

Testing Errors

We now move on to test some of our error views; by comparing the status code.

#test_views.py

class Test404Error(TestCase):
    def test_404_error_is_raised(self):
        response = self.client.get(reverse("helloworld:error_as_404"))
        self.assertEqual(404, response.status_code)

class TestNotAllowed(TestCase):
    def test_not_allowed(self):
        response = self.client.get(reverse("helloworld:not_allowed"))

        self.assertEqual(405, response.status_code)


class TestOnlyPost(TestCase):
    def test_raises_error_on_get(self):
        response = self.client.get(reverse("helloworld:error_if_not_post"))

        self.assertEqual(405, response.status_code)

    def test_all_ok_on_post(self):
        response = self.client.post(reverse("helloworld:error_if_not_post"), {'name': 'Jim'})

        self.assertEqual(200, response.status_code)
        self.assertContains(response, "{0}, this can only be called with a post".format("Jim"))

When we expect a redirect is made we can use the assertRedirects function to validate this. The function takes the status codes of our page load and also the page we direct to. 301 indicates that this is a page which has moved permanently.

#test_views.py

class TestRedirect(TestCase):
    def test_redirect(self):
        response = self.client.post(reverse("viewsintroduction:redirect_view"))

        self.assertTrue(301, response.status_code)
        self.assertRedirects(response, "http://djangoproject.com", 301, 200)  # Status codes Us/Them

Test URLS

When we test URLs we should test two things;

  • The reverse function returns the correct URL string
  • The resolve function returns the correct view function. This does not only include the function and URL name but the app name of the view, the URL namespace and any arguments which will be passed into the view function.
#test_urls.py

from django.core.urlresolvers import reverse, resolve
from django.test import TestCase

from home import views

class TestHomeUrls(TestCase):
    urls = 'home.urls'

    def test_reverse(self):
        self.assertEqual('/', reverse('home'))

    def test_resolve(self):
        resolver = resolve(reverse('home'))

        self.assertEqual(views.home, resolver.func)
        self.assertEqual('home', resolver.url_name)

        self.assertEqual((), resolver.args)
        self.assertEqual({}, resolver.kwargs)
        self.assertEqual(None, resolver.app_name)
        self.assertEqual('', resolver.namespace)

Note: Setting urls = ‘home.urls’ upon the test class above defines the URL routing config we are using. Here we test only an apps config which explains why we reference the URL by their name only without the namespace which we set in the project’s urls.py

When our URL config contains get query parameters we can pass them into the reverse function with the kwargs parameter which is a dictionary.

#test_urls.py

class TestViewsIntroductionUrls(TestCase):
    urls = 'viewsintroduction.urls'

    def test_reverse(self):
        self.assertEqual('/address/1/update/', reverse('address_update', kwargs={'pk': 1}))

Testing Forms

When testing forms we pass the input data as a dictionary into the constructor.

We can access the form fields via the fields property; below we ensure that we have the correct fields and that they contain the right configuration.

We can assert that the form validates as expected with form.is_valid(). Any errors raised will be found within the property errors on the form which is a dictionary of errors. Field errors will be found under a key named after the field name; it returns a list of errors.

#test_forms.py

from django.core.urlresolvers import reverse
from django.forms import DecimalField
from django.test import TestCase

from formsintroduction.forms.bastic_form_example import BasicFormExample
from formsintroduction.forms.class_form_example import ClassBasedForm
from viewsintroduction.models import PhoneAddress


class TestBasicForm(TestCase):
    def test_fields(self):
        form = BasicFormExample({})

        self.assertEqual(2, len(form.fields))
        self.assertTrue("height" in form.fields.keys())
        self.assertTrue("name" in form.fields.keys())
        self.assertIsInstance(form.fields['height'], DecimalField)
        self.assertEqual(2, form.fields['height'].max_value)

    def test_valid_data(self):
        form = BasicFormExample({"name": "Luke", "height": 1.11})

        self.assertTrue(form.is_valid())

    def test_invalid_data(self):
        form = BasicFormExample({"name": "Luke"})

        self.assertFalse(form.is_valid())
        self.assertTrue("name" not in form.errors.keys())
        self.assertTrue("height" in form.errors.keys())

        self.assertEqual(form.errors["height"], ['This field is required.'])

Testing Class Derived Forms

Class based views are tested in pretty much the same way. However when we call save we get an instance of the record created which we can test against.

In the example below we test the form meta data to ensure that the correct fields are set along with the right model class.

#test_forms.py

class TestClassBasedForm(TestCase):
    def test_valid_data(self):
        form = ClassBasedForm({"number": 123, "street_name": "The high street", "city": "Bristol"})

        self.assertTrue(form.is_valid())

        a_record = form.save()

        self.assertEqual(123, a_record.number)
        self.assertEqual("The high street", a_record.street_name)
        self.assertEqual("Bristol", a_record.city)

    def test_invalid_data(self):
        form = ClassBasedForm({})

        self.assertEqual(form.errors["number"],
                         ['We need a number of the house!', 'None of the fields have been set!!!'])

    def test_meta(self):
        self.assertEqual(PhoneAddress, ClassBasedForm.Meta.model)
        self.assertEqual(("city", "street_name", "number"), ClassBasedForm.Meta.fields)

Posting Forms

We use the test client to test posting a form to the server. We pass the form data as a dictionary along with the URL.

#test_forms.py

    def test_post_invalid(self):
        response = self.client.post(reverse("formsintroduction:form_class_create"),
                                    {"city": "Word_Word", "street_name": "street", "number": 1},
                                    follow=True)

        self.assertEqual(200, response.status_code)
        self.assertFormError(response, 'form', 'city', ["Name must be a capitalised single word"])

    def test_post_valid(self):
        response = self.client.post(reverse("formsintroduction:form_class_create"),
                                    {"city": "Foo", "street_name": "Foo", "number": 1},
                                    follow=True)

        self.assertEqual(200, response.status_code)
        self.assertRedirects(response, "http://testserver/viewsintroduction/address/1/", 302)

Testing An Authorised View

Unsuccessful Authentication

The Django authorisation system will automatically redirect a user to the registered login page when they try to access a view which requires authentication. Our first test case assures that we are redirected successfully.

from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.test import TestCase


class TestAuthedView(TestCase):
    def test_redirect_to_loging(self):
        response = self.client.post(reverse("auth:users"))

        self.assertTrue(302, response.status_code)
        self.assertRedirects(response, "auth/login/?next=/auth/users/", 302, 200)

In the above example the 302 and 200 parameters passed into assertRedirects are status codes of the HTTP for the initial page and then the redirect page. 302 is a redirect while 200 is a successful request.

Successful Authentication

The Django test web client comes with the ability to log a user in:

self.client.login(username='username', password='password')

Note: don’t forget that when we are running tests we start with a blank database so you will need to create a login first. The example below creates a new user and activates it in line; this should be moved to a test fixture set-up function.

from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.test import TestCase

class TestAuthedView(TestCase):

    def test_can_login(self):
        user = User.objects.create_user(username='FOOTESTFOO', password='pwd')
        user.is_active = True
        user.save()

        login = self.client.login(username='FOOTESTFOO', password='pwd')

        self.assertTrue(login)

        response = self.client.post(reverse("auth:users"))

        self.assertTrue(200, response.status_code)
        self.assertContains(response, "Registered Users")
        self.assertContains(response, "First Name")
        self.assertContains(response, "Last Name")
        self.assertContains(response, "FOOTESTFOO")

Testing Cookies

We can access cookies from the cookies property of the Django test web client once we have called post or get. It is a dictionary keyed upon the cookie name:

self.client.cookies
self.client.cookies["hits"]

The cookie is returned as a http.cookies.Morsel. We can access the cookie value with the value property. The class responds as a dictionary for other data, in the following example we ensure the expiry has been set correctly with the max-age key.

from django.core.urlresolvers import reverse
from django.test import TestCase

class TestSessions(TestCase):
    def test_cookie(self):
        response = self.client.post(reverse("sessionsandcookies:cookies"))

        self.assertTrue(200, response.status_code)

        self.assertTrue("hits" in self.client.cookies)
        self.assertEqual('1', self.client.cookies["hits"].value)
        self.assertEqual(30, self.client.cookies["hits"]["max-age"])

Testing Session Data

We can access session data upon the Django test web client after we have called post or get. It returns a SessionBase which responds as a dictionary to retrieve the session data from their key names. All functionality of the SessionBase class is available; below we call get_expiry_age to ensure our session expiry policy has worked as expected:

from django.core.urlresolvers import reverse
from django.test import TestCase


class TestSessions(TestCase): 

    def test_sessions(self):
        response = self.client.post(reverse("sessionsandcookies:sessions"))

        self.assertTrue(200, response.status_code)

        self.assertTrue("has_visited" in self.client.session)
        self.assertEqual('True', self.client.session["has_visited"])
        self.assertEqual(10, self.client.session.get_expiry_age())

Additional Assertions

As mentioned previously, Django provides some additional assert functions to the standard Python unittest. class We have covered many of them above though there are two others which I think will be quite useful.

The assertFieldOutput assertion allows us to validate against good and bad field input data. I tried to get this to work against the defined fields or a model or form however it seems to only work with the class type.

The assertNumQueries assertion will ensure that the database has been touched the correct number of times.

#test_additionl_assertions.py

from django.forms import EmailField, CharField
from django.test import TestCase

from viewsintroduction.models import PhoneAddress


class TestAdditionalAssertions(TestCase):
    def test_assertFieldOutput(self):
        self.assertFieldOutput(EmailField, {'a@a.com': 'a@a.com'}, {'aaa': ['Enter a valid email address.']})

        self.assertFieldOutput(CharField, {'aa': 'aa'},
                               {'word': ['Ensure this value has at most 3 characters (it has 4).'],
                                'a': ['Ensure this value has at least 2 characters (it has 1).']}, [],
                               {"max_length": 3, "min_length": 2})

    def test_assert_num_queries(self):
        with self.assertNumQueries(1):
            PhoneAddress.objects.create(city="Foo", street_name="Foo", number=1)

        self.assertNumQueries(1, lambda: PhoneAddress.objects.create(city="Foo", street_name="Foo", number=1))

References

Python: Unit Testing

Unit Testing

This is part of my Python & Django Series which can be found here including information on how to download all the source code.

The ability for software to test and diagnose itself is a powerful feature.

A Simple Example

Lets take a simple function which adds two numbers together.

Code:

def add_two_numbers(a, b):
    """
    A simple method to test
    """

    return a + b

We can create a test to ensure that add_two_numbers works as expected by comparing the result of a call to the function with our expected result.

Code:

from unittest import TestCase, main

class MyTestClass(TestCase):
    """
    A simple unit test example
    """

    def test_add_two_numbers(self):
        self.assertEqual(add_two_numbers(1, 2), 3)

A test class inherits from unittest.TestCase. All functions which are prefixed with test_ will be determined as tests which are required to be run.

Above we call add_two_numbers with parameters 1 and 2. We then use the returned value as a parameter to the assertEqual function along with our expected result of 3.

If the assertion validates as expected the assertion returns allowing control to carry on, otherwise an error is raised and the test is marked as failed.

A test function can have any number of assertions called.

We can run our test function with the main function from unittest.

Code:

if __name__ == '__main__':
    main()

Output:

.py::MyTestClass true
Testing started at 12:42 …

Process finished with exit code 0

If a bug appeared in our code we would see a result similar to the following.

Output:

.py::MyTestClass true
Testing started at 12:44 …

Process finished with exit code 0

Failure
Traceback (most recent call last):
File “/data/data/Dropbox/Development/SandBox/Git/ThePythonPit/PythonSandBox/Testing/unittest_examples/simple_example.py”, line 26, in test_add_two_numbers
self.assertEqual(add_two_numbers(1, 2), 4)
AssertionError: 3 != 4

Assertions

In the previous section we saw the assertEqual assertion. The unittest module provides many assertion functions to cater for a range of possible test criteria.

Equals Assertions

Equality assertion can be made with the assertEqual and inequality assertion can be made with the assertNotEqual function. Both functions take two parameters; the result and the expected result.

Code:

self.assertEqual(1, 1)
self.assertNotEqual(1, 2)

For numerical results the assertAlmostEqual and assertNotAlmostEqual functions allow equality assertion within a tolerance of error. The tolerance is passed in as the third parameter and represents the number of decimal places to be used when determining equality.

The call to assertAlmostEqual takes 1.1 and 1.11 with a tolerance of 1 d.p. This would fail if we used assertEqual but as 1.11 becomes 1.1 when rounding to 1 d.p and therefore the assertion passes.

Code:

self.assertAlmostEqual(1.1, 1.11, 1)  # 3rd argument is the precession
self.assertNotAlmostEqual(1.1, 1.11, 2)  # 3rd argument is the precession

The assertEqual function can take most types. All of the following asserts for lists, tuples, sets, dictionaries and multi-line strings pass assertion.

When being called for collections, the test requires both collections to be of the same type, contain the same number of elements and the elements at the same ordinal position to be equal.

Code:

self.assertEqual([1, 2, 3], [1, 2, 3]) # list
self.assertEqual((1, 2, 3), (1, 2, 3)) # tuple
self.assertEqual({1, 2, 3}, {1, 2, 3}) # set
self.assertEqual({'a': 1}, {'a': 1})  # dictionary
self.assertEqual("onentwo", "onentwo") # multi-line string

Unittest does provide specific assert equal functions for each type though these are implicitly called via the assertEquals function. You should favour using the assertEquals functions.

Code:

self.assertListEqual([1, 2, 3], [1, 2, 3])
self.assertTupleEqual((1, 2, 3), (1, 2, 3))
self.assertSetEqual({1, 2, 3}, {1, 2, 3})
self.assertDictEqual({'a': 1}, {'a': 1})
self.assertMultiLineEqual("onentwo", "onentwo")

Code:

The assertEqual function works upon equality; as such an integer of value 1 and a float of value 1.0 will pass an assertion check together.

self.assertEqual(1.0, 1)

Booleans Assertions

The assertFalse and assertTrue functions for ensuring that a boolean type is either false or true respectively.

Code:

self.assertFalse(False)
self.assertTrue(True)

Collections Assertions

A number of assertions specifically for collections are provided.

We have already seen the assertEqual function which determines if two parameters are equal.

When working with collections this performs the following checks

  • The collection types are equal
  • The collections contain the same number of elements
  • Each element at the same ordinal position equals that in the other collection.

The elements can be of another type as long as their values are equal. In the example below one list contains integers and the other floats but the assertion passes as the elements are equal.

Code:

self.assertEqual([1.0, 2.0, 3.0], [1, 2, 3]) 

The assertSequenceEqual function works the same as assertEqual though it will not fail if the collections are of different types. Below we ensure that the contents of a list and a tuple are equal.

Code:

self.assertSequenceEqual((1, 2, 3), [1, 2, 3])  # Checks only the sequence

The assertIn and assetNotIn funcitons allows checks to see if an element is contained or not contained within a collection. The check is based upon equality.

Here we check that 1 is in 1,2,3 and that 4 is not in 1, 2, 3.

Code:

self.assertIn(1, (1, 2, 3))
self.assertNotIn(4, (1, 2, 3))

The assertCountEqual function has to be a contender for the worst named function in history. This function ensures that two collections contain exactly the same elements though their order is not important.

Code:

self.assertCountEqual((1, 2, 3), (3, 2, 1))  # Badly named. This checked elements and not their order

Comparison Assertions

Python provides the comparison checks in the form of less than, less than or equal to, greater than and greater than or equal to.

Code:

self.assertLess(1, 10)
self.assertLessEqual(1, 1)
self.assertGreater(10, 1)
self.assertGreaterEqual(1, 1)

Identity Assertions

Identity ensures that two parameters point to the same object instance.

In Python each type instance is assigned it’s own object id upon creation. More information can be found here .

The assertIs and assertIsNot can ensure that two objects are and are not the same instance respectively.

Code:

self.assertIs(1, 1)
self.assertIsNot(1, 2)

For parameters which are not referencing any data or have not been initialised they will point to the None type. Here we can check to see if a parameter is pointing to or not pointing to None with the assertIsNone and assertIsNotNone functions.

Code:

self.assertIsNone(None)
self.assertIsNotNone(1)

The assertIsInstance and assertNotIsInstance functions can be used to see if a parameter holds a specific type. Here we pass a parameter holding an instance of a type along with the class name of the type that we want to insure it references or does not reference.

Code:

self.assertIsInstance((), tuple)
self.assertNotIsInstance((), set)

Regular Expressions Assertions

Code:

We can use regular expressions to ensure the format of a string is as expected with the assertRegex and assertNotRegex functions

self.assertRegex('Luke', "^[a-zA-Z]{3,4}$")
self.assertNotRegex('Lukey', "^[a-zA-Z]{3,4}$")

Exceptions Assertions

Code should throw exceptions when we want it to or when it is called incorrectly. We can use the assertRaises function to assert that not only an exception is raised but it is of a certain type.

Below we ensure that a ZeroDivisionError error is raised.

Code:

with self.assertRaises(ZeroDivisionError) as ex:
    result = 1 / 0

self.assertEqual(str(ex.exception), "division by zero")

In the above example we assign the raised exception to a variable ex, we can then run assertions upon the exception to make sure it is as expected. We check the string representation of the object is as expected. The latter check can be enforced with the assertRaisesRegex function.

Code:

with self.assertRaisesRegex(ZeroDivisionError, "^division by [a-zA-z]{4}$"):
    result = 1 / 0

We can also annotate a test with the @expectedFailure attribute. Here the test will fail if an error is not raised.

Output:

  @expectedFailure
    def test_expectedFailure(self):
        self.fail("This is an expected failure")

Warnings Assertions

Python provides the same functions for warnings as it does for exceptions; they work in exactly the same way

Code:

with self.assertWarns(DeprecationWarning) as wn:
    warn("deprecated", DeprecationWarning)

self.assertEqual(str(wn.warning), "deprecated")

with self.assertWarnsRegex(DeprecationWarning, "^deprecate[a-z]$"):
    warn("deprecated", DeprecationWarning)

Assertions Messages

Each assertion can optionally take a string to be used as an error message when the test fails.

Code:

self.assertFalse(False, "False is not false!")

Would report as the following:

Output:

AssertionError: True is not false : False is not false

The following would be reported if the error message had not been provided.

Output:

AssertionError: True is not false

Failing Tests

We can fail a test in code with the fail method.

Code:

self.fail("Fail!!!")

Test Fixture

If a test class has a function called setUp, it will be run before every test function within it. If an error is raised within the setUp function then no test functions will be run.

If a test class has a function called tearDown, it will be run after every test function within it. This function will always be run after each test function regardless if the test passes or fails.

Code:

from unittest import TestCase


class TestFixtureExample(TestCase):

    def setUp(self):
        # Set up / initialise before a test
        # If this fails then no tests will be run
        print("In the setUp")

    def tearDown(self):
        # Destroy any resources required during the test
        # Will always be run if setUp runs regardless of tests successes
        print("In the tearDown")

    def test_fixture_one(self):
        self.assertTrue(True)

    def test_fixture_two(self):
        self.assertTrue(True)

    def test_fixture_three(self):
        self.assertTrue(True)

Output:

.py::TestFixtureExample true
Testing started at 14:17 …
In the setUp
In the tearDown
In the setUp
In the tearDown
In the setUp
In the tearDown

Test Suite

The TestSuite class can be used to register tests which can then be run with the TextTestRunner.

The addTest can be used to add an individual test method into a TestSuite instance.

The TestLoader().loadTestsFromTestCase() can be used to create a TestSuite with all test functions of a test class.

The TextTestRunner().run() function can then run all TestSuites passed in.

**Code:

from unittest import TestSuite, TextTestRunner, TestLoader

# Test Suite
def my_test_suite():
    suite_one= TestSuite()
    suite_one.addTest(MyTestClass('test_add_two_numbers')) # Adds MyTestClass.test_add_two_numbers()

    suite_two = TestLoader().loadTestsFromTestCase(TestAssertsExample)

    return TestSuite([suite_one, suite_two])

# Run the test suite
if __name__ == '__main__':
    TextTestRunner().run(my_test_suite())

Skipping Tests

Test functions can be annotated with specific unittest attributes.

Skip can be used to stop a test from running. This can also be done in code with the SkipTest function

SkipIf can be used to stop a test from running if a boolean statement evaluates to true.

SkipUnless can be used to stop a test from running unless a boolean statement evaluates to true.

Code:

class TestAttributes(TestCase):

    @skip("Test is not run")
    def test_skip(self):
        self.fail("This should not be run")

    @skipIf(True, "This is not run")
    def test_skipIf(self):
        self.fail("This should not be run")

    @skipUnless(False, "This is not run")
    def test_skipUnless(self):
        self.fail("This should not be run")

    def test_skipTest(self):
        SkipTest("This should not be run")