Unit tests and assertions (was: [Exceptional C++ Style] Item 18 - Virtuality)
Kevlin Henney
kevlin at curbralan.com
Sun Jan 9 12:23:40 EST 2005
In message <018a01c4f64b$1e6fccc0$0300000a at pc>, Terje Slettebø
<tslettebo at broadpark.no> writes
>>From: "Kevlin Henney" <kevlin at curbralan.com>
>
>> In addition, introducing such a DbC framework is generally a waste of
>> effort. Although it is based on good intentions, it is mostly ritual
>> rather than time well invested. A set of unit tests will be a much
>> clearer guide to the contract and the interface. Wading through an NVI
>> version of a contract is more than a little tedious, especially when the
>> notion of contract enforcement is based on assertions: all that
>> infrastructure for something that isn't even compiled in.
>
>Unless I misunderstand you, aren't unit tests and asserts rather
>complementary to each other? The asserts checks
>preconditions/postconditions/invariants, while you may use unit tests to
>check that they indeed work, i.e. exercise the asserts.
When a contract is phrased so that it does not describe what happens
when the normal terms of a contract are violated, the assumed behaviour
is undefined, ie the program may terminate, an exception may be thrown,
the called function need never terminate, everything may continue as
normal, etc. A contract phrased like this could be checked (or ignored)
quite legitimately through 'assert' (as in the <cassert> macro).
However, there is nothing for a test case to assert in such a case.
Where a contract also publishes the specific quality of failure for
transgression of some or all of its normal parts, because such
behaviours are published in terms of definite measurable outcomes, they
may be tested for.
What example-based unit test cases allow you to do is present and check
the items specified in the contract associated with a measurable
outcome. In other words, specific normal outcomes in valid situations
and specified bad-news in unpleasant circumstances.
This covers one of the most significant blindspots activities in the use
of assertions in core code: asserting on the correctness of the return
values or outcome, ie the postcondition. Writing such assertions is
often too trivial to demonstrate anything, hence they are omitted
(consider the postcondition for a container's empty function: for
queries the postcondition can often be the definition), or they are
often non-trivial to write, hence they are omitted (consider the
postcondition of a sorting function). Example-based test cases are
specific and illustrative, and can deal with accumulation of state
changes -- predicate calculus has no concept of history.
When I was first introduced to design by contract <mumble> years ago, I
was taken by the whole concept, but the principle/principal weakness
appeared to be in writing assertions for postconditions. The notion of a
contract is one of the most powerful interface design tools available,
but we must be careful to separate that idea from what that means for
code -- DbC does not entail asserts. Postconditions define what an
outcome must satisfy, but in their generalised form they are not good
tests of an implementation.
Preconditions fall into two broad groups:
(1) Preconditions with no associated penalty terms for violation, ie
undefined behaviour. These cannot be tested for in unit test cases. They
test the correctness of the caller rather than the called code, so there
is nothing to test at this level.
(2) Preconditions associated with some form of well-defined penalty, eg
an exception. With the exception of a contract that leads to process
death, these can be tested for using unit tests.
In case (1) preconditions that lead to undefined behaviour can be
further subdivided into three groups:
(i) Preconditions for which it is not possible to write an assertion
within the production code, eg "the passed iterator must be valid".
(ii) Preconditions for which introducing an assertion is intrusive,
either in terms of the effort involved or in terms of its runtime
effect, eg "the passed data set must be normalised".
(iii) Preconditions for which assertions are trivial to write.
Naturally enough, people tend to target (iii), and there are certainly
many cases in this class. However, getting carried away with (iii) tends
to block recognition of (2), cases where it is the component's
responsibility to do -- and publish -- something more appropriate than
just the possibility of falling over.
Where case (iii) can become useful as written assertions is in the
context of integration testing. In such a situation, above the level of
the unit, then testing the caller under certain circumstances does make
sense and arises as a natural consequence of integration. However, this
does not automatically mean that introducing 'assert' in such cases is
appropriate.
Perhaps the form of assertion that comes off best is in support of
data-structure (eg class invariant) or control-flow invariants (eg loop
invariant). Often these are truly matters of internal structure that are
not appropriate for the outside world to see (eg this internal pointer
is never null) and is white-box knowledge that should not leak into
black-box tests. They also follow (i), (ii) and (iii) in terms of
applicability, but are things that can in principle be revealed by unit
tests and not just integration tests.
And then, of course, there are all the contracts that cannot be stated
fully and sensibly in terms of pre- and postconditions. For example, the
contract that binds the behaviour of Object.{e,E}quals in {Java, .NET},
or contracts in the context of re-entrant code, such as callbacks.
So, yes, unit testing is often complementary to assertions, but
sometimes they impinge on the same concerns. Programmers may consider
the wrong path for their specific concerns. Many who plaster 'assert'
over their code do so motivated by concerns that are better addressed
with unit tests and -- but to a (much) lesser degree -- vice versa.
Kevlin
--
____________________________________________________________
Kevlin Henney phone: +44 117 942 2990
mailto:kevlin at curbralan.com mobile: +44 7801 073 508
http://www.curbralan.com fax: +44 870 052 2289
Curbralan: Consultancy + Training + Development + Review
____________________________________________________________
More information about the Effective-cpp
mailing list