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.


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