This is a question that I have been asking myself for centuries, and still have not received a satisfactory answer.
But I believe that when it comes to checking arguments, you need to distinguish between two cases:
Are you checking the argument to catch logical programming errors?
if (foo == null) throw new ArgumentNullException("foo");
probably an example of this.
Are you checking the argument because it is some kind of external input (provided by the user or read from the configuration file or from the database) that may be invalid and must be rejected?
if (customerDateOfBirth == new DateTime(1900, 1, 1)) throw …;
there may be this type of argument checking.
(If you advertise an API consumed by someone outside of your team, example 2 also applies as well.)
I suspect that methodologies such as unit testing, contract design, and to some extent “early failure,” focus mainly on verifying the first type of argument. That is, they are trying to detect logical programming errors, and not invalid input.
If so, then I dare to say that it doesn’t really matter which error detection method you follow; Each has its advantages and disadvantages. & dagger; As a last resort (for example, when you have absolute confidence in your ability to write code without errors), you can even completely reject these checks.
However, whatever method you choose to detect logical errors in your code, you still need to check user input, etc., so you need to distinguish between two types of argument checks.
& dagger;) An amateurish, incomplete attempt to compare the relative strengths and weaknesses of Design by Contract, unit testing and the “failed early”:
(Although you did not ask for it ... I just mentioned a few key differences.)
Failure before (for example, checking for an explicit argument when starting a method):
- Writing basic checks like
null
protection is easy to write - can mix logical error protection and external input validation with the same syntax
- does not allow you to check the interaction of methods.
- does not encourage you to strictly define (and therefore think) your method contracts
Unit Testing:
- Allows you to test code in isolation, without running a real application, so error detection can be faster
- If a logical error occurs, you do not need to trace the stack to find the reason, because each unit test indicates a specific “usage example” of your code.
- allows you to test not only individual methods, for example. even the interaction between several objects (think stubs and layouts)
- convenient tests (for example, protection against
null
) is more work than when approaching with an error (if you strictly adhere to the Arrange-Act-Assert pattern)
Design by contract:
- forces you to explicitly specify the contract of your classes (although this is also possible with unit tests, otherwise otherwise).
- makes it easy to formulate class invariants (internal conditions that must always be satisfied)
- not supported by many programming languages / structures, as other approaches