Locally testing Python Morsels exercises

Trey Hunner smiling in a t-shirt against a yellow wall
Trey Hunner
5 min. read Python 3.7—3.11
Share
Copied to clipboard.

Let's talk about testing Python Morsels exercise solutions locally.

The Exercise Statement

Let's say we have an exercise to create a Python class that represents a rectangle. The Rectangle class should accept an optional length and width and when initialized and it has length, width, perimeter, and area attributes. It should also have a nice string representation.

The bonus requires us to make Rectangle objects comparable (with equality and inequality).

Our solution

We've written a solution that represents the required Rectangle class in a rectangle module (a rectangle.py file):

class Rectangle:

    """The rectangle class"""

    def __init__(self, length=1, width=1) -> None:
        self.length = length
        self.width = width
        self.perimeter = 2 * (self.length + self.width)
        self.area = self.length * self.width

    def __repr__(self) -> str:
        return f"Rectangle({self.length}, {self.width})"

Manually testing your code

We can manually test our code by passing our rectangle.py file to a Python interpreter along with a -i argument:

$ python3 -i rectangle.py
>>>

This will run rectangle.py from the command-line and drop into an interactive Python interpreter (REPL):

>>> r1 = Rectangle()
>>> r1
Rectangle(1, 1)
>>> r1.area
1
>>> r2 = Rectangle(4,3)
>>> r2
Rectangle(4, 3)
>>> r1.perimeter
4
>>> r2.perimeter
14
>>> r2.width
3

We can create objects (like r1 and r2 above) and can play around with them to verify the functionality of our class.

Test Script

Every Python Morsels exercise comes with a test script for testing your code locally. The test script for this exercise (test_rectangle.py) contains several individual test cases to verify that our code works as expected.

This is the test script provided for this exercise, test_rectangle.py, which tests our Rectangle class:

import unittest

from rectangle import Rectangle


class RectangleTests(unittest.TestCase):

    """Tests for Rectangle."""

    def test_length_width(self):
        rect = Rectangle(3, 6)
        self.assertEqual(rect.length, 3)
        self.assertEqual(rect.width, 6)

    def test_default_values(self):
        rect = Rectangle()
        self.assertEqual(rect.length, 1)
        self.assertEqual(rect.width, 1)

    def test_perimeter(self):
        rect = Rectangle(3, 6)
        self.assertEqual(rect.perimeter, 18)

    def test_area(self):
        rect = Rectangle(3, 6)
        self.assertEqual(rect.area, 18)

    def test_string_representation(self):
        rect = Rectangle(3, 6)
        self.assertEqual(str(rect), "Rectangle(3, 6)")
        self.assertEqual(repr(rect), "Rectangle(3, 6)")

    # To test the Bonus part of this exercise, comment out the following line
    @unittest.expectedFailure
    def test_equality(self):
        a = Rectangle()
        b = Rectangle(1, 1)
        self.assertEqual(a, b)
        self.assertFalse(a != b)


if __name__ == "__main__":
    unittest.main(verbosity=2)

Running the automated tests

To test our Rectangle class, we'll save the test_rectangle.py test script (downloaded from the Python Morsels website) in the same folder/directory as your rectangle.py file.

rectangle/
│
├── rectangle.py
└── test_rectangle.py

We also need to make sure our current working directory is the directory that contains our code (rectangle.py) and our test script (test_rectangle.py). That may require changing directories (to wherever we've saved these files):

$ cd /home/trey/python_morsels/rectangle/

We can now run the automated tests against our code by running our test script:

$ python3 test_rectangle.py

The output shows whether a given test passed or failed:

test_area (__main__.RectangleTests) ... ok
test_default_values (__main__.RectangleTests) ... ok
test_equality (__main__.RectangleTests) ... expected failure
test_length_width (__main__.RectangleTests) ... ok
test_perimeter (__main__.RectangleTests) ... ok
test_string_representation (__main__.RectangleTests) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.001s

OK (expected failures=1)

We have three possible outcomes that summarise the result of running the tests:

  1. OK means all tests passed successfully
  2. FAIL means some or all tests did not pass
  3. ERROR means a test raised an exception other than AssertionError

We can also use -m unittest along with -k to run tests for test methods matching a given name:

$ python3 -m unittest -v test_rectangle.py -k test_area
test_area (test_rectangle.RectangleTests) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Testing the bonus(es) for an exercise

Python Morsels exercises usually have one or more bonuses, which you can optionally work on after the base problem. Within Python Morsels test scripts, bonuses are decorated with @expectedFailure, which makes sure a failing test doesn't result in a failing exercise.

Once a complete solution for the exercise is ready we can test the bonus by commenting-out the @unittest.ExpectedFailure line.

If we comment out that line in our the tests for our Rectangle class:

    # @unittest.expectedFailure
    def test_equality(self):
        a = Rectangle()
        b = Rectangle(1, 1)
        self.assertEqual(a, b)
        self.assertFalse(a != b)

We'll see a failure if we run our tests again (because we're not passing the bonus yet):

$ python3 test_rectangle.py
test_area (__main__.RectangleTests) ... ok
test_default_values (__main__.RectangleTests) ... ok
test_equality (__main__.RectangleTests) ... FAIL
test_length_width (__main__.RectangleTests) ... ok
test_perimeter (__main__.RectangleTests) ... ok
test_string_representation (__main__.RectangleTests) ... ok

======================================================================
FAIL: test_equality (__main__.RectangleTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/trey/python_morsels/rectangle/test_rectangle.py", line 38, in test_equality
    self.assertEqual(a, b)
AssertionError: Rectangle(1, 1) != Rectangle(1, 1)

----------------------------------------------------------------------
Ran 6 tests in 0.001s

FAILED (failures=1)

If we update our Rectangle class (in our rectangle.py module) to implement equality checks:

class Rectangle:

    """The rectangle class"""

    def __init__(self, length=1, width=1) -> None:
        self.length = length
        self.width = width
        self.perimeter = 2 * (self.length + self.width)
        self.area = self.length * self.width

    def __repr__(self) -> str:
        return f"Rectangle({self.length}, {self.width})"

    def __eq__(self, other):
        if isinstance(other, Rectangle):
            return (self.length, self.width) == (other.length, other.width)
        return NotImplemented

when we run the tests again we'll see that they all pass:

$ python3 test_rectangle.py
test_area (__main__.RectangleTests) ... ok
test_default_values (__main__.RectangleTests) ... ok
test_equality (__main__.RectangleTests) ... ok
test_length_width (__main__.RectangleTests) ... ok
test_perimeter (__main__.RectangleTests) ... ok
test_string_representation (__main__.RectangleTests) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.001s

OK

In the output instead of expected failure we will see OK this time.

Have questions?

Still have questions on local testing?

Check the help page on local testing.

If you don't see your question answered on that page, click the "Contact Us" button at the bottom of the page.

A Python Tip Every Week

Need to fill-in gaps in your Python skills? I send weekly emails designed to do just that.