Skip to content

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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

lundybernard
Copy link

@lundybernard lundybernard commented Jul 12, 2025

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/

@henryiii
Copy link
Collaborator

henryiii commented Jul 13, 2025

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:

  • pytest is the most popular framework. unittest is mostly there so CPython can test itself, and has a few specific uses it's good for. It's particularly bad for scientific work.
  • Keeping the tests as easy to write as possible means more tests are written. unittest has a ton of boiler plate.
  • Fixtures, parametrization, etc. are all much better in pytest than in any xtest style framework.
  • Installing a dependency to run unit tests is fine. Developers run unit tests. Developers can install dependencies.

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:

  • Integration tests: runs the entire app. Smaller projects often put them in /tests, larger projects might place them in a separate repository. Uses pytest or a command line runner. Can be slow.
  • Unit tests: verifies the various parts work individually. Usually in /tests. Uses pytest. Should be reasonable to run often by the developer.
  • Smoke test. Should be in the package, and runnable from the package. Uses unittest. Most common in compiled packages. Should verify the build works, and any architecture specific behavior works. Should be very fast.

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? ...)

@henryiii
Copy link
Collaborator

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.

@henryiii
Copy link
Collaborator

Here's an example I added to boost-histogram: scikit-hep/boost-histogram#1022

@lundybernard
Copy link
Author

The term I use is "Diagnostic Tests",
The audience for diagnostic tests is people checking or troubleshooting live / production deployments who need to verify that an installation is correct.

FFR:
"Smoke tests" are a sub-set of test cases, that are run to quickly verify that key features of a system work.
We some times resort to creating a smoke test suite out of desperation, when the runtime of the full test suite becomes a burden.

@lundybernard lundybernard force-pushed the docs/testing branch 6 times, most recently from ae6f62e to e38e5c7 Compare August 1, 2025 18:28
@henryiii
Copy link
Collaborator

henryiii commented Aug 1, 2025

Let me know when it's ready for another review!

@lundybernard
Copy link
Author

Thank you for your patience @henryiii,
I have a proof-reader who is helping me with revisions to the structure, and grammar/punctuation.
We should be ready for a full review soon.

I have one question we could use help with.
In the current version, we have a lot of detailed info about how to write unit tests, which I think would fit better in a Topical Guide. Should I go ahead and split those sections out into a new topical guide on unit tests now?

@lundybernard lundybernard force-pushed the docs/testing branch 2 times, most recently from 4c19f10 to 6b4e093 Compare August 13, 2025 19:33
@lundybernard lundybernard force-pushed the docs/testing branch 9 times, most recently from 6b30db4 to 1e10a46 Compare August 21, 2025 21:54
@lundybernard
Copy link
Author

@henryiii this is ready for another review

@henryiii
Copy link
Collaborator

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?

@lundybernard
Copy link
Author

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.

Copy link
Collaborator

@henryiii henryiii left a 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!

```

```python
from src.lib import say_hello, dangerous_sideffects
Copy link
Collaborator

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. :)

namespace into an imported dependency's namespace, like so:

```python
def test_(mocker):
Copy link
Collaborator

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.

Copy link
Collaborator

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.

Consider the benefits of refactoring your imports like so:

```python
from numpy import sum as np_sum, Array as NpArray
Copy link
Collaborator

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. :)


We recommend using Pytest for running tests in your development environments. To
run unit tests in your source folder, from your package root, use
`pytest {path/to/source}`. To run tests from an installed package (outside of
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth mentioning that this can be set in pyproject.toml, as most test suites allow you to just run pytest without args. If you do, this can be just the unit tests, requiring a user to specify other tests explicitly. We actually ran into a problem with specifying both paths in cibuildwheel; don't remember exactly what it was, but it is better to just specify the unit tests folder as the default I think.


## Testing Edgecases

While writing unit tests, you may be tempted to test edgecases. You may have a
Copy link
Collaborator

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.

`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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

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
Copy link
Collaborator

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.

- 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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- 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,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants