• No results found

Load-Testing RMI-Based Servers with JUnit

JUnit is an open-source Java project that provides a framework for writing and executing unit tests. It promotes writing test code that asserts the validity of the application functionality. A JUnit test case is a Java class that is compiled and executed to test the application code. This approach provides the benefits of automated retesting with the ease of maintaining the testing code in sync with the application code. Last, but not least, developers get to write Java code instead of click buttons in debuggers and test tools, which is probably a significant contributor to the popularity of the framework.

To use JUnit, a developer must write a test case that extends junit.framework.TestCaseor implements junit.framework.Test. The test case consists of calls to the classes that are being tested and assertions that the return values match the expected result. For example, a test case for a bank account can get the current balance, make a deposit, and then verify that the new balance matches the old balance plus the deposit amount. The quality of the test case is directly proportional to the zeal of the developer. The idea is to try to cover all possible scenarios, including the erroneous ones. After the tests are written, they are compiled and executed individually or in groups. JUnit is a well-documented and easy-to-learn framework, and if you haven’t worked with it yet, please invest a couple of hours in reading the manual and the examples. (Don’t forget to update your résumé because good managers will view it as a sign of a quality developer.) The framework and its related documentation can be down- loaded for free from http://www.junit.org. The rest of this section focuses on developing a load test for Chat based on JUnit.

Chat was certainly not built to be indestructible, but it was not meant to scale to hundreds of users either. As long as it can handle between three and six simultaneous users, it is probably enough to satisfy the concurrency requirements for a demo application. For any load test, you should set the high mark a little above the anticipated maximum load. So, for our example, we will use 10 as the number of virtual clients it should support. Our goal is to simulate this number of clients simultaneously sending messages to the same Chat applica- tion. We also want to stagger the calls to mimic real-life experience. Staggeringmeans that, instead of sending the requests at the same time, they are sent around the same time. Sometimes the termsimultaneousis used to describe the clients that are sending a request at the same time and concurrent is used to describe the clients that maintain a conversation with the server but are sending requests around the same time. On a single CPU system, there is really no distinction between concurrent and simultaneous execution because there can be no true parallel processing, making the terms interchangeable.

107

We will begin by developing a test case that simulates a user sending one message to the target host. Then we will build a harness that uses the test case to create a number of virtual users repeatedly sending the messages. To be flexible, we will allow parameterization of the test by specifying the number of simultaneous users to simulate, the number of times to repeat the test, and the lag time to use for staggering the calls.

The test case is written in covertjava.loadtest.ChatTestCase. It extends TestCaseand implements the core logic in its testSendMessage()method, which is shown in Listing 11.1.

LISTING 11.1 testSendMessageSource Code

public void testSendMessage() {

logger.info(“Sending test message...”); try {

StringBuffer message = new StringBuffer(); message.append(“[ChatTestCase@”);

message.append(Integer.toHexString(this.hashCode())); message.append(“] Test “);

message.append(messagesSent++);

ChatServer.getInstance().sendMessage(this.host, message.toString()); logger.info(“Sent message successfully”);

}

catch (Exception e) { e.printStackTrace();

assertTrue(“Exception: “ + e.getMessage(), false); }

}

ChatTestCasecreates a test message and uses ChatServerto send it to the target host. The key aspect of this method is a call to assertTrue()in the catchstatement with falseas the second parameter, which tells JUnit that this test has failed. Otherwise, the method simply returns, which would mean success. This way of testing is certainly far from perfect to ensure that the Chat server has processed the message correctly. It does not test whether the message has been parsed and appropriately added to the conversation history window. However, it provides a fairly decent tactic of testing the network communication and the throughput of the remote server and therefore will suffice to illustrate the point.

The next step is creating a test suite that contains ChatTestCaseinstances. This is accom- plished in the ChatLoadTestclass; the code is shown in Listing 11.2.

LISTING 11.2 Creation of a Test Suite

ActiveTestSuite suite = new ActiveTestSuite(); for (int i = 0; i < clientsNumber; i++) {

Test test = new ChatTestCase ();

test = new DelayedTest(test, (int)(Math.random()*lagTime)); test = new RepeatedTest(test, repeatRuns);

suite.addTest(test); }

The parameters, such as clientsNumberand lagTime, are read from the properties file to support customization. JUnit test suites serve as containers for test cases and make running multiple tests easier. ActiveTestSuiteruns all its tests simultaneously and then waits for them to finish before returning the result. JUnit uses a decorator pattern to attach additional functionality to tests. RepeatedTest, for example, runs a given thread repeatedly for a given number of times. To provide staggering execution for a test, we have added the DelayedTest

decorator class that sleeps for a given period of time before running the test. The end result of the code in Listing 11.2 is a clientsNumbernumber of clients that will simultaneously send messages after sleeping for a random time.

You can run a JUnit test in several ways, but we will use the Swing GUI to get visual feedback on the testing progress. If an instance of Chat is not running on the localhost yet, we start it using CovertJava\distrib\bin\chat.bat. Then we use the loadtestJUnit.batfile located in the CovertJava\bindirectory to open the JUnit GUI and execute our test suite. Shortly after the tests begin running, the JUnit GUI should look similar to Figure 11.1.

109

Load Testing RMI-Based Servers with JUnit

Most of the tests failed, and looking at the result panel, we can see the error message: testSendMessage(covertjava.loadtest.ChatTestCase): Exception

java.lang.NullPointerException: null. Examining the Chat application shows that the conversation history component contains a mess and that several NullPointerExceptions are in the console window. Guess what? We are starting to reap the benefits of load testing. Reducing the number of clients to two makes the tests run successfully, so the problem must come from multithreading. Chapter 9, “Cracking Code with Unorthodox Debuggers,” and Chapter 10, “Using Profilers for Application Runtime Analysis,” have provided techniques for finding and fixing the problems that arise from concurrent execution. You can try applying these to find out what is wrong with Chat.

For those who feel knowledgeable enough, I will disclose that the problem comes from the way the new messages are appended to the conversation history. Swing is not thread safe, and it is generally recommended to interact with Swing components from the AWT event dispatch thread. The designers of Swing have sacrificed robustness for speed, and we can’t really blame them because Swing performance has long been under scrutiny. Chat receives the incoming messages on an RMI thread and delegates message processing to the MainFrame class. MainFrame’s appendMessagemethod, which appends a new message to the conversation JEditorPane, does not use synchronization. Therefore, if several users try to send a message to the same host at the same time, several threads will be trying to set data to the same JEditorPane. This is a classic data overrun problem that can be solved by making the appendMessagemethod synchronized. After making this change, rerunning the load test produces a clean result that allows us to happily consider the job done.

The benefit of using JUnit for load-testing is that it is simple and enables you to leverage any test cases that might already have been written. It is also an effective method of testing RMI- based servers because automatic script recording by load-testing tools does not always produce maintainable results.