A functions/methods contract is a very real thing. I like to use sqrt(x)
as an example:
- x must be real
- x must not be null
- x >= 0
- return value not null
- return value >= 0
Invoking a function not compliant with the contract or using it's return value in a way the contract does not guarantee leads to errors and bugs, inevitably.
The next thing to realize is that continuing a task with flawed information is far worse than crashing. If a bug appears and you continue running the application with the incorrect data from that function you risk polluting/corrupting your data. That corruption can spread aroudn your application like a virus, causing crashes in other places and corrupting even more data. If its more subtle and you fail to notice the problem in time you may not even be able to recover from a backup.
That is way it's better to fail fast/early and just tell the user "sorry, we can't do this for you right now, please come back tomorrow".
A lot of points in the contract of most everyday code can be inferred (e.g. parameters and return values not null unless stated), a function does not modify its parameters unless the name cleary indicates it (addTo
, ...).
But in every larger project there is a considerable amount of contract elements that a user of the function can only make out by reading the entire code. No matter how hard you strive towards always programming by the conventions you defined, there will always be some functions.
Now, depending on how important it is for the program to crash rather than output garbage (e.g. avionics), you want to prevent contract violations (because, as stated, they inevitably lead to bugs and errors). To do so there are basically three options available to the software engineer:
- expect users of code to read it thoroughly before using it
- document the contract in the documentation comment and expect users of code to read that documentation before using it
- code the contract into the function and crash whenever it is violated
The first option almost never works. People usually only read other code when it's not doing what they expect it to in their current task.
The second option is only slightly better, depending on the programmers working with the code. Some read docblocks, others don't even know they're there and instead spend ten minutes researching on the internet what the docblock could have told them in 10 seconds.
The third option doesn't give the user a choice. If the function is not invoked correctly, the program crashes before further harm can be done. If the function doesn't behave well the output assertion will crash the program.
Then also there is the thing with changes in the contract. Those usually happen unconsiously. But when a functions contract changes a lot of its usages have to be adjusted, too. Another way to look at this: documentation (especially contracts) is code that never runs. It is therefore doomed to rot pretty quickly. And rotten code will not do you any good.
With option 1 there you are out of luck unless it triggers a bug covered by your testsuite. Same goes for option 2.
Option 3 will make your testsuite fail in a lot of places unless you adjusted the contract. That will at least give you a chance to notice you just changed the contract and adjust client code accordingly.
Extremes are almost never the best option as goes for excessive use of such assertions. IMO it is important that you define conventions project-wide (or codebase-wide). You can then use assertions for the cases where you deviate from the convention. Note that, for the example sqrt(x)
this would still leave assertions like such:
- x must be real
- x >= 0
- return value not null
- return value >= 0
To address you question regarding unit tests: contract assertions are in no way a replacement for unit tests. They complement each other. Just look at sqrt(x)
again: Asserting that the output is not null and >= 0 does not at all test whether sqrt(4) == 16
.