-
Notifications
You must be signed in to change notification settings - Fork 65
docs: Add outline of unit testing recommendations #619
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Over, looks good, just one overall issue that can be solved mostly by moving stuff around. I do not think we should push anyone toward unittest over pytest for unit tests. The reasons:
That last point is important: unit tests are for developers to develop the code. End users do not need to run unit tests. Once the units work correctly, then you don't need to verify that again. However, I think we should instead emphasize a new category of tests, the one we mentioned (I don't remember your term, "smoke test" is what I thought of). That's a separate category from unit tests, and for that one you don't want dependencies; it's a great place to use unittest. So:
So I'd basically recommend moving the unittest parts to a new section about smoke tests; the actual content is fine, just I think it needs reordering. (And replace "smoke" with whatever you mentioned today, I've just forgotten what it was. Validity? Verification? ...) |
|
The only other thing I'd add is maybe a mention somewhere that this is guidelines for good design, but even poorly structured tests are better than no tests. I don't want to discourage someone from writing tests, but instead give them ways to improve their test design if they want to, and help as users scale up to larger projects. |
|
Here's an example I added to boost-histogram: scikit-hep/boost-histogram#1022 |
|
The term I use is "Diagnostic Tests", FFR: |
f932498 to
8af993d
Compare
ae6f62e to
e38e5c7
Compare
|
Let me know when it's ready for another review! |
4999dcf to
b9158de
Compare
|
Thank you for your patience @henryiii, I have one question we could use help with. |
4c19f10 to
6b4e093
Compare
6b30db4 to
1e10a46
Compare
|
@henryiii this is ready for another review |
1e10a46 to
5716e45
Compare
|
For now, keeping it on one page makes sense, as there's a discussion on restructuring going on anyway. I'm about to leave for a week-long vacation, so might not be able to get to this. I've got a couple of sections I'd like to work on a bit, and I'd like to compare this with the current pytest page to see if I can reduce overlap or cross-link. One solution could be to merge in more-or-less the current state (I could do a quick review), then I could make PRs to update it later. Or we can wait till I get back - do you have a preference? |
|
This went through several rounds of proof-reading and revision, so I'm happy to merge it, and continue working on it in the future through new Issues/PRs. I don't mind if you prefer to wait and do a more thorough final review when you get back. Whatever works best for you. |
5716e45 to
34d10c3
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a few comments; some of it is fine for followup. Just the key stuff like renaming the src. import matters. Take it out of draft when you are ready!
docs/pages/principles/testing.md
Outdated
| ``` | ||
|
|
||
| ```python | ||
| from src.lib import say_hello, dangerous_sideffects |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please don't name anything src. That's a folder name we use specially intending it to never show up in imports. It's not good to make people think it's okay to write "src" in their import statements. Select some other name. :)
docs/pages/principles/testing.md
Outdated
| namespace into an imported dependency's namespace, like so: | ||
|
|
||
| ```python | ||
| def test_(mocker): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can do it in a followup, but we should use monkeypatch where possible. Mocking should be used if you have to make a thing to monkeypatch in, but the patching process is much better with monkeypatch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I like keeping test_func or some similar name, I've seen people actually write def test(...) tests, which is really annoying, as it's not usually clear what it's testing, and it's hard to select with -k.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed test_ to test_func in those examples.
I think there is a lot of ground to cover regarding patching with mocks and static values, probably worth its own topical guide.
I'm not very familiar with the MonkeyPatch plugin, but I'm happy to learn more and incorporate it into these docs.
Personally, I prefer to patch out dependencies using autospec Mocks, so that the tests fail if the interface of the dependency changes. I'd like to work out the details in another issue or on the scientific-python discord.
| Consider the benefits of refactoring your imports like so: | ||
|
|
||
| ```python | ||
| from numpy import sum as np_sum, Array as NpArray |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should not be done except very special circumstances. Normally, you should monkeypatch the import and not use import ... from. There are some circumstances where you need to avoid this sort of import, like to avoid circular import issues. You can't import a unittest TestCase this way or it triggers the runner, in fact! Some projects strongly recommend avoiding from imports. There are special cases where you need this, for example if you need to patch in something but you it must only affect the local code, so it's good as a tip, but not the general recommendation for design. Especially renaming imports like this. :) Happy to address in a followup, though.
Also full imports are completely unambiguous. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I'd love to follow up on best practices for imports.
The main reason I prefer this style is that it makes patching, drop-in replacements, and refactoring a little easier. I agree that this may be an advanced-use tip, more appropriate for very polished code than code in the early stages of development.
side-note:
I checked importing a TestCase, because that sounds really weird... You can import a TestCase without executing it, unless you're using a test runner; python test.py does not execute an imported TestCase, but both pytest test.py and python -m unittest test.py do!
docs/pages/principles/testing.md
Outdated
|
|
||
| ## Testing Edgecases | ||
|
|
||
| While writing unit tests, you may be tempted to test edgecases. You may have a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd clarify that this is about long, trick things to test. Generally, you should always test edge cases like minimum, maximum, None, etc. I think the problem is the name - "edge cases" I think implies the wrong thing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this needs a little more elaboration.
I think excessive edge case testing in unit tests is bad, we do want to test a reasonable subset of expected, and supported, valid inputs. This may be a good place to mention the TDD rule about not testing against invalid input until it actually becomes a problem. That said, I would not complain if someone showed me a huge test suite for a critical function that still runs in less than a second.
I'll work on the wording in this section.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I rewrote this section, and changed the title to "Extensive Input Testing".
The rewrite emphasizes readability, and recommend organizing edge case and extensive input testing in a way that preserves it.
docs/pages/principles/testing.md
Outdated
| `import numpy as np`. However, as we develop our unit tests, this can cause | ||
| difficulty with mocking, and complicate refactoring. | ||
|
|
||
| To patch out numpy.sum in your test, you either need to patch the _global_ numpy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| To patch out numpy.sum in your test, you either need to patch the _global_ numpy | |
| To patch out `numpy.sum` in your test, you either need to patch the _global_ numpy |
docs/pages/principles/testing.md
Outdated
| difficulty with mocking, and complicate refactoring. | ||
|
|
||
| To patch out numpy.sum in your test, you either need to patch the _global_ numpy | ||
| module, which can have unintended side-effects, or specifically patch numpy.sum |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI, monkey patching is isolated to just the function (and can even be reduced to just a line) so it normally works fine. It's only in special cases where something gets called multiple times and you only want to change your usage, which is pretty rare.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs some clarification. Yes, the lifetime of the patch is limited to the lifetime of the function, but the patch can target the namespace of the file-under-test, or the namespace of the dependency.
I can provide some examples to illustrate, but they are a bit long. This detail probably bares some detailed explanation, along with a discussion on when to use a Mock, vs when to substitute in a fake.
docs/pages/principles/testing.md
Outdated
| - Test files should be named `test_{file under test}.py`, so that test runners | ||
| can find them easily. | ||
|
|
||
| - test\_.py files should match your source files (file-under-test) one-to-one, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| - test\_.py files should match your source files (file-under-test) one-to-one, | |
| - `test_*.py` files should match your source files (file-under-test) one-to-one, |
7fa2ccd to
f12b380
Compare
Co-authored-by: Lauren Moore <[email protected]>
1d9fcff to
6a27607
Compare
61b996e to
c66f86f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just had a quick last look, I think it's good! We can make PRs for followups as needed. Thanks for all the work on this!
This PR contains an outline of a proposed Development Guide / Principles / Unit Testing Recommendations section.
I am opening this PR now to allow discussion of the outline, and key-points, before fleshing out the complete page.
📚 Documentation preview 📚: https://scientific-python-cookie--619.org.readthedocs.build/