2

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

  • 3
    You are comparing apples and oranges here. Why would you even get the idea they are remotely same? – Euphoric Oct 30 '19 at 13:03
  • 1
    I wonder if you're getting unit testing confused with assertions. Is there a way you could illustration your questions with some code to make sure we're on the same page? – user214290 Oct 30 '19 at 13:04
  • Because I thought they both handle errors, but by the sound of it they don't... – Mirko Oricci Oct 30 '19 at 13:05
  • 1
    I will post the code so everything should be easier – Mirko Oricci Oct 30 '19 at 13:05
  • Also, I have posted the test code on Stack Overflow and I managed to get a 'ValueError not raised' message in the test. A user said that I had to raise the exception so I went on the main code and tried to write a try/except statement but it doesn't seem to work – Mirko Oricci Oct 30 '19 at 13:16
3

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.

  • Ok, I got it now. I have managed to make the test successful but in the wrong way. As you pointed out, the error would arise from the getter. Thanks for this explanation otherwise I would have written all tests wrong even though successful. – Mirko Oricci Oct 30 '19 at 14:51
5

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')
  • Ok I got what you say, so I have set up a scenario with valid values, and it works, then I have create an exception (hopefully is done in the right manner) in the main code, I set up a scenario with the negative number, -2, but I get 'AssertionError: None != 0'. Where is that even coming from? What is the program trying to say? Where I have assumed that None should be 0? Could you please point me out where is the code error? – Mirko Oricci Oct 30 '19 at 13:54
  • Ok, now it's clear. Thanks – Mirko Oricci Oct 30 '19 at 14:55
  • 1
    I might have my head stuck in Visual Studio parlance too much, but what do you mean by "unit tests are a build time concern"? A "build" in Visual Studio is basically "compiling" (and I use the term "compiling" very loosely here). You're talking about "build time" as the process for creating a releasable artifact, which could include continuous integration, configuration and deployment, right? – Greg Burghardt Oct 30 '19 at 22:33
  • I too suffer from working with compiled languages a lot. What I'm getting at is that Unit Tests are not deployed with your code. They are there to check the correctness of the code you have written so far. In a continuous integration (CI) environment, the unit tests are run as part of the build. – Berin Loritsch Oct 30 '19 at 23:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.