Let's talk about testing Python Morsels exercise solutions locally.
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).
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})"
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.
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)
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:
OK
means all tests passed successfullyFAIL
means some or all tests did not passERROR
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
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.
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.
Need to fill-in gaps in your Python skills?
Sign up for my Python newsletter where I share one of my favorite Python tips every week.
Need to fill-in gaps in your Python skills? I send weekly emails designed to do just that.