Quality By Design - Part 2
|
|
Abstract
"I'm going to cure the world of software bugs and make everybody's lives much
easier." [Chaplin 2001] Bugs in software are bad. They cost more and more to
fix as they are discovered later in the project life-cycle. Often bugs that
were fixed previously arise again after changes to the software. Testing is
often cut short due to pressing deadlines and software is released with bugs
still present. Developers often do not exhaustively test their own code each
time they make changes.
This article, in three parts, explains how some simple design and development
techniques can be used to eliminate all those bug concerns and massively
accelerate your productivity. The three techniques described are Automated Unit
Testing, used heavily in Extreme Programming(XP); Design By Contract, and
Response Matrices.
By using these techniques you can quite quickly double your productivity, and
in some cases increase productivity four or even five fold.
Introduction
In Part 1
I explained how to build Automated Unit Test Harnesses using simple assertions
together with the benefits of implementing Automated Unit Test Harnesses.
In this part, Part 2, I will show how you can use Design By Contract techniques
to make your software more robust and easier to integrate.
Finally, in Part 3
, I’ll explain how you can utilise Response Matrices with Design By Contract to
drive out exhaustive sets of Unit Tests for your classes.
Design By Contract
Once you get your head around Design By Contract (DBC) and it clicks with you,
you will realise how simple and effective the technique is. I will summarise
DBC here and refer you to other sources for more exhaustive explanations.
DBC uses the concept of a contractual Client-Supplier relationship. The Client
is the bit of code calling a method, and the method is the Supplier. There is a
contract between them that says ‘If the Client adheres to a certain set of
rules about how it calls the method then the Supplier will guarantee some
expected behaviour (a return value say, or a change of state). We use
preconditions and postconditions to specify the contract. If the Client
satisfies the preconditions, then the supplier will promise to satisfy the
postconditions. An example,
Let’s say I have a class called Account, whereby you can withdraw and deposit,
but you are not allowed to go over you overdraft limit. If you attempt to go
over your limit an exception should be raised, since this is an error.

<:get><:get>
The conditions for the WithDraw method can be stated as follows:
Account::Withdraw(idblAmount)
-----------------------------
Pre:
idblAmount > 0
idblAmount < = Balance + mdblOverDraftLimit
Post:
mdblBalance = mdblBalance@pre + idblAmount
[The @pre indicates the value of the property before the method was called.]
[Aside: The conditions written above are actually called 'Constraints'. The
standard way of writing these constraints is using the Object Constraint
Language (OCL). This is part of the UML specification. It is designed to be
simple to understand and use. You can pick up the basics within a day.
Alternatively, you could write your constraints in what ever language you want.
The rule is that they must be boolean expressions and evaluate to True or
False.]
There are a number of ways of enforcing this contractual relationship:
-
We can leave it to the Client to ensure they have met the conditions and do not
put any checks in the Supplier. This way does have advantages over others, but
is not within the scope of this paper.
-
We put checks in the Supplier to ensure that the Client adhered to the
conditions. This is Defensive Programming which is safer, but not as flexible
than the first option. When you first do use DBC I recommend you do it this
way.
Thus, using the second approach we would have code as follows:
‘** Pre-condition checks **’
If idblAmount < = 0 Then
Err.Raise ERR_AMT_INVALID, Err.Source, “Amt must
be > 0”
End If
If idblAmount > Balance + mdblOverDraftLimit Then
Err.Raise ERR_OD_VIOLATED, Err.Source, “Amt
exceeds od limit”
End If
mdblBalanceBefore = mdblBalance
‘** Method body **’
mdblBalance = mdblBalance – idblAmount
‘** Post condition checks **’
If mdblBalance <> mdblBalanceBefore + idblAmount Then
Err.Raise ERR_POST_FAILED, Err.Source,
“Account:WithDraw….”
End If
Okay, so for this very small example it does appear that there is a lot of code
that essentially doesn’t do much. But, for larger components with complex
interfaces there could be many pre and post conditions. If you have an object
that can take many states then you need whole sets of preconditions depending
on the accepting state of the object. With clearly defined preconditions and
postconditions you can check that the Client is using you correctly and throw
an error if they are not. You can also provide a meaningful message as to why
things do not work, rather than just throwing something like 'An error
occurred', or 'Amount not allowed'. Your error descriptions should state what
rule failed, why it failed, and how it can be overcome. E.g. 'the amount cannot
exceed zero balance plus overdraft. You entered an amount of X, when available
funds was Y. Please enter a smaller amount.'
Something to avoid is optional parameters. These things are bad news. With one
optional parameter you need two sets of preconditions. With two optional
parameters you might need four sets and so on.
Thinking in advance what the preconditions are for a particular method gives
you an indication, before you have even written the code, how complex the
behaviour could be. If you have many preconditions you may want to re-visit
your design to make it simpler. Simpler designs are easier to understand,
easier to code, have easier contracts to enforce, and are much easier to test.
In Part 3 when we look at Response Matrices, we will see how simple designs
make testing much easier.
Class Invariants
Something I've not mentioned yet is the class invariant. This is part of DBC in
addition to preconditions and postconditions. An invariant is something that
must always be true before and after an interface is called. They can also be
checked after the method body to ensure that the state of the object is
correct. In this case the invariants would be:
msngBalance > = 0 – msngOverdraftLimit
msngOverdraftLimit > = 0
I have purposely not mentioned them in this paper, since you can show using Set
Theory (which all this derives from by the way) that the preconditions and
postconditions imply the invariant anyway, so the invariant tests are
superfluous.
Invariants are very useful for determining the pre and post conditions for the
class interfaces. You will find it easier to start by writing the invariant for
a class, and then move onto the pre and post conditions.
Ensuring the Supplier Is Testable
You should make sure that the class you write is actually testable. This means
that any properties that are included as part of a precondition are exposed to
the client via a public property method. By exposing these properties we can
then write Automated Unit Test (from Part 1). Everything is starting to come
together now!
So. Lets take a look at what the Automated Unit Tests would look like.
Testing The Class Using Automated Unit Tests
There are three tests for the withdraw method. In Part 3 you will see how these
tests were derived exhaustively.
-
If I pass an amount less than zero and an amount less than balance plus
overdraft it should raise an error = ERR_AMT_INVALID
-
If I pass an amount greater than zero, but greater than balance plus overdraft
it should raise an error = ERR_OD_VIOLATED.
-
If I pass an amount greater than zero, and less than balance plus overdraft,
the balance afterwards should be equal to its previous balance minus the
amount.
Dim objCAccount As CAccount
Set objCAccount = New CAccount
[Assume at this stage that the overdraft is set to 1000, and the balance is
300]
'/* Test 1. */'
On Error Resume Next
ObjCAccount.WithDraw(-100)
Debug.Assert Err.Number = ERR_AMT_INVALID
'/* Test 2. */'
On Error Resume Next
ObjCAccount.WithDraw(2000)
Debug.Assert Err.Number = ERR_OD_VIOLATED
'/* Test 3. */'
On Error Resume Next
dblBalanceBefore = objCAccount.Balance
ObjCAccount.WithDraw(500)
Debug.Assert Err.Number = 0
Debug.Assert Err.Number = (objCAccount.Balance = dblBalanceBefore - 500)
Starting to Use Design By Contract
For new comers to this topic who wish to try out some of these techniques I
would suggest the following track:
-
Choose a fairly small component to build, or set of classes that only have a
few interfaces. Choose classes whose objects have a single state.
-
Implement Design By Contract on those classes.
-
Once you’ve mastered single state objects, move onto ones with multiple states.
In Part 3 I will show you how to use Response Matrices with Design By Contract
to drive out exhaustive sets of Unit Tests for your classes.
|