A python coding assignment
This assignment will test your ability to understand, modify and write python code.
- The assignment is worth 40% of the total module mark.
- The deadline is the end of next week (Friday January 17th at 13:00)
We have only one exercise this week, “An introduction to unit testing in python”. Following on from the last lecture, this will be crucial to understanding the assignment.
You will be expected to work on the assignment in your own time but there will be at least five hours of timetabled lab sessions allocated to working on this assignment.
Bring your questions to the lab sessions.
What do I need to do?
The assignment is presented as 40 python unit tests in three parts.
GAMR1520 python assignment
├── module1.py
├── module2.py
├── module3.py
├── test_all.py
├── test_module1.py
├── test_module2.py
└── test_module3.py
The files test_module1.py, test_module2.py and test_module3.py contain the tests. You must not modify the test code, an exact copy will be used to mark your work.
The file test_all.py can be used to run all the tests, to see your total score.
The files module1.py, module2.py and module3.py contain broken/incomplete implementations that you must fix and/or complete to pass the tests.
Marking scheme
80% of the assignment mark will be awarded for passing tests. Since there are 40 tests, this equates to 2% per test.
- module1.py is worth 10% (test_module1.py contains five tests)
- module2.py is worth 50% (test_module2.py contains twenty-five tests)
- module3.py is worth 20% (test_module3.py contains ten tests)
The remaining 20% of the mark will be awarded for good code style (PEP8 compliance), readability and efficiency in each of the three modules.
We are expecting well-structured python code. It doesn’t need to be perfect but please take care to tidy it up before submission.
You should aim to have completed a good portion of the assignment (e.g. 20 passing tests) by the end of tomorrow. The first 20 tests are much easier than the second half.
REMEMBER: The test code should not be modified, you only need to modify the modules under test (module1.py, module2.py and module3.py)
module1.py
This first set of problems is a very basic introduction to the process. module1.py contains a few simple variables, two functions and a class definition.
this = 'hard'
comfortable = 'wait'
it = 'different'
def my_function():
return 'nothing'
def my_function_with_an_argument(arg):
print(arg)
class MyPython:
level = 'not sure'
We will see that test_module1.py contains five simple tests, all of which fail.
You need to update module1.py to pass all the tests. This should take no more than about 15 minutes.
TestModule1.test1_this_should_be_easy
This is the first test.
import unittest
import module1
class TestModule1(unittest.TestCase):
def test1_this_should_be_easy(self):
self.assertEqual(module1.this, 'easy')
This is the relevant code and the output from running the test.
this = 'hard'
AssertionError: 'hard' != 'easy'
- hard
+ easy
TestModule1.test2_it_should_be_comfortable
The second test asserts that two variables should be equal.
import unittest
import module1
class TestModule1(unittest.TestCase):
def test2_it_should_be_comfortable(self):
self.assertEqual(module1.it, module1.comfortable)
This is the relevant code and the output from the failing test.
comfortable = 'wait'
it = 'different'
AssertionError: 'different' != 'wait'
- different
+ wait
TestModule1.test3_my_function_returns_hello_world
The third test is looking at the return value of my_function()
.
import unittest
import module1
class TestModule1(unittest.TestCase):
def test3_my_function_returns_hello_world(self):
result = module1.my_function()
self.assertEqual(result, 'hello world')
Here’s the function and the output from the failing test.
def my_function():
return 'nothing'
AssertionError: 'nothing' != 'hello world'
- nothing
+ hello world
test4_my_function_with_an_argument_returns_the_argument
The next test includes two assertions testing the function with two different inputs.
import unittest
import module1
class TestModule1(unittest.TestCase):
def test4_my_function_with_an_argument_returns_the_argument(self):
result_with_test = module1.my_function_with_an_argument('test')
self.assertEqual(result_with_test, 'test')
result_with_five = module1.my_function_with_an_argument(5)
self.assertEqual(result_with_five, 5)
Here’s the existing function. It fails the test with this output.
def my_function_with_an_argument(arg):
print(arg)
AssertionError: None != 'test'
TestModule1.test5_MyPython_level_is_good_enough
The final test in the first set is looking at an attribute of a class.
import unittest
import module1
class TestModule1(unittest.TestCase):
def test5_MyPython_level_is_good_enough(self):
self.assertEqual(module1.MyPython.level, 'good enough')
The code looks like this.
class MyPython:
level = 'not sure'
AssertionError: 'not sure' != 'good enough'
- not sure
+ good enough
The test fails with a familiar error.
module2.py
The second module contains stub implementations for five functions.
The functions have no arguments defined and no return values. Each has just an empty code block.
def function1():
pass
def function2():
pass
def function3():
pass
def function4():
pass
def function5():
pass
Completing these functions with the correct arguments, logic and return values is worth up to 50% of the assignment (20% of the entire module mark).
The TestFunction1
class
Here’s a couple of example tests for function1()
from the TestFunction1
class.
They assert that the function should convert "hello"
into "hellohellohello"
and it should also convert 1
into "111"
.
import unittest
import module2
class TestFunction1(unittest.TestCase):
def test_with_string(self):
result = module2.function1('hello')
self.assertEqual(result, 'hellohellohello')
def test_with_integer(self):
result = module2.function1(1)
self.assertEqual(result, '111')
Both tests fail with the same error.
TypeError: function1() takes 0 positional arguments but 1 was given
The TestFunction2
class
Here’s a couple of example tests for function2()
from the TestFunction2
class.
They assert that the function should convert the arguments 3
, 2
and 1
into 5
and it should also convert the arguments 1
, 2
and 3
into -1
.
import unittest
import module2
class TestFunction2(unittest.TestCase):
def test_with_321(self):
result = module2.function2(3, 2, 1)
self.assertEqual(result, 5)
def test_with_123(self):
result = module2.function2(1, 2, 3)
self.assertEqual(result, -1)
Both tests fail with the same error.
TypeError: function2() takes 0 positional arguments but 3 were given
The TestFunction3
class
Here’s a couple of example tests for function3()
from the TestFunction3
class.
They assert that the function should convert the arguments "hello world"
and 10
into "hello w..."
and it should also convert the arguments "hello world"
and 5
into "he..."
.
import unittest
import module2
class TestFunction3(unittest.TestCase):
def test_hello_world_10(self):
result = module2.function3('hello world', 10)
self.assertEqual(result, 'hello w...')
def test_hello_world_5(self):
result = module2.function3('hello world', 5)
self.assertEqual(result, 'he...')
Both tests fail with the same error.
TypeError: function3() takes 0 positional arguments but 2 were given
The TestFunction4
class
The TestFunction4
class provides tests for function4()
.
Here’s a couple of tests.
class TestFunction4(unittest.TestCase):
def test_hello_world(self):
"""Using the default tag type produces a <div>"""
result = module2.function4('hello world')
self.assertEqual(result, '<div>hello world</div>')
TypeError: function4() takes 0 positional arguments but 1 was given
Notice they show different errors this time.
class TestFunction4(unittest.TestCase):
def test_hello_world_p(self):
"""Specifying the tag type works"""
result = module2.function4('hello world', 'p')
self.assertEqual(result, '<p>hello world</p>')```
TypeError: function4() takes 0 positional arguments but 2 were given
The TestFunction5
class
The TestFunction5
class provides tests for function5()
.
Here’s a couple of tests.
class TestFunction5(unittest.TestCase):
def test_5_2(self):
result = module2.function5(5, 2)
self.assertEqual(result, "*****\n*****")
def test_2_5(self):
result = module2.function5(2, 5)
self.assertEqual(result, "**\n**\n**\n**\n**")
Hopefully you get the idea.
The code in function5()
needs to produce a formatted string.
In this case, the first argument is the number of columns and the second argument is the number of rows.
It seems that there should be an asterisk in each cell.
function5(5, 2) = ***** function5(2, 5) = **
***** **
**
**
**
module3.py - Grid
The final set of tests in test_module3.py define two classes Grid
and Game
.
class TestGrid(unittest.TestCase):
def test_empty_grid(self):
empty_grid = "\n".join([
" | | ",
"---+---+---",
" | | ",
"---+---+---",
" | | "
])
my_grid = Grid()
self.assertEqual(str(my_grid), empty_grid)
For example, the provided Grid
class lacks a __str__
method.
class Grid:
pass
module3.py - Game
The tests indicate that the Game
class should have a Grid
instance as an attribute.
class TestGame(unittest.TestCase):
def test_properties(self):
game = Game()
self.assertIsInstance(game.grid, Grid)
self.assertEqual(game.turn, 'X')
self.assertEqual(game.status, "X's turn")
self.assertFalse(game.game_over)
The provided Game
class also lacks the attributes turn
, status
and game_over
.
class Grid:
pass
These are just two of the tests defined in test_module3.py. If you manage to build a working game that passes all ten tests then well done, you deserve to achieve a high mark.
PEP8 - Basic code layout
20% of your mark will be awarded for good code style (PEP8 compliance).
- Limit all lines to a maximum of 79 characters.
- Indent with four spaces (not tabs!)
- Surround top-level function and class definitions with two blank lines.
- Method definitions inside a class are surrounded by a single blank line.
- Use blank lines in functions, sparingly, to indicate logical sections.
class MyThing:
def __init__(self, a, b):
self.something = [a, b]
def a_method(self):
return self.something[0] - self.something[-1] * 10
def __str__(self):
return f"{self.something[0]}: {self.something[-1]}"
code is read much more often than it is written - PEP8
PEP8 - imports
Imports go at the top of the file, after any module comments or docstrings, before constants.
"""
This is a module...
""" # docstring
import os # Standard library
import sys
from random import random, randint
from external_library import some_class, something_else # Third party
from mypkg import my_function # Local
MY_CONSTANT = 1 # constants
Wildcard imports (
from <module> import *
) should be avoided
PEP8 - Avoid extraneous whitespace in the following situations:
Immediately inside parentheses, brackets or braces:
spam(ham[1], {eggs: 2}) # Correct
spam( ham[ 1 ], { eggs: 2 } ) # Wrong
Between a trailing comma and a following close parenthesis:
foo = (0,) # Correct
bar = (0, ) # Wrong
Immediately before a comma, semicolon, or colon:
if x == 4: print(x, y) # Correct
if x == 4 : print(x , y) # Wrong
These examples are taken directly from PEP8
PEP8 - Avoid extraneous whitespace in the following situations:
Immediately before the open parenthesis that starts the argument list of a function call:
spam(1) # Correct
spam (1) # Wrong
Immediately before the open parenthesis that starts an indexing or slicing:
dct['key'] = lst[index] # Correct
dct ['key'] = lst [index] # Wrong
More than one space around an assignment (or other) operator to align it with another:
x = 1 # Correct
y = 2 # Correct
long_variable = 3 # Correct
x = 1 # Wrong
y = 2 # Wrong
PEP8 - Operators vs keyword arguments
Always surround these binary operators with a single space on either side: assignment (=
), augmented assignment (+=
, -=
etc.), comparisons (==
, <
, >
, !=
, <>
, <=
, >=
, in
, not in
, is
, is not
), Booleans (and
, or
, not
).
a = 1 # Correct
a=1 # Wrong
But…
Don’t use spaces around the =
sign when used to indicate a keyword argument, or when used to indicate a default value for an unannotated function parameter:
# Correct:
def complex(real, imag=0.0):
return magic(r=real, i=imag)
# Wrong:
def complex(real, imag = 0.0):
return magic(r = real, i = imag)
PEP8 - naming
Function names should be lowercase, with words separated by underscores as necessary. Sometimes called snake_case.
def a_long_function_name():
do_something()
Variable names follow the same convention as function names.
a_long_variable_name = "try to make sure the name makes sense when used in your code"
speed = distance / time
Class names should normally use the CamelCase convention.
class MyClass:
a_method(self):
do_something()
Conclusion
Hopefully you get the idea.
You are provided with the bare minimum code.
There are many tests and they all fail.
You should get familiar with running tests and interpreting the output.
You need to edit the code being tested to make it pass the tests.
You must not edit the test code, only the modules under test.
You should work on it one step (one test, one function, one class) at a time.
The first ones should be easy, the later ones are harder.
Tidy up your code before you submit.