13

Assuming I have a function that takes a complex object and does something with it:

def foo(bar: SomeComplexObject):
    ...

In unit tests bar will be replaced by a mock object, but that of courses now raises type warnings. Should I simply ignore or suppress these or is there a proper way to deal with them (without changing the original function signature of course)?

Update: I've seen now that this is an open issue on mypy, but its been in that state for over two years. Has any consensus emerged on how to work around this?

gmolau
  • 2,815
  • 1
  • 22
  • 45
  • Can't you create mocks that are actual instances of the class they mock? – Ulrich Eckhardt May 16 '18 at 05:21
  • 1
    As far as I know, no. I can create them with the same signature as the real class via the `spec`/`autospec` parameter, but they will still be instances of `unittest.mock.Mock`. – gmolau May 16 '18 at 05:23
  • I must honestly say that I haven't used the type annotations yet, so I'm guessing. However, the `Mock` documentation says "If spec is an object (rather than a list of strings) then __class__ returns the class of the spec object. This allows mocks to pass isinstance() tests." -- isn't that what could do the job? – Ulrich Eckhardt May 16 '18 at 05:29
  • I think mypy doesn't rely on isinstance checks, it's more complicated than that. Doing a `reveal_type()` on the mock class shows that it is indeed interpreted as `unittest.mock.Mock`, not the actual class. – gmolau May 16 '18 at 05:45
  • 5
    Why do you run `mypy` over unit tests in first place? Tests are expected to violate many things, including typing. You can ignore the tests in `mypy.ini` by adding a section `[mypy-tests.*]` and `ignore = True` line. – hoefling May 17 '18 at 20:00
  • 5
    That doesn't look like a good approach to me. Tests are an integral part of the code base, there is no reason why they shouldn't be held to the same standard as everything else. Tests are meant to violate business logic in some narrowly defined circumstances, but not general language syntax. Typing in itself should also not be tested, it is either correct or it isn't. If it's not that is a syntax error that a type checker is meant to detect before any tests are run. I would consider it one of greatest benefits of mypy that it alleviates the need for typing-related tests. – gmolau May 18 '18 at 15:09
  • 1. What language syntax that you are talking about do mocks violate? 2. Tests are meant to violate typing, or else you'll be stuck with happy day testing: according to you, a test for function `def spam(x: int)` that validates whether passing a string or `None` fails or not does not fit into the domain. This is a dangerous path to enter. – hoefling May 20 '18 at 20:43
  • My point is that violating typing rules is equivalent to a syntax error and should thus be caught before testing, not through tests. Passing a string to `def spam(x: int)` is a violation of that functions contract, i.e. it is implied that such a test would fail (you could event call it an error if it doesn't because then the arguments type would be overly restrictive, but that might be debatable). Testing that the function does the right thing with it's int should of course happen, but that has nothing do to with typing. – gmolau May 21 '18 at 17:08

1 Answers1

7

I'm going to put my 2¢ in and say that you should type-check your testsuite. Its still code and static type checking will help you write better code faster.

That leaves the question of how. Ideally, if your function expects SomeComplexObject then you also pass in an instance thereof. Either by building one in your test fixtures, or by subclassing and overriding what you don't need. The best unit test is the one that operates on proper input.

That still leaves the case where this is impractical or we explicitly want to test how invalid input is handled. In that case just explicitly cast your mock to the type that mypy requires:

from typing import cast

def test_foo():
    mock_bar = cast(SomeComplexObject, MockBar())
    foo(mock_bar)
vbraun
  • 1,851
  • 17
  • 14
  • 2
    Your answer helped me a lot for an equivalent problem. I wanted to check a call of a patched mock inside my function-under-test. mypy complained about the function-under-test's return value not having an attribute `assert_called_once`. I could resolve my issue by reverting your answer and putting `return_value = cast(Mock, return_value)` before `return_value.inside_call.assert_called_once()`. Thank you! – claudio Oct 21 '20 at 20:16