Question

I have spent two full days now trying to understand the difference between unit testing and handling exception, but I can't get it.

Things I have understood (or I think I have):

  • Unit testing tests functions and classes
  • can be done with the unittest module in Python
  • exceptions are non-syntax errors
  • exceptions can be handled with try/except statements inside the main .py file
  • exceptions come in different types
  • never raise a general exception, always specify the type

Things I don't get:

  • when and what is worth to unit test
  • if I can use - and if it's good or bad practice - try/except statements in unit testing

  • if it's true that both aim to achieve the exact same goal, and namely to handle exceptions in a more 'readable' manner

  • if they are two different things

  • if they have to be used together

For example, I have a Game class with a turn attribute. What I want to do is to test that the turn value is a positive number, even though I know it will never be negative, I just want to use this to practice with testing.

In the real world, should I just use a try/except statement in the main code, or should I unit test it anyway.

I have a main file with this code:

import pygame

# initialize
pygame.init()

# set the window
window_size = (800, 800)
game_window = pygame.display.set_mode(window_size)
pygame.display.set_caption('My Canvas')

# define colours
colours = {
    'black': (0, 0, 0),
    'white': (255, 255, 255),
    'gold': (153, 153, 0),
    'green': (0, 180, 0)
}


class Game:
    def __init__(self):
        self.turn = 0
        self.player = None
        self.winner = None

    # getter methods
    def get_turn(self):
        try:
            assert self.turn >= 0
        except ValueError:
            print('turn number must be positive')

    def get_player(self):
        return self.player

    def get_winner(self):
        return self.winner

    # setter methods

    def set_turn(self, turn):
        self.turn = int(turn)

    def set_player(self, player):
        self.player = player

    def set_winner(self, winner):
        self.winner = winner

and a test.py file open right next to it, so I can write test straight away, with this code:

import unittest
from canvas import Game


class TestPlayer1(unittest.TestCase):

    def setUp(self):

        # Game objects
        self.game_turn_0 = Game()
        self.game_turn_5 = Game()
        self.game_turn_negative = Game()

        # values
        self.game_turn_0.turn = 0
        self.game_turn_5.turn = 5
        self.game_turn_negative.turn = -2

    def test_get_turn(self):
        self.assertEqual(self.game_turn_0.get_turn(), 0)
        self.assertEqual(self.game_turn_5.get_turn(), 5)
        with self.assertRaises(ValueError):
            self.game_turn_negative.get_turn()


if __name__ == '__main__':
    unittest.main()

Can I have some clarifications, please?

Thank you

Was it helpful?

Solution

Let's look at your get_turn method:

def get_turn(self):
    try:
        assert self.turn >= 0
    except ValueError:
        print('turn number must be positive')

There are a couple problems here:

  • In most languages, assertions only execute in debug mode. If you run Python with the -O flag, your assertion will never execute!
  • A failed assertion will raise an AssertionError, not a ValueError
  • This should not have been allowed to be in an invalid state in the first place. If self.turn is negative, then the actual bug happened somewhere else.

Here's how I would rewrite it:

def set_turn(self, turn):
    if turn < 0:
        raise ValueError('Turn number must be nonnegative')
    self.turn = int(turn)

def get_turn(self):
    assert self.turn >= 0   # This should never fail. If it does, you made a programming mistake.
    return self.turn

And then in your unit tests, you could write calls to make sure they behave as expected.

OTHER TIPS

Exception handling and unit testing solve different problems:

  • Exception Handling: Allows a running program to recover from an exceptional condition (like expecting to be able to open a file but it is open in another program).
  • Unit Tests: Tests the behavior of small parts (units) of your program to make sure they behave the way you expect.

Exception handling is a run time concern, while unit tests are a build time concern.

The two can work together. For example, let's say you have a utility that attempts to move files from one place to another, and verify that they are bit for bit the same. In your utility, you need to scan for the available files, and then work on them one by one.

A number of things can cause the utility to not be able to copy a file:

  • The file is in use by another process
  • You do not have permissions to delete the file from its present location, or write it to the destination
  • If you have more than one utility running at once, the file could be gone by the time you get to it in your loop

Those are all exceptions. You have to decide whether any of those exceptions should cause your utility to stop processing. For example, the final exception is something we don't want to stop prematurely with, and it is probably OK to skip a file if another process has it. The permissions thing really prevents your utility from doing its job, so you'll need to stop if that's the case.

In your unit tests, you would set up a scenario where each of those conditions would be met, and then assert that the application behaves appropriately:

  • Unit test 1 sets up a source and destination directory where everything is set up for a successful copy. It works, no exception handling required!
  • Unit test 2 sets up a source and destination directory, and then puts a lock on the source file. It fails, so you need to add exception handling to your utility to handle the lock (i.e. skip the existing file and move on
  • Unit test 3-5 sets up source and destination directory and a source file, but changes the permissions (source, destination, both) and asserts that the utility stops as soon as it realizes it can't copy any files. If the exception handling you put in before isn't too broad, then this should work. If not, you have to change your application to make all the tests pass.
  • Unit test 6 would be a bit more difficult to instrument since you would need to know when the source directory scan is complete, but it would set up source and destination directories, source file and then delete the source file after the scan is complete but before the move operation was performed. If the application ends early it fails, so you need to add exception handling, or at least an exists check before the copy/verify/delete operation is complete.

As you can see they are not mutually exclusive concepts to improve your application. However, they serve very different purposes.


I will say using exception handling where a simple if statement is more appropriate, you should change your code. In the example you provided there was a snippet that looked like this:

    try:
        assert self.turn >= 0
    except ValueError:
        print('turn number must be positive')

That would be a misapplication of an assert, particularly because you are not letting the exception leave the method. That could would be much cleaner, and execute faster (especially in loops) like this:

    if selt.turn < 0:
        print('turn number must be positive')

That code provides the same effect in a more readable way, that doesn't have the overhead of exception handling. Exception handling is expensive.

Now, if you wanted that to be an exceptional case that stops the game because now the game is in an undetermined state, you should not catch the exception in that code, and let whoever is calling the code deal with it. That would simply look like this:

    assert(self.turn >= 0, 'turn number must be positive')
Licensed under: CC-BY-SA with attribution
scroll top