Understanding with statement to catch ValueError in unittest class
Asked Answered
U

3

7

New to unittest and Python in general, came across example in a tutorial introduction to unit testing wherein a with statement is used to catch a ValueError.

The script being tested (invoice_calculator.py) is:

def divide_pay(amount, staff_hours):
    """
    Divide an invoice evenly amongst staff depending on how many hours they
    worked on a project
    """
    total_hours = 0
    for person in staff_hours:
        total_hours += staff_hours[person]

    if total_hours == 0:
        raise ValueError("No hours entered")

    per_hour = amount / total_hours

    staff_pay = {}
    for person in staff_hours:
        pay = staff_hours[person] * per_hour
        staff_pay[person] = pay

    return staff_pay

The unit test includes this function in order to catch an edge case wherein staff_hours = None :

import unittest
from invoice_calculator import divide_pay

class InvoiceCalculatorTests(unittest.TestCase):
    def test_equality(self):
        pay = divide_pay(300.0, {"Alice": 3.0, "Bob": 6.0, "Carol": 0.0})
        self.assertEqual(pay, {'Bob': 75.0, 'Alice': 75.0, 'Carol': 150.0})

    def test_zero_hours_total(self):
        with self.assertRaises(ValueError):
            pay = divide_pay(360.0, {"Alice": 0.0, "Bob": 0.0, "Carol": 0.0})

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

Regarding the use of the with statement in test_zero_hours_total(self), what is actually happening here in terms of how this statement works/is being executed?

Is the test_zero_hours_total() function basically working as follows (layman's description): the expected error should be ValueError (which we're doing by passing ValueError to the function assertRaises()) when 360.0, {"Alice": 0.0, "Bob": 0.0, "Carol": 0.0} (which would raise a ValueError in divide_pay()) is passed as arguments to the divide_pay() function?

Unreel answered 16/12, 2015 at 23:31 Comment(0)
E
17

I'm not 100% sure what your question is here ...

TestCase.assertRaises creates an object that can be used as a context manager (which is why it can be used with the with statement). When used this way:

with self.assertRaises(SomeExceptionClass):
    # code

The context manager's __exit__ method will check the exception information passed in. If it is missing, an AssertionError will be thrown causing the test to fail. If the exception is the wrong type (e.g. not an instance of SomeExceptionClass), an AssertionError will be thrown as well.

Enteron answered 16/12, 2015 at 23:37 Comment(2)
My question is: is my understanding of how the with statement in the unit test works basically correct?Unreel
@Malvin9000 -- I think so? Does your understanding match with the description I just wrote? ;-)Enteron
R
3

It sounds like you understand what the test is doing. You might find it useful to see how you could write the test if assertRaises did not exist.

def test_zero_hours_total(self):
    try:
        pay = divide_pay(360.0, {"Alice": 0.0, "Bob": 0.0, "Carol": 0.0})
    except ValueError:
        # The exception was raised as expected
        pass
    else:
        # If we get here, then the ValueError was not raised
        # raise an exception so that the test fails
        raise AssertionError("ValueError was not raised")

Note that you don't have to use assertRaises as a context manager. You can also pass it the exception, a callable, and arguments for that callable:

def test_zero_hours_total(self):
    self.assertRaises(ValueError, divide_pay, 360.0, {"Alice": 0.0, "Bob": 0.0, "Carol": 0.0})
Reposition answered 16/12, 2015 at 23:40 Comment(3)
And with the help of contextlib, this form of assertRaises becomes pretty easy to write.Enteron
@Enteron I've not used contextlib before, I'll have to give that a go :)Reposition
This would be a fun toy problem to get yourself used to it.Enteron
G
0

should it be: self.assertEqual(pay, {'Bob': 100.0, 'Alice': 200.0, 'Carol': 0.0})

Guarantor answered 28/6, 2023 at 1:59 Comment(2)
This does not provide an answer to the question. Once you have sufficient reputation you will be able to comment on any post; instead, provide answers that don't require clarification from the asker. - From ReviewKoreykorff
@rob: Can you explain why this isn’t an answer to the question? It looks like one to me. Am I missing something?Chaplin

© 2022 - 2025 — McMap. All rights reserved.