How to raise an error when testing a function in Python - python

I'm trying to do a very simple test where my function takes in one parameter, a path to a database, tries to connect to the database using pyodbc.connect(database path) and if the path is invalid raises an error. However, as my test is structured currently, I am unable to make the test pass (raise the error) when I pass the function being tested a bad path.
I've tried to pass the test using mocks and without mocks. I believe the correct way to go would be to use mocks, however I also could not get that to work (see code below). I've found questions that were similar to what issue I'm having, but I could not use the suggestions in those questions to get my test to work.
Function Being Tested
import pyodbc
# When function runs by itself (no test) pyodbc.InterfaceError is raised if database path is bad!
def connect_to_database(database):
try:
conn = pyodbc.connect(database)
return conn
except pyodbc.InterfaceError as err:
raise err
Tests
Without mocking
def test_invalid_path_to_database(self):
self.assertRaises(pyodbc.InterfaceError, connect_to_database, '')
With mocking
def setUp(self):
self.mock_conn= mock.patch('my_package.my_module.pyodbc.connect').start()
self.addCleanup(mock.patch.stopall)
def test_invalid_path_to_database(self):
self.mock_conn.side_effect = pyodbc.InterfaceError
self.assertRaises(pyodbc.InterfaceError, connect_to_database, '')
I expect when the passed database path to the function is not valid an error (in this case InterfaceError) should be raised and the test should pass. This is not happening as the test is structured.

Related

Catching Exception in a Property Function with Pytest

I have a pytest function as such:
def test_zork1_serial_number_error(zork1_unicode_error_serial):
"handles a serial code with a unicode error"
with pytest.raises(UnicodeDecodeError) as execinfo:
serial_code = zork1_unicode_error_serial.serial_code
assert serial_code == "XXXXXX"
The code that this hits is:
#property
def serial_code(self) -> str:
code_bytes = bytes(self.view[0x12:0x18])
try:
if code_bytes.count(b"\x00"):
print("111111111111")
return "XXXXXX"
return code_bytes.decode("ascii")
except UnicodeDecodeError:
print("222222222222")
return "XXXXXX"
The print statements were just there for me to validate that the appropriate path was being hit. When I run the test I get this:
zork1_unicode_error_serial = <zmachine.header.Header object at 0x10e320d60>
def test_zork1_serial_number_error(zork1_unicode_error_serial):
"handles a serial code with a unicode error"
with pytest.raises(UnicodeDecodeError) as execinfo:
serial_code = zork1_unicode_error_serial.serial_code
> assert serial_code == "XXXXXX"
E Failed: DID NOT RAISE <class 'UnicodeDecodeError'>
tests/header_test.py:42: Failed
------------------------------------------------------------------------------ Captured stdout setup ------------------------------------------------------------------------------
/Users/jnyman/AppDev/quendor/tests/../zcode/zork1-r15-sXXXXXX.z2
------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------
222222222222
Notice how the "222222222222" is captured in the standard output, thus the appropriate path is being hit and thus the exception is also clearly being generated. Yet Pytest is saying that this exception was not raised. (I have also tested this code manually as well to make sure the exception is being generated.)
I've also tried the path of instead "marking" the test as such, like this:
#pytest.mark.xfail(raises=UnicodeDecodeError)
def test_zork1_serial_number_error(zork1_unicode_error_serial):
...
And that passes. However, it also passes regardless of what exception I put in there. For example, if I do #pytest.mark.xfail(raises=IndexError) that also passes even though an IndexError is never raised.
I can't tell if this has something to do with the fact that what I'm testing is a property. Again, as can seen from the captured standard output, the appropriate code path is being executed and the exception is most definitely being raised. But perhaps the fact that my function is a property is causing an issue?
I have read this Python - test a property throws exception but that isn't using Pytest and it's unclear to me how to retrofit the thinking there. I also aware that perhaps throwing an exception in a property is not a good thing (referencing this: By design should a property getter ever throw an exception in python?) so maybe this test problem is pointing to a code smell. But I don't see an immediate to make this better without adding extra complication. And that still wouldn't explain why Pytest is not seeing the exception generated when it clearly is being generated.

Django test does not add coverage with AssertRaises

There two lines that are not being executed by django tests when they are called as self.assertRaises.
I am using: Python 3.6.9, Django 3, Coverage.
I have this class:
class AverageWeatherService:
subclasses = WeatherService.__subclasses__()
valid_services = {
subclass.service_key: subclass for subclass in subclasses
}
#classmethod
def _check_service(cls, one_service):
if one_service not in cls.valid_services:
logger.exception("Not valid service sent")
raise NotValidWeatherFormException("Not valid service sent")
And I have a local API that is up in my pc.
Then I wrote this test:
def test_integration_average_temp_services_error(self):
self.assertRaises
(
NotValidWeatherFormException,
AverageWeatherService()._check_service,
"MyFakeService",
)
And although the test is successful with assert raises properly used this test is not adding coverage but If I call this method in a wrong way like this one:
def test_integration_average_temp_services_error2(self):
self.assertRaises
(
NotValidWeatherFormException,
AverageWeatherService()._check_service("MyFakeService")
)
Then of course I get an error running the test because the exception is raised and not properly catched by assertRaises BUT It adds coverage. If I run this test wrongly I have my code 100% covered. If I use assertRaises as the first way these two lines are not being covered (According to coverage html).
logger.exception("Not valid service sent")
raise NotValidWeatherFormException("Not valid service sent")
Also If I execute the method as the first way, the logger exception is not shown in console and when I run tests as the second way I am able to visualize the logger.exception on the terminal.
Any ideas of what is going on?
Thanks in advance.
I could solve it.
This is the workaround:
def test_integration_average_temp_services_error(self):
with self.assertRaises(NotValidWeatherFormException):
AverageWeatherService()._check_service("MyFakeService")

xlwings: how to detect whether a python function is being called from VBA module?

Situation
The xlwings package provides a convenient way to call python functions from an excel VBA module. The xlwings documentation gives the following basic example:
Write the code below into a VBA module.
Sub HelloWorld()
RunPython ("import hello; hello.world()")
End Sub
This calls the following code in hello.py:
# hello.py
import numpy as np
import xlwings as xw
def world():
wb = xw.Book.caller()
wb.sheets[0].range('A1').value = 'Hello World!'
Trying to run the python function world() directly (instead of calling it from excel VBA) gives the following error message:
Exception: Book.caller() must not be called directly. Call through
Excel or set a mock caller first with Book.set_mock_caller().
Question
I would like to modify the world() function such that it raises a custom exception instead when being run directly. In order to achieve this I first need to determine programmatically whether the world() function is being run directly or being called from excel VBA (at least that's what I'd think). How could I do this?
You can catch the exception and then raise your own:
def world():
try:
wb = xw.Book.caller()
except Exception:
raise CustomException(custom_message)
wb.sheets[0].range('A1').value = 'Hello World!'
You are worried that Exception is too generic, and rightly so. But that's xlwings' fault, not yours. If it is raising a generic Exception that's all you are left with to catch. You could check the exception message to make sure that you are not catching the wrong exception, but that would be brittle. Error messages are usually undocumented and not to be regarded as public, stable API.
Alternatively you can fix the problem where the problem is, xlwings' source code, and make it do what looks to me like the right thing to do: raising a more specific exception.
class NotFromExcelError(Exception):
pass
And at the end of caller:
raise NotFromExcelError('Book.caller() must not be called directly. Call through Excel '
'or set a mock caller first with Book.set_mock_caller().')
I hope a pull request like this would be accepted because raising a bare Exception like it currently does looks really wrong.

PANDAS read_sql faults when running NOSE test - Could not parse rfc1738 URL from string

Have you seen this pandas sqlite error? ArgumentError("Could not parse rfc1738 URL from string '/')? It only happens in a NOSE test.
From the other questions on SO, the err seems to be related to sqlite. But I can't tell how since there are no others using a simple sqlite3 db like I am.
In my unit test, I call a class method that reads from an sqlite3 DB via pandas read_sql. It works perfectly in any python session as well as Jupyter notebook. But for some reason when I run the code through a nosetest, nose tells me there's an Argument Error. And I cannot reproduce it.
I've confirmed the DB is working properly. As I mentioned, the pd.read_sql works perfectly in any other setting.
In the method definition, I am doing the following read,
# get data
div_query = 'SELECT * FROM Dividends WHERE Symbol == "{stock}"'.format(stock = symbol)
div_history = pd.read_sql(div_query, con=dbcnx)
And in the NOSE test,
def test_shiftBeforeDividend():
# here is where the err occurs
result = filters.shiftBeforeDividend('DUK')
# result now equals ArgumentError("Could not parse rfc1738 URL from string '/'")
def setup():
try:
db = 'mydb.db'
test_class.connectToDB(db)
return test_class
except Exception as e:
return e
def teardown():
try:
filters.closeDBConnection(self.dbcnx[0])
except Exception as e:
return e
# startup
filters = setup()
Any ideas on how to eliminate the issue?
Turns out after some hair pulling that the error was a simple misplaced '/' in the db file path. Hence, sqlite could not connect to a DB and all calls to the DB resulted in an error. In all the help topics I read on similar subjects, it seems that this error always results from an improper DB reference (i.e., file path). So if you run into the issue, make sure your file path exists and is correct before trying operations on your DB.

Is there a way to abort a test if the settings library setup fails?

I have a library management_utils.py that's something like:
path = global_settings.get_rdio_base_path()
if path == "":
raise PathRequiredError("Path is required...")
def some_keyword():
# keyword requires path to be set to some valid value
In my test case file I have something like:
***Settings***
Library management_utils
***Test Cases***
Smoke Test
some keyword
...
Is it possible to abort running these test cases if the management_utils setup fails? Basically I'd like to abort execution of these test cases if PathRequiredError was raised in management_utils.py.
When I run the tests, I see the error being raised but execution continues on.
I saw in the Robot documentation you can set ROBOT_EXIT_ON_FAILURE = True in your error class but this doesn't seem to work for this case. Also ideally I'd be able to do something more granular so that it only aborts the test cases that require this Library, not all test execution.
Thank you!
The problem is that the exception is raised during library loading, since it is in the top level of module. ROBOT_EXIT_ON_FAILURE only effects if the failure comes from a keyword.
Instead, do this:
def get_path():
path = global_settings.get_rdio_base_path()
if path == "":
raise PathRequiredError("Path is required...")
def some_keyword():
path = get_path()
...
Now the exception is raised inside a keyword, and the test execution will be stopped.
As for the other point, there's no way to abort just some tests using ROBOT_EXIT_ON_FAILURE.

Categories

Resources