return ACCOUNT_WAS_LOCKED; }
catch NegativeAmountException negativeAmountException {
if amountToWithdraw.isNegative { return SUCCESS;
} else {
return FAILURE; }
} ifamountToWithdraw.greaterThanbalance {
return FAILURE; }
if amountToWithdraw.isNegative { return FAILURE;
} if correctResult.equalsactualResultingBalance {
return SUCCESS; }
else { return FAILURE;
} }
} There are two things to note here. The first is that we really are using our distributed exceptions
quite heavily. Its important to make sure that the server really does catch improper arguments e.g., an attempt to withdraw a negative amount, and that when the server does throw an
exception, it is does so for the correct reasons.
The second point to note is that its not entirely obvious what a failure means. Suppose performActualTest
fails. This could be due to either of the following reasons: •
The lock wasnt set, and another client thread managed to perform an operation. •
The actual withdrawal code is flawed. The right way to distinguish between these is to write additional tests that check only the locking
mechanism. If the locking mechanism works, then we know the withdrawal code must be flawed. In our case, its not such a big deal; our codebase is small enough to simply spot errors once we
have a good hint. However, in larger applications, distinguishing between possible causes of error is incredibly useful.
This is an important point. When youre building fine-grained tests, they rely, to a large extent, on the existence of many
other fine-grained tests. Every test you add makes the others work better, and makes the testing suite more effective. The
general rule of thumb: if you can talk about something and have a name for it e.g., the locking mechanism, you
should be able to test it.
13.1.3.2 Build aggregate tests that test entire use cases
The use case weve been relying on, first defined in Chapt er 5
, basically consists of a sequence of simple tests performed on the same account. If this were an industrial application, wed
implement this as a single test for the reasons outlined in the next section. However, the code to do so is quite similar to the code for our other tests, and doing so so only serves to make the
testing framework more complex; it serves no pedagogical purpose at all.
13.1.3.3 Build a threaded tester that repeatedly performs these tests
The next step is to build an object that can repeatedly invoke tests. In our case, weve chosen to do this by extending
Thread . Instances of
TestThread repeatedly create tests purely at
random, invoke the test, and then store the test object in an instance of TestResultHolder
. After doing this a predetermined number of times the argument
numberOfOperations is
passed into TestThread
s constructor, the thread notifies its owner, an instance of TestAppFrame
, that it is done: public class TestThread extends Thread {
private static final int MILLISECONDS_TO_PAUSE = 2000; private static int _idNumberCounter;
private NameRepository _nameRepository; private TestResultHolder _testResultHolder;
private int _numberOfOperationsLeft; private TestAppFrame _owner;
private String _idNumber; public TestThreadNameRepository nameRepository, int
numberOfOperations, TestResultHolder testResultHolder, TestAppFrame owner {
_testResultHolder = testResultHolder; _nameRepository = nameRepository;
_numberOfOperationsLeft = numberOfOperations; _owner = owner;
_idNumber = String.valueOf_idNumberCounter++; }
public void run { while_numberOfOperationsLeft 0 {
Test testToPerform = getRandomTest ; testToPerform.performTest_idNumber;
_testResultHolder.addResulttestToPerform; try {
Thread.sleepMILLISECONDS_TO_PAUSE; }
catch Exception ignored{} _numberOfOperationsLeft--;
} _owner.testThreadFinishedthis;
} private Test getRandomTest {
double choice = Math.random ; if choice .1 {
return new GetBalance_nameRepository; }
ifchoice .6 { return new MakeDeposit_nameRepository;
} return new MakeWithdrawal_nameRepository;
} }
The only curious thing here is how we determine what test to use. The answer is that we randomly pick one. At first, this might seem a little disturbing. It may make for more convincing
testing if we followed scripts or what we think an actual user session would be like.
The answer to this objection is twofold. The first is that, to a large extent, if wed encoded the use cases as tests, those tests would be scripts and would reflect what we think an actual user
session would be like. However, even past that, random testing has a significant positive aspect. If an application can handle random method invocations well, it can handle pretty much anything
that gets thrown at it. If, on the other hand, our tests reflect what we think the user will do, we havent really tested how robust the application is at all.
This leads to a compromise. We can make TestThread
an abstract class with a single abstract method:
protected abstract Test getRandomTest Then, we create two concrete subclasses of
TestThread :
getRandomThread Type 1 Randomly chooses from among all the tests available.
getRandomThread Type 2 Also makes random choices. But Type 2 chooses only from among the use-case tests in
an attempt to simulate the real world more accurately. The reason for having two subclasses of
TestThread is simple. They actually return slightly
different types of information. Type 1 ensures that the application functions correctly and is reasonably bulletproof. Type 2 can give much more accurate information about application
performance and scalability. In our case, since we have no use-case tests, weve implemented only Type 1.
13.1.3.4 Build a thread container that launches many threads and stores the resultsof the test in an indexed structure
In our case, this is the main GUI component, TestAppFrame
. The Perform Test button has the following action listener attached to it:
private class TestLauncher implements ActionListener { public void actionPerformedActionEvent event {
try { reset ;
numberOfThreads = Integer.valueOf_numberOfThreadsField.getText
intValue ; int numberOfOperations =
Integer.valueOf_numberOfOperationsField getText.intValue ;
int counter; for counter = 0; counter _numberOfThreads;
counter++ { TestThread nextThread = new
TestThread_nameRepository, numberOfOperations, _testResultHolder,
TestAppFrame.this; nextThread.start ;
Thread.sleep100; wait a little
bit to spread out the load }
while someThreadsNotFinished { Thread.sleep10000; 10 seconds.
Its not bad }
} catch Exception exception {}
finally {resetGUI ;} }
} This resets all the data structures in
TestAppFrame by calling
TestAppFrame s
reset method:
private void reset { _testResultHolder = new TestResultHolder ;
_numberOfFinishedThreads = 0; }
After this is done, TestLauncher
creates a number of instances of TestThread
, based on the value the user typed into
_numberOfThreadsField , and starts them running. Finally, when all
the threads have finished, TestLauncher
calls TestAppFrame
s resetGUI
method, which simply computes the very simple statistics we display in the main text area:
private void resetGUI { _testResultHolder.sortResults ;
_accountChooser.removeActionListener_chooserListener; _accountChooser.removeAllItems ;
_accountChooser.addItemALL_ACCOUNTS; Iterator i = _testResultHolder.getAccountNames.iterator ;
whilei.hasNext { _accountChooser.addItemi.next ;
} _resultsArea.setText;
_accountChooser.addActionListener_chooserListener; computeSummaryforAllAccounts ;
}
13.1.3.5 Build a reporting mechanism