On Mon, Feb 20, 2012 at 11:28 AM, Barry Warsaw
On Feb 17, 2012, at 02:57 PM, Mark Janssen wrote: FWIW, I think doctests are fantastic and I use them all the time. There are IMO a couple of things to keep in mind:
- doctests are documentation first. Specifically, they are testable documentation. What better way to ensure that your documentation is accurate and up-to-date? (And no, I do not generally find skew between the code and the separate-file documentation.)
- I personally dislike docstring doctests, and much prefer separate reST documents. These have several advantages, such as the ability to inject names into doctests globals (use with care though), and the ability to set up the execution context for doctests (see below). The fact that it's so easy to turn these into documentation with Sphinx is a huge win.
Since so many people point this out, let me say that I completely agree that doctests are not a *replacement* for unittests, but they are a fantastic *complement* to unittests. When I TDD, I always start writing the (testable) documentation first, because if I cannot explain the component under test in clearly intelligible English, then I probably don't really understand what it is I'm trying to write.
My doctests usually describe mostly the good path through the API. Occasionally I'll describe error modes if I think those are important for understanding how to use the code. However, for all those fuzzy corner cases, weird behaviors, bug fixes, etc., unittests are much better suited because ensuring you've fixed these problems and don't regress in the future doesn't help the narrative very much.
I think is an example of (mal)adapting to an incomplete module, rather than fixing it. I think doctest can handle all the points you're making. See clarification pointers below...
1. Execution context determined by outer-scope doctest defintions.
Can you explain this one?
I gave an example in a prior message on this thread, dated Feb 17. I think it's clear there but let me know. Basically, the idea is that since the class def can also have a docstring, where better would setup and teardown code go to provide the execution context of the inner method docstrings? Now the question: is it useful or appropriate to put setup and teardown code in a classdef docstring? Well, I think this requires a committment on the behalf of the coder/documentor to concoct useful (didactic) example that could go there. For example, (as in the prior-referenced message) I imagine putting example of defining a variable of the classes type (">>> g = Graph({some complex, interesting initialization})"), which might return a (testable) value upon creation. Now this could, logically, be put in the classes __init__ method, but that doesn't make sense for defining an execution context, and *in addition*, that can be saved for those complex corner cases you mentioned earlier.
I usually put all this in an additional_tests() method, such as:
Yes, I do the same for my modules with doctests. A dummy function which can catch all the non-interesting tests. This, still superior, in my opinion, than unittest. It is easier syntactically, as well as for casual users of your code (It has no leaning curve like understanding unittest). This superiority to unittest, by the way, is only realized if the second suggestion (smart comparisons) is implemented into doctest.
2. Smart Comparisons that will detect output of a non-ordered type (dict/set), lift and recast it and do a real comparison.
I'm of mixed mind with these. Yes, you must be careful with ordering, but I find it less readable to just sort() some dictionary output for example. What I've found much more useful is to iterate over the sorted keys of a dictionary and print the key/values pairs.
Yes, but you see you're destroying the very intent and spirit of doctest. The point is to make literate documentation. If you adapt to it's incompleteness, you reduce the power of it.
Without #1, "literate testing" becomes awash with re-defining re-used variables which, generally, also detracts from exact purpose of the test -- this creates testdoc noise and the docs become less useful. Without #2, "readable docs" nicely co-aligning with "testable docs" tends towards divergence.
I've no doubt that doctests could be improved, but I actually find them quite usable as is, with just a little bit of glue code to get it all hooked up. As I say though, I'm biased against docstring doctests.
Well, hopefully, I've convinced you a little that the limitations in doctests over unittests are almost, if not entirely due, to the incompleteness of the module. If the two items I mentioned were implemented I think it would be far superior to unittest. (Corner cases, etc can all find a place, because every corner case should be documented somewhere anyway!!) Cheers!! mark santa fe, nm
On Mon, Feb 27, 2012 at 12:28:17PM -0700, Mark Janssen wrote:
On Mon, Feb 20, 2012 at 11:28 AM, Barry Warsaw
wrote: On Feb 17, 2012, at 02:57 PM, Mark Janssen wrote: FWIW, I think doctests are fantastic and I use them all the time. There are IMO a couple of things to keep in mind:
- doctests are documentation first. Specifically, they are testable documentation. What better way to ensure that your documentation is accurate and up-to-date? (And no, I do not generally find skew between the code and the separate-file documentation.)
I second what Barry says here. Doctests are for documentation, or at least, doctests in docstrings are for documentation, which means they should be simple and minimal, and only cover the most important parts of your function. Certainly they should only cover the interface, and never the implementation, so not used for regression testing or coverage of odd corner cases. I have no problem with extensive doctests if they are put in an external document. I do this myself. But when I call help(func), I want to learn how to use func, not see seven pages of tests that don't help me understand how to use the function.
My doctests usually describe mostly the good path through the API. Occasionally I'll describe error modes if I think those are important for understanding how to use the code. However, for all those fuzzy corner cases, weird behaviors, bug fixes, etc., unittests are much better suited because ensuring you've fixed these problems and don't regress in the future doesn't help the narrative very much.
And again, +1 with what Barry says here, which means I disagree with your response:
I think is an example of (mal)adapting to an incomplete module, rather than fixing it. I think doctest can handle all the points you're making. See clarification pointers below...
I don't accept this argument. Doctest is designed for including example code in documentation, and ensuring that the examples are correct. For that, it does a very good job. It makes a great hammer. Don't use it when you need a spanner. It's not that doctest can't handle regression tests, but that regression tests shouldn't be put inside the the function docstring. Why should people see a test case for some bug that occurred three versions back in the documentation? Put it in a separate test suite, either unit tests, or a literate programming doc using doctest. Don't pollute the docstring with tests that aren't useful documentation. When people read your docstring, you have to expect that they are reading it in isolation. They want to know "How do I use function spam?", and any example code should show them how to use function spam: >>> data = (23, 42, 'foo', 9) >>> collector = [5] >>> spam(data, collector) >>> collector [5, 28, 70, None, 79] That works as documentation first, and as a test second. This does not: >>> spam(data, collector) # data and collector defined elsewhere >>> collector [5, 28, 70, None, 79] The fact that each docstring sees a fresh execution context is a good thing, not a bug.
1. Execution context determined by outer-scope doctest defintions.
Can you explain this one?
I gave an example in a prior message on this thread, dated Feb 17. I think it's clear there but let me know.
Basically, the idea is that since the class def can also have a docstring, where better would setup and teardown code go to provide the execution context of the inner method docstrings?
Is that a trick question? I don't want docstrings to have automatic setup and teardown code at all, and if they did, I certainly don't want them to be in some other object's docstring (related or not).
Now the question: is it useful or appropriate to put setup and teardown code in a classdef docstring?
In my opinion, no, neither useful nor appropriate. It would be counter-productive, by reducing the value of documentation as documentation, while still being insufficiently powerful to replace unit tests. A big -1 on this. [...]
Well, hopefully, I've convinced you a little that the limitations in doctests over unittests are almost, if not entirely due, to the incompleteness of the module. If the two items I mentioned were implemented I think it would be far superior to unittest.
I already think that doctest is far superior to unittest, for testing executable examples in documentation. I don't think it is superior to unittest for unit testing, or regression testing. Nor is it inferior -- its just different.
(Corner cases, etc can all find a place, because every corner case should be documented somewhere anyway!!)
I think you have a different idea of "corner case" than I do. Corner cases, in my experience, refer to the implementation: does the function work correctly when the input is in the corner? Since this is testing the implementation, it shouldn't be in the documentation. The classic example is, does this list-function work when the list is empty? So I would expect that the unit tests for, say, the sorted() built-in will include a test case for sorted([]). (This applies regardless of whether you use unittest, doctest, nose, or some other testing framework.) But the documentation for sorted() don't need to explicitly state that sorting an empty list returns an empty list. That's a given from the accepted meaning of sorting -- if there's nothing to sort, you get nothing. Nor does it need to explicitly state that sorting a list with one item returns a list with one item. A single example of sorting a list of (say) four items is sufficient to document the purpose of sorted(), but it would be completely insufficient for unit testing purposes. -- Steven
Steven D'Aprano
On Mon, Feb 27, 2012 at 12:28:17PM -0700, Mark Janssen wrote:
Well, hopefully, I've convinced you a little that the limitations in doctests over unittests are almost, if not entirely due, to the incompleteness of the module.
I don't think ‘doctest’ is incomplete. It comprehensively covers the use case for which it is designed. The ‘unittest’ module is limited in usefulness at testing code examples in documentation. That doesn't make it incomplete, either.
If the two items I mentioned were implemented I think it would be far superior to unittest.
I already think that doctest is far superior to unittest, for testing executable examples in documentation. I don't think it is superior to unittest for unit testing, or regression testing. Nor is it inferior -- its just different.
+1 QotW. -- \ “The fact of your own existence is the most astonishing fact | `\ you'll ever have to confront. Don't dare ever see your life as | _o__) boring, monotonous, or joyless.” —Richard Dawkins, 2010-03-10 | Ben Finney
On 2/28/2012 2:59 AM, Steven D'Aprano wrote:
Corner cases, in my experience, refer to the implementation: does the function work correctly when the input is in the corner? Since this is testing the implementation, it shouldn't be in the documentation.
It depends on the function. fact(0) = 1 could go in a doc string. So, I think, could combo(0,0) = 1. But sometimes implementations introduce a special case that is an artifact of the implementation and definitely not belong in a doc string. An real example is approximating the normal integral (cumlative normal distribution) with different approximations for [0,a] and (a,infinity). Unit tests should test f(a) and f(a+epsilonl) and the difference (should be >= 0) to make sure the two approximations 'join' properly so as to be transparent to the user. If the join point changes, so does the special unit test. -- Terry Jan Reedy
participants (4)
-
Ben Finney
-
Mark Janssen
-
Steven D'Aprano
-
Terry Reedy