Skip to content

Compact way to combine tests that raise with tests that return values #10089

Closed
@phil20686

Description

@phil20686

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    type: proposalproposal for a new feature, often to gather opinions or design the API around the new feature

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions