LibraryHippo 2020 - Unit Tests
Now that I've completed all my spikes and decided to move forward, I'd like to add a little more rigour to the project. Original LibraryHippo had a comprehensive suite of unit tests and I'll port them over (and perhaps augment them with integration tests). Today I'll add the first unit test to the project, then port over a bunch more when you're not looking!
Prerequisites
The first thing we need is a test-running package. My favourite is pytest.
❯ pip install pytest ❯ inv freeze
Now I need a place to put the tests. I'll broadly be following the structure
from Patrick Kennedy's
Testing a Flask Application using pytest,
although most of it won't be needed today, so I'll just make the tests\unit
structure, including empty __init__.py
files so my imports work.
❯ 'tests', 'tests\unit' | ForEach-Object { New-Item -Type File $_\__init__.py -Force }
First test
Next, I write a simple unit test for the WPL
class, to make sure it correctly reads the items I have on hold.
Run it using pytest
, and success!
============================= test session starts ============================= platform win32 -- Python 3.8.1, pytest-5.4.2, py-1.8.1, pluggy-0.13.1 rootdir: D:\Sandbox\LibraryHippo collected 1 item tests\unit\test_wpl.py . [100%] ============================== 1 passed in 2.20s ==============================
Isolate tests from the library website
That test shows that the code is doing something, but it's dependent on responses from the Waterloo Public Library website. If I don't have any holds, or the site is down, my test will fail. I'll use requests-mock to fake out interactions with the library system.
❯ pip install requests-mock ❯ inv freeze
I'd never used requests-mock before, and it was incredibly easy. It provides a
pytest fixture on which I can set expectations for the requests module. Within
minutes, I'd used the mock_requests
fixture to configure fake results for
- getting the login page
- posting the login page
- getting the holds page
- getting the checkouts page
This passes just as in its previous iteration, even after I made the test more specific. Since I control the "response from the library", I can expect a particular held item to be present. In the future, this will allow me to easily verify that holds with different statuses, such as "in transit" or "missing" are reported properly.
A note on mocking styles
As a maintainer of the third most popular and first best .NET mocking framework, I have opinions on mocking practices. For one, I generally advise against monkey patching or anything else that seems like magic. I've worked in environments where these effects were abused, and tests became very difficult to debug.
The new test relies on a magically-provided requests_mock
object, and
actions on that object affect the functioning of the requests
module. This
gave me pause. Ultimately, I decided to go with it, for two reasons. First, the
pytest fixtures have
well-known behaviour and should undo the requests_mock
's changes after every
test function. Second, the actual monkey patching is too convenient to not try.
I toyed with the idea of adding a fixture that created a new session, had
requests-mock intercept only that, and then pass both those objects to each
test, and it just seemed like too much work for the benefit. Hopefully, as sole
maintainer on LibraryHippo, I'll be able to keep a handle on the magic mocking.
If not, I can always fall back to a more explicit style.