[Python-ideas] Simplicity of C (was why is design-by-contracts not widely)

Steven D'Aprano steve at pearwood.info
Sun Sep 30 04:02:49 EDT 2018


On Sun, Sep 30, 2018 at 02:50:28PM +1000, Chris Angelico wrote:

> And yet all the examples I've seen have just been poor substitutes for
> unit tests. Can we get some examples that actually do a better job of
> selling contracts?

In no particular order...

(1) Distance 

Unit tests are far away from the code you are looking at. Things which 
go together ought to be together, but typically unit tests are not just 
seperate from the thing they are testing, but in a completely different 
file.

Contracts are right there, next to the thing they belong with, but 
without being mixed into the implementation of the method or function.


(2) Self-documenting code

Contracts are self-documenting code. Unit tests are not. Unit tests are 
full of boilerplate, creating instances, setting up test data, checking 
the result. Contracts simply cut to the chase and state the 
requirements and the promises made:

    the input must be a non-empty list
    the result will be a string starting with "Aardvark"

as executable code.


(3) The "Have you performed the *right* tests?" problem

Unit tests are great, but they have a serious problem: they test ONLY 
the canned data you put in your test. For non-trivial functions, unit 
tests tell you nothing about the general behaviour of your function, 
only the specific behaviour with the given input:

    The key problem with testing is that a test (of any kind) 
    that uses one particular set of inputs tells you nothing 
    at all about the behaviour of the system or component when 
    it is given a different set of inputs.

http://thinkrelevance.com/blog/2013/11/26/better-than-unit-tests

Contracts are one (partial) solution to this problem.

If you have a unit test that does this:

    def test_spam(self):
        self.AssertEqual(spam(2).count("eggs"), 2)

then it tests ONLY that spam(2) contains "eggs" twice, and that's it. It 
tells you nothing about whether spam(3) or spam(1028374527601) is 
correct. In fact, under Test Driven Development, it would be normal to 
write the test first, and then implement spam as a stub that does this:

    def spam(n):
        return "eggs eggs"

proving my point that a passing test with one input doesn't mean the 
function is correct for another input.

In contrast, the post-condition:

    def spam(n):
        ensure:
            result.count("eggs") == max(0, n)
        # implementation


is tested on every invocation of spam (up to the point that you decide 
to disable post-condition checks). There is no need for separate tests 
for spam(2) and spam(3) and spam(1028374527601) unless you have some 
specific need for them. (Say, a regression test after fixing a 
particular bug.)


(4) Inheritance

Contracts are inherited, unit tests are not.


(5) Unit tests and contracts are complementary, not alternatives

Unit tests and contracts do overlap, and in the areas of overlap 
contracts are generally superior. But sometimes it is too hard to 
specify a contract in sufficient detail, and so unit tests are more 
appropriate.

And for some especially simple functions, you can't specify the 
post-condition except by duplicating the implementation:

    def max(a, b):
        """Return the maximum of a and b."""
        ensure:
            result == a if a >= b else b
        implementation:
            return a if a >= b else b

In that case, a post-condition is a waste of time, and one should just 
unit test it.

https://sebnozzi.github.io/362/contracts-replace-unit-tests/

Another way to think about it is that unit tests and contracts have 
different purposes. Pre-conditions and class invariants are a form of 
defensive programming that ensures that your prerequisites are met, that 
code is called with the correct parameters, etc. Unit tests are a way of 
doing spot tests that the code works with certain specified inputs.


(6) Separation of concerns: function algorithm versus error checking

Functions ought to validate their input, but doing so obfuscates the 
function implementation. Making that input validation a pre-condition 
separates the error checking and input validation from the algorithm 
proper, which helps make the code self-documenting.


(7) You can't unit test loop invariants

Some languages have support for testing loop invariants. You can't unit 
test loop invariants at all.

https://en.wikipedia.org/wiki/Loop_invariant#Programming_language_support


(8) Executable documentation

Contracts are executable code that document what input is valid and what 
result is returned. Executable code is preferrable to dead comments 
because comments rot:

"At Resolver we've found it useful to short-circuit any doubt and just 
refer to comments in code as 'lies'. "
-- Michael Foord paraphrases Christian Muirhead on python-dev, 2009-03-22

Contracts can also be extracted by static tools.




-- 
Steve


More information about the Python-ideas mailing list