AssertionError
We can add assertions into our code, though it is not something usually needed in simple code.
def something_complex(arg):
result = str(arg)
# lots of complex processing here
# Now we can actively confirm it's a string
# This is a message to the reader to clarify what just happened
assert isinstance(result, str), "Woah! This should be a string here."
# And we continue processing.
result = "-".join(arg.split())
An assert
statement will raise an AssertionError
if the condition fails.
But we should never rely on these assertion errors, if they are raised, it indicates a bug.
These are ignored by the compiler in optimised mode (python -o
).
Unit testing in python
The unittest
module allows us to write test code that makes assertions about our application code.
Create a tests.py
module with classes that inherit from the unittest.TestCase
class.
import unittest
class PlayerTestCase(unittest.TestCase):
pass
class EnemyTestCase(unittest.TestCase):
pass
End your module with a call to unittest.main()
.
if __name__ == '__main__':
unittest.main()
An abstract example
Tests are methods with names that must begin with the word 'test'
.
When the unittest.main()
function is called, all these methods will be executed.
import unittest
import the_thing_to_test
class MyTestCase(unittest.TestCase):
def test_one_specific_scenario(self):
self.assertTrue(the_thing_to_test.is_valid)
def test_a_different_scenario(self):
self.assertEqual(the_thing_to_test.times_two(3), 6)
if __name__ == '__main__':
unittest.main()
Within the methods we can call various kinds of assertions.
Assertions
Some common assertions.
self.assertEqual(first, second, msg=None)
self.assertNotEqual(first, second, msg=None)
self.assertTrue(expr, msg=None)
self.assertFalse(expr, msg=None)
self.assertListEqual(first, second, msg=None)
self.assertTupleEqual(first, second, msg=None)
self.assertSetEqual(first, second, msg=None)
self.assertDictEqual(first, second, msg=None)
self.assertAlmostEqual(first, second, places=7, msg=None, delta=None)
self.assertNotAlmostEqual(first, second, places=7, msg=None, delta=None)
More assertions
Some less common assertions.
self.assertGreater(first, second, msg=None)
self.assertGreaterEqual(first, second, msg=None)
self.assertLess(first, second, msg=None)
self.assertLessEqual(first, second, msg=None)
self.assertIs(first, second, msg=None)
self.assertIsNot(first, second, msg=None)
self.assertIsNone(expr, msg=None)
self.assertIsNotNone(expr, msg=None)
self.assertIsInstance(obj, cls, msg=None)
self.assertIsNotInstance(obj, cls, msg=None)
self.fail(msg=None)
The unittest
module
import unittest
from my_module import my_function
class TestMyFunction(unittest.TestCase):
def test_with_zero(self):
self.assertEqual(my_function(0), 0)
def test_with_two(self):
self.assertEqual(my_function(2), 6)
def test_with_a_billion(self):
self.assertEqual(my_function(1000000000), 3000000000)
def test_with_string(self):
with self.assertRaises(TypeError):
my_function('should fail with TypeError')
if __name__ == '__main__':
unittest.main()
A real(ish) example
Download the tests for the core aspects of py2048, a 2048 clone.
Look for what is being tested
It will probably be imported at the top of the page and then referenced within each test.
For example, this code is testing a module called core
(i.e. code in core.py).
This TestStackLeft
class is specifically looking at the core.stack_left()
function.
"Tests for the core 2048 functions"
import unittest
import core
class TestStackLeft(unittest.TestCase):
def test_empty(self):
"An empty row is unaffected by a move"
result = core.stack_left([None, None, None, None])
self.assertEqual(result, [None, None, None, None])
The TestStackLeft.test_empty()
function is testing the case where an empty row is passed into the core.stack_left
function. In this case, the function should have no effect.
Possible core.stack_left
implementation
We need to create a core
module, that’s just a core.py file.
Inside the file, we need to implement a stack_left()
function.
def stack_left(data):
"""Move the non-None items in one row to the left"""
return [None, None, None, None]
This function passes our test.
Since our tests are incomplete, we can get away with a stupid function that ignores the input and returns the same list every time.
Testing the core.stack_left
function under different circumstances.
def test_one_value(self):
"A single non-None tile should be moved to the left"
result = core.stack_left([None, None, 2, None])
self.assertEqual(result, [2, None, None, None])
def test_two_values(self):
"Two non-None tiles should be moved to the left and retain their order"
result = core.stack_left([None, 2, None, 4])
self.assertEqual(result, [2, 4, None, None])
def test_three_values(self):
"Three non-None tiles should be moved to the left and retain their order"
result = core.stack_left([None, 2, 4, 2])
self.assertEqual(result, [2, 4, 2, None])
def test_four_values(self):
"All non-None tiles should not move"
result = core.stack_left([4, 2, 4, 2])
self.assertEqual(result, [4, 2, 4, 2])
Possible core.stack_left
implementations
Pass all the tests.
def stack_left(row):
"""Move the non-None items in one row to the left"""
result = [None, None, None, None]
stack_index = 0
for tile in row:
if tile:
result[stack_index] = tile
stack_index += 1
return result
A more pythonic version which uses the sorted
builtin function with a custom key.
def stack_left(row):
"""move the non-None items in one row to the left"""
return sorted(row, key=lambda tile: tile is None)
Bonus lab exercises from IMAT1914
If you want to work through the process of building the game step-by-step.
There are also videos of me going through the process on panopto, but I can’t work out how to share them with you yet.
The working final game
Download the core.py library of helper functions. It includes stuff like this.
def stack_left(row):
"""move the non-None items in one row to the left"""
return sorted(row, key=lambda tile: tile is None)
def merge_left(stacked_row):
"""Merge similar non-None items to the left"""
for i in range(3):
if stacked_row[i] and stacked_row[i] == stacked_row[i+1]:
stacked_row[i] *= 2
stacked_row[i + 1] = None
return stacked_row
def row_left(row):
"""A full move involves stacking, merging and then stacking again"""
stacked = stack_left(row)
merged = merge_left(stacked)
return stack_left(merged)
A command line interface
The game.py module implements a simple text-based playable game.
SCORE: 0
. . . .
. . . .
. . . .
. 2 2 .
move (W=Up, A=Left, S=Down, D=Right, Q=Quit): a
SCORE: 4
. . 2 .
. . . .
. . . .
4 . . .
move (W=Up, A=Left, S=Down, D=Right, Q=Quit):
A tkinter
GUI
The gui.py module implements a fully playable tkinter
version with keyboard control.
Coming Soon
Next year we have some important things to do.
- A
unittest.TestCase
lab exercise - A python assignment (a series of
unittest.TestCase
classes) worth 40% - Lab time allocated to completing the python assignment
- An introduction to javascript and the HTML canvas
- A game-like javascript animation exercise
- A javascript assignment
- Lab time allocated to completing the javascript assignment
Take a break.
If you enjoyed tkinter, then try to build the 2048 game. The instructions should be clear enough. If you jump ahead to future materials, I’d focus on unit testing for the assignment.