I am learning how to unit test with unittest in Python.
What I am trying to do is to raise a ValueError in the case a negative turn number, and display a message, such as 'turn cannot be negative'.
the code I have written so far is the following:
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 = -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):
print('value error!')
if __name__ == '__main__':
unittest.main()
but the result is not as expected, as you can see:
Testing started at 09:55 ...
C:\Users\oricc\PycharmProjects\practisingDrowing\venv\Scripts\python.exe "C:\Program Files\JetBrains\PyCharm Community Edition 2019.2.3\helpers\pycharm\_jb_unittest_runner.py" --target test_canvas.TestPlayer1.test_get_turn
Launching unittests with arguments python -m unittest test_canvas.TestPlayer1.test_get_turn in C:\Users\oricc\PycharmProjects\practisingDrowing
pygame 1.9.6
Hello from the pygame community. https://www.pygame.org/contribute.html
value error!
Failure
Traceback (most recent call last):
File "C:\Users\oricc\AppData\Local\Programs\Python\Python37-32\lib\unittest\case.py", line 59, in testPartExecutor
yield
File "C:\Users\oricc\AppData\Local\Programs\Python\Python37-32\lib\unittest\case.py", line 628, in run
testMethod()
File "C:\Users\oricc\PycharmProjects\practisingDrowing\test_canvas.py", line 23, in test_get_turn
print('value error!')
File "C:\Users\oricc\AppData\Local\Programs\Python\Python37-32\lib\unittest\case.py", line 203, in __exit__
self._raiseFailure("{} not raised".format(exc_name))
File "C:\Users\oricc\AppData\Local\Programs\Python\Python37-32\lib\unittest\case.py", line 135, in _raiseFailure
raise self.test_case.failureException(msg)
AssertionError: ValueError not raised
Assertion failed
Assertion failed
Ran 1 test in 0.034s
FAILED (failures=1)
Process finished with exit code 1
Assertion failed
Assertion failed
Assertion failed
Assertion failed
I have watched a few videos by now, read the documentation on the python website, and read a few posts. I just can get the grasp.
I mean, I can't get how to use it in my case.
Could anyone explain to me how does this work?
Thanks
You use the assertRaises context manager around an operation that you expect to raise an error. If you look at your test code, can you see a line that should raise an error? I can't.
You are close - you have the general structure. But what you need is
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()
I have only changed the last line. See how that is the operation that you are expecting to raise an exception?
Also, you have a bug in the setUp - you need to set self.game_turn_negative.turn = -2, not self.game_turn_negative = -2. Be sure to fix that too.
Now to check that the test is working, try running the test and see it pass, then change your code so that a negative turn value does not raise an exception, and run the test again. You should see that the test suite fails, with a complaint that a ValueError wasn't raise when it was expected.
with assertRaises is designed around the expectation that the exception will be raised within the with block. In this case the only code running within the with block is print('value error!') - which will never raise a ValueError. This then causes the assertion to fail as a ValueError was not raised.
To solve your problem you'll need to adjust your application so that when an invalid condition is detected, a ValueError is raised. You will then be able to catch the ValueError inside the assertRaises block. It's worth noting that there appears to be a problem with your assignment to self.game_turn_negative. For the game_turn_0 and game_turn_5 values you're assigning an integer value to the .turn attribute, rather than the top level variable. For game_turn_negative you're setting it to Game() and then later on setting it to -2 (rather than setting self.game_turn_negative.turn).
To fix your code, this may work:
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()
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 = -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):
print('value error!')
if __name__ == '__main__':
unittest.main()
Related
I am still learning unittest and therefore, am unable to tell if there's a something missing in my test case in test_iterators.py below. Can someone help me to understand why the ValueError failed to be raised within unittest? Here are the scripts:
iterators.py
"""
Simple class to count from zero to N
"""
class count_to(object):
def __init__(self, nber):
self.nber = nber
def __iter__(self):
return count_to_iter(self.nber)
class count_to_iter(object):
def __init__(self, nber):
self.stopat = nber
self.current_nber = 0
def __next__(self):
if self.stopat < 0:
raise ValueError
elif self.current_nber > self.stopat:
raise StopIteration
self.current_nber += 1
return self.current_nber - 1
if __name__ == '__main__':
for x in count_to(-1):
print(x)
tests/test_iterators.py
import unittest
import iterators
class TestBaseIterators(unittest.TestCase):
def setUp(self):
pass
# Can't get the negative test right yet. It returns None instead of raising a ValueError
# Calling iterators.py directly and execute main successfully raised a ValueError however
def test_negative(self):
with self.assertRaises(ValueError): iterators.count_to(-1)
if __name__ == '__main__':
unittest.main()
I have used a similar approach to test raised errors previously and it worked. However, for this particular test case, here's what I get from the test.
test_negative (test_iterators.TestBaseIterators) ... FAIL
NoneType: None
======================================================================
FAIL: test_negative (test_iterators.TestBaseIterators)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/kerwei/Git/Concepts/tests/test_iterators.py", line 19, in test_negative
with self.assertRaises(ValueError): iterators.count_to(-1)
AssertionError: ValueError not raised
----------------------------------------------------------------------
Ran 1 test in 0.004s
FAILED (failures=1)
If I were to call iterators directly from __main__, I can then successfully receive the ValueError.
(py36) Kers-MacBook-Air:Concepts kerwei$ python iterators.py
Traceback (most recent call last):
File "iterators.py", line 29, in <module>
for x in count_to(-1):
File "iterators.py", line 19, in __next__
raise ValueError
ValueError
count_to(-1) creates a new count_to instance, it does not iterate over it, but you placed the test on self.stop_at value and raise the ValueError in the count_to_iter.__next__ method, so you will obviously not get a ValueError until you iterate on the count_to instance.
The naive fix would be to force iteration, ie:
def test_negative(self):
with self.assertRaises(ValueError):
# passing the iterable to `list` will force iteration
list(iterators.count_to(-1))
But the root problem is actually more of a design issue: raising a ValueError at this point is far from optimal since it will only happen when actually consuming the iterable, so you will have to inspect the call stack up until you find where count_to has been passed a wrong value. A much better solution is to check the value and eventually raise directly at the point where count_to is instanciated so it breaks always and immediatly (instead of "eventually, when you try to use the iterator in some possibly remote part of the code):
class count_to(object):
def __init__(self, nber):
if nber < 0:
raise ValueError("count_to argument must be a positive integer")
self.nber = nber
def __iter__(self):
return count_to_iter(self.nber)
And then your current test code will work as intended.
I'm debugging Python with Visual Studio 2015 (Python Tools extension).
I'm very new to Python and strangely when i debug the bellow code, it runs to line 18, then line 19, then line 18 again where it exits unceremoniously. Doesn't hit my catch block, doesn't hit line 20 or 21. No error message. Anyone know what might be causing this?
The unit test is testing a common google api functionality found on Git Hub here.
My Code:
import unittest
import sys
import os
import exceptions
from _multiprocessing import flags
sys.path.insert(0, "C:\Users\Jamie.Marshall\Documents\Visual Studio 2015\Projects\GetDCMPlalyStationData\DCMPSDataQuery")
try:
from dfareporting_utils import get_arguments
except ImportError:
print(ImportError.message)
print('No Import')
try:
#Line18
class Test_dfareporting_utility_test(unittest.TestCase):
#Line19
def test_A(self):
flags = get_arguments(sys.argv, __doc__, parents=[argparser])
self.assertEqual(flags[0], "")
except Exception as ex:
print(ex.message)
print(ex.source)
if __name__ == '__main__':
unittest.main()
When a class is initialized, only the signature lines of the class, the code at the top level of the class and signature lines the methods inside are executed. The code in the methods is executed only when the methods are called. An example:
E class MyClass:
|
E my_class_attribute = 1
|
E def my_method(self, a):
N b = a + 1
N return b
E = excuted; N = not executed
Your try around the class will have no effect unless an error is raised on the class or the def line.
At the result your try around the class will have no effect unless an error is raised in one of that lines.
To catch an exception in a test put the try / except inside the method. But you should actually have a defined state in your test and the error should be expected or not present, so it might be the best to expect an error and test for it with with self.assertRaises(YourErrorType):
I'm rewriting a afl-fuzz (a C application) to Python. Since I don't have enough understanding of its inner workings, I would like to replicate its functionality as close as possible.
I'm trying to run a functional test of a routine that forks Python interpreter, runs execve and if it fails, reports failure to its caller by returning 42. The test runs nice outside of unittest, but fails when put into it:
#!/usr/bin/env python
import os
import sys
import unittest
def run_test():
x = os.fork()
if not x:
sys.exit(42)
waitpid_result, status = os.waitpid(x, os.WUNTRACED)
print(os.WEXITSTATUS(status))
class ForkFunctionalTest(unittest.TestCase):
def test_exercise_fork(self):
run_test()
if __name__ == '__main__':
print('Expecting "42" as output:')
run_test()
print('\nAnd here goes unexpected SystemExit error:')
unittest.main()
Here's how it fails:
Expecting "42" as output:
42
And here goes unexpected SystemExit error:
E
======================================================================
ERROR: test_exercise_fork (__main__.ForkFunctionalTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "afl-fuzz2.py", line 23, in test_exercise_fork
run_test()
File "afl-fuzz2.py", line 15, in run_test
sys.exit(42)
SystemExit: 42
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (errors=1)
1
.
----------------------------------------------------------------------
Ran 1 test in 0.014s
OK
Is there a way to make unittest work with this function without changing run_test? I tried os._exit instead of sys.exit(), but it made the program die in both processes.
It turns out that os._exit actually worked, but in my unit tests I needed to mock it out given that I mocked out os.fork. Silly mistake.
sys.exit() raises a SystemExit class exception, which, if not caught, quits the program. You can try to catch the exception:
def text_exercise_fork(self):
try:
run_test()
except SystemExit as e:
print(e.args[0])
I would like to use raise without printing the traceback on the screen. I know how to do that using try ..catch but doesn't find a way with raise.
Here is an example:
def my_function(self):
resp = self.resp
if resp.status_code == 404:
raise NoSuchElementError('GET'+self.url+'{}'.format(resp.status_code))
elif resp.status_code == 500:
raise ServerErrorError('GET'+self.url+'{}'.format(resp.status_code))
When executing this, if I have a 404, the traceback will print on the screen.
Traceback (most recent call last):
File "test.py", line 32, in <module>
print ins.my_function()
File "api.py", line 820, in my_function
raise NoSuchElementError('GET ' + self.url + ' {} '.format(resp.status_code))
This is an API wrapper and I don't want users to see the traceback but to see the API response codes and error messages instead.
Is there a way to do it ?
I ran into a similar problem where a parent class was using the exception value on raise to pass messages through but where I didn't want to dump the traceback. #lejlot gives a great solution using sys.excepthook but I needed to apply it with a more limited scope. Here's the modification:
import sys
from contextlib import contextmanager
#contextmanager
def except_handler(exc_handler):
"Sets a custom exception handler for the scope of a 'with' block."
sys.excepthook = exc_handler
yield
sys.excepthook = sys.__excepthook__
Then, to use it:
def my_exchandler(type, value, traceback):
print(': '.join([str(type.__name__), str(value)]))
with except_handler(my_exchandler):
raise Exception('Exceptional!')
# -> Exception: Exceptional!
That way, if an exception isn't raised in the block, default exception handling will resume for any subsequent exceptions:
with except_handler(my_exchandler):
pass
raise Exception('Ordinary...')
# -> Traceback (most recent call last):
# -> File "raise_and_suppress_traceback.py", line 22, in <module>
# -> raise Exception('Ordinary...')
# -> Exception: Ordinary...
The problem is not with raising anything, but with what python interpreter does, when your program terminates with an exception (and it simply prints the stack trace). What you should do if you want to avoid it, is to put try except block around everything that you want to "hide" the stack trace, like:
def main():
try:
actual_code()
except Exception as e:
print(e)
The other way around is to modify the exeption handler, sys.excepthook(type, value, traceback), to do your own logic, like
def my_exchandler(type, value, traceback):
print(value)
import sys
sys.excepthook = my_exchandler
you can even condition of exception type and do the particular logic iff it is your type of exception, and otherwise - backoff to the original one.
Modified #Alec answer:
#contextmanager
def disable_exception_traceback():
"""
All traceback information is suppressed and only the exception type and value are printed
"""
default_value = getattr(sys, "tracebacklimit", 1000) # `1000` is a Python's default value
sys.tracebacklimit = 0
yield
sys.tracebacklimit = default_value # revert changes
Usage:
with disable_exception_traceback():
raise AnyYourCustomException()
Use this if you only need to hide a traceback without modifying an exception message. Tested on Python 3.8
UPD: code improved by #DrJohnAStevenson comment
Catch the exception, log it and return something that indicates something went wrong to the consumer (sending a 200 back when a query failed will likely cause problems for your client).
try:
return do_something()
except NoSuchElementError as e:
logger.error(e)
return error_response()
The fake error_response() function could do anything form returning an empty response or an error message. You should still make use of proper HTTP status codes. It sounds like you should be returning a 404 in this instance.
You should handle exceptions gracefully but you shouldn't hide errors from clients completely. In the case of your NoSuchElementError exception it sounds like the client should be informed (the error might be on their end).
You can create a class that takes two values; Type and code for a custom Exception Message. Afterwards, you can just pass the class in a try/except statement.
class ExceptionHandler(Exception):
def __init__(self, exceptionType, code):
self.exceptionType = exceptionType
self.code = code
print(f"Error logged: {self.exceptionType}, Code: {self.code}")
try:
raise(ExceptionHandler(exceptionType=KeyboardInterrupt, code=101))
except Exception:
pass
I have my own exceptions and i want to test farther fields in the ex other then the message.
Reading this thread i tried the idea of using a context. I wrote this generic function
def test_runtime_error(test, exception_type, message, display_parameter, path, callable_obj, *args):
pdb.set_trace()
with test.assertRaises(exception_type) as cx:
callable_obj(*args)
ex = cx.exception
test.assertEqual(ex.message,message)
test.assertEqual(ex.display_parameter,display_parameter)
test.assertEqual(ex.path,path)
The path and display_parameter are my own specific fields. I'm not inventing the wheel here, i took most of it from the source.
I'm using it like that
class ExceptionsTest(unittest.TestCase):
def test_something(self):
data = {"name" : "A"}
obj = MyModel.objects.get(pk=1)
test_runtime_error(self,CustomException, 'message', 'A', [], obj.create, data)
The arguments are passed correctly into the callable_obj. the function raises the expected exception. but right after the execution of callable_obj the function breaks and the exception is not fetched. BTW, when i ran the same code in the test it self it worked fine.
Whats wrong here ?
The issue here appears to be this line:
pdb.set_trace()
If you leave it in, but don't have import pdb, the code below will fail with:
E
======================================================================
ERROR: testRaises (__main__.ExceptionTest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "./except.py", line 22, in testRaises
self.verifyComplexException(MyException, 'asdf', RaiseException, 'asdf')
File "./except.py", line 14, in verifyComplexException
pdb.set_trace()
NameError: global name 'pdb' is not defined
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (errors=1)
which matches your description. If you do add the import pdb line, it will drop into the debugger, which is a completely different behavior that cannot be confused for the exit with E or exit with F status, so it can't be that.
Here's a complete example based on this idea which works as intended (licensed under Apache 2.0; see my repo):
import unittest
class MyException(Exception):
def __init__(self, message):
self.message = message
def RaiseException(message):
raise MyException(message)
class ExceptionTest(unittest.TestCase):
def verifyComplexException(self, exception_class, message, callable, *args):
with self.assertRaises(exception_class) as cm:
callable(*args)
exception = cm.exception
self.assertEqual(exception.message, message)
def testRaises(self):
self.verifyComplexException(MyException, 'asdf', RaiseException, 'asdf')
if __name__ == '__main__':
unittest.main()