Description
What's the problem this feature will solve?
Improved useability in simple cases
Describe the solution you'd like
See code sample and attached examples
Additional context
This is some code that I have written again and again and it removes a lot of boilerplate in writing tests. Seems like it would be a useful addition at source in pytest.
def assert_equal(value, expect, **kwargs):
"""
Test utility for assorted types, see pd.testing.assert_frame_equal and siblings for supported keywords.
Parameters
----------
value: the value
expect: the expectation
kwargs: passed through depending on the type.
Raises
------
AssertionError: if not equal
"""
if isinstance(value, pd.DataFrame):
assert_frame_equal(value, expect, **kwargs)
elif isinstance(value, pd.Series):
assert_series_equal(value, expect, **kwargs)
elif isinstance(value, pd.Index):
assert_index_equal(value, expect, **kwargs)
else:
assert value == expect
def _is_exception_type(expect):
return isinstance(expect, type) and issubclass(expect, Exception)
def _is_exception_instance(expect):
return isinstance(expect, Exception)
def assert_or_raise(func, expect, *args, **kwargs):
"""
Calls func(*args, **kwargs) and asserts that you get the expected error. If no error is specified,
returns the value of func(*args, **kwargs)
"""
if _is_exception_type(expect):
with pytest.raises(expect):
func(*args, **kwargs)
elif _is_exception_instance(expect):
with pytest.raises(type(expect), match=str(expect)):
func(*args, **kwargs)
else:
return func(*args, **kwargs)
def assert_call(func, expect, *args, test_kwargs: Optional[Dict] = None, **kwargs):
val = assert_or_raise(func, expect, *args, **kwargs)
if not (
_is_exception_type(expect) or _is_exception_instance(expect)
):
assert_equal(val, expect, **(test_kwargs or {}))
return val
And then you can use it like this:
@pytest.mark.parametrize(
"input_string, expect",
[
(
"21:00:05.5 [America/New_York]",
TimeOfDay(
datetime.time(21, 0, 5, microsecond=500000),
tz=pytz.timezone("America/New_York")
)
),
(
"21,00:05.5 [America/New_York]",
ValueError(
"Time string.*did not match.*"
)
)
]
)
def test_time_of_day_builder(input_string, expect):
assert_call(
TimeOfDay.from_str,
expect,
input_string
)
This code now covers a wide variety of easy cases and leads to extremely compact test cases. To summarise the behaviour of assert call is as follows:
- If expect is an exception instance, calls it "with raises" and uses the instance error message as the argument to match.
- If expect is an exception type, calls it with raises but with no argument to match
- If expect is anything else, compares equality.
Moreover the equality operator will check for pandas and numpy types, and delegates to their standard equality methods depending on type, allowing you to pass "test_kwargs" such as atol or rtol to the numeric data types.
Without writing functions like this, you end up either having to separate tests that cause errors from tests that do not, or you must put branching logic separately in each test. In my experience the construction above allows you to write much simpler cleaner tests where 90+% of your library unit tests will be a call to assert_call.