GAMR1520: Markup languages and scripting

GAMR1520

Markup languages and scripting

Python assignment and unit testing

Dr Graeme Stuart


A python coding assignment

This assignment will test your ability to understand, modify and write python code.

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.

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).

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.

Thanks for listening

Any questions?

We should have plenty of time for questions and answers.

Just ask, you are probably not the only one who wants to know.

Dr Graeme Stuart