03.06.2015 Views

Using Automated Tests to Document Software Architectures - ASPE

Using Automated Tests to Document Software Architectures - ASPE

Using Automated Tests to Document Software Architectures - ASPE

SHOW MORE
SHOW LESS

You also want an ePaper? Increase the reach of your titles

YUMPU automatically turns print PDFs into web optimized ePapers that Google loves.

<strong>Using</strong> <strong>Au<strong>to</strong>mated</strong> <strong>Tests</strong> <strong>to</strong><br />

<strong>Document</strong> <strong>Software</strong> <strong>Architectures</strong><br />

A WHITE PAPER PREPARED FOR <strong>ASPE</strong> TECHNOLOGY BY JERRY OVERTON,<br />

SENIOR SOFTWARE ENGINEER, OBJECT COMPUTING, INC. (OCI)<br />

www.aspetech.com <strong>to</strong>ll-free: 877-800-5221


<strong>Using</strong> <strong>Au<strong>to</strong>mated</strong> <strong>Tests</strong> <strong>to</strong><br />

<strong>Document</strong> <strong>Software</strong><br />

<strong>Architectures</strong><br />

Introduction<br />

by<br />

Jerry Over<strong>to</strong>n, Senior <strong>Software</strong> Engineer<br />

Object Computing, Inc. (OCI)<br />

The book Extreme Programming Explained, by Kent Beck,<br />

suggests that the architecture of an application is better<br />

documented using au<strong>to</strong>mated tests rather than detailed<br />

specifications. The book briefly describes an example of<br />

documenting the required processing capacity of an application<br />

using tests. Improvements <strong>to</strong> the architecture of the application<br />

are made by updating the software <strong>to</strong> pass the tests. The<br />

example by Beck was straightforward, but it also lacked detail.<br />

For more detailed software quality requirements, is it possible <strong>to</strong><br />

write tests that can verify that an architecture satisfies these<br />

complex requirements? This article presents a case study<br />

designed <strong>to</strong> explore the viability of documenting more complex<br />

architectural requirements using au<strong>to</strong>mated tests.<br />

The Case Study<br />

The design of the case study for this article is taken from the<br />

book Essential <strong>Software</strong> Architecture by Ian Gor<strong>to</strong>n. In the book,<br />

Gor<strong>to</strong>n describes a design for the Information Capture and<br />

Dissemination Environment (ICDE). The ICDE is an application<br />

that au<strong>to</strong>matically captures events as a result of various actions<br />

of workstation users. The information captured is made available<br />

<strong>to</strong> third-party <strong>to</strong>ols interested in analyzing various aspects of<br />

user behavior.<br />

This case study is based on an actual system and should provide<br />

the kind of complexity needed <strong>to</strong> better test the viability of


au<strong>to</strong>mated tests as software architecture documentation. This<br />

article attempts <strong>to</strong> partially re-create the architectural solution<br />

given in the book, but <strong>to</strong> do so using au<strong>to</strong>mated tests rather<br />

than detailed specifications.<br />

Augmenting Source Code<br />

<strong>Document</strong>ation<br />

When comparing real examples of au<strong>to</strong>mated tests <strong>to</strong> the<br />

architectural solution described in Essential <strong>Software</strong><br />

Architecture, it becomes clear that source code (written in Java)<br />

alone is not expressive enough <strong>to</strong> replace every aspect of an<br />

architecture specification. Even for an experienced software<br />

developer reading well-written code, it is often necessary <strong>to</strong><br />

have additional background and context information <strong>to</strong><br />

understand what the code does.<br />

Java source code does not provide a good mechanism for<br />

documenting context or background for concerns that span<br />

classes located in several different source files. This<br />

documentation could be created as comments in the source files,<br />

but the information would have <strong>to</strong> be repeated for all applicable<br />

files or placed in a single location. Repeating the same<br />

documentation in multiple files makes the documentation much<br />

more difficult <strong>to</strong> maintain and placing the documentation in one<br />

place assumes that the reader will know where <strong>to</strong> find the<br />

information.<br />

In this article, UML models are used <strong>to</strong> document context<br />

information that cannot be easily described using source code<br />

alone. In keeping with the agile style of au<strong>to</strong>mated testing, all<br />

models were created using Agile Modeling. Models were created<br />

using the simplest <strong>to</strong>ols possible and contain only the elements<br />

necessary <strong>to</strong> communicate context. Once a model was created, it<br />

was updated only when absolutely necessary. The models are<br />

not meant <strong>to</strong> be exact representations of the code.<br />

How <strong>to</strong> Read the Rest of this Article<br />

The reaminder of this article is written as an architecture<br />

description of the ICDE application. The next section gives an<br />

overview of the entire architecture and each major section after<br />

that describes the solution for a specific architecturally


significant scenario. All scenarios are introduced in the<br />

architecture overview, but for brevity, only one scenario is<br />

described in detail.<br />

The fact that the Notify Event test class both extends JUnit<br />

TestCase and implements an interface from the architecture may<br />

look weird at first. The test was written according <strong>to</strong> the The<br />

Self-Shunting Unit Test Pattern. In this pattern, tests<br />

impersonate collabora<strong>to</strong>rs in order <strong>to</strong> find out things that only a<br />

collabora<strong>to</strong>r could know. This trick was very useful for testing<br />

some of the more complex architectural requirements.<br />

Start with the Architecture Overview <strong>to</strong> get an idea of the<br />

elements introduced and why. Read the Event Notification<br />

section for an explanation of how the elements are used <strong>to</strong><br />

satisfy the quality requirements of that scenario. The Conclusion<br />

section summarizes the most important lessons learned by<br />

working through this exercise. A .zip file containing the full<br />

source code shown below is available for review.<br />

Architecture Overview<br />

The ICDE application records events generated by workstation<br />

users. Information from users is s<strong>to</strong>red in the ICDE application<br />

and that information is available for query by third-party <strong>to</strong>ols.<br />

Third-party <strong>to</strong>ols can register <strong>to</strong> receive notifications of particular<br />

events. When those events occur, the system notifies registered<br />

listeners.


The overall design for the application uses a combination of the<br />

Presentation-Abstraction-Control Pattern (PAC) and the Observer<br />

Pattern. The user's state is modeled after the observer pattern<br />

and the third party <strong>to</strong>ol is modeled after the PAC pattern. The<br />

listener class is the intersection between the two patterns and<br />

allows updates from the user <strong>to</strong> be communicated <strong>to</strong> third-party<br />

<strong>to</strong>ols.


Key <strong>to</strong> the architecture solution is the user state. The user state<br />

is responsible for maintaining a list of listeners and their<br />

associated interests. The user state updates listeners with new<br />

information as the state changes. Listeners are allowed <strong>to</strong> poll<br />

for all relevant updates or query for specific ones.<br />

package userFramework;<br />

public class AbstractUserState implements UserState{<br />

private HashMap events = new HashMap();<br />

private HashMap register = new HashMap();<br />

public void update(int eventType, String event){<br />

events.put(eventType, event);<br />

}<br />

public String poll(StateListener listener){<br />

//return all events that the listener has<br />

registered for<br />

String message = "";<br />

for (int i=0; i <<br />

register.get(listener).length; i++){<br />

message +=<br />

events.get(register.get(listener)[i]);<br />

}<br />

return message;<br />

}<br />

public String query(int eventType){<br />

//return the events that match the given type<br />

return events.get(eventType);


events){<br />

}<br />

}<br />

public void register(StateListener listener, int[]<br />

}<br />

register.put(listener, events);<br />

For the third-party <strong>to</strong>ol solution, the most architecturally<br />

significant class is the listener. The listener is registered with the<br />

user state and is responsible for keeping the user interface and<br />

local user view of the third-party <strong>to</strong>ol up <strong>to</strong> date.<br />

package thirdPartyToolFramework;<br />

import userFramework.UserState;<br />

public class AbstractStateListener implements StateListener{<br />

private UserState userState;<br />

private UserView userView;<br />

private UI ui;<br />

view){<br />

}<br />

public AbstractStateListener<br />

(UserState state, UI userInterface, UserView<br />

userState = state;<br />

ui = userInterface;<br />

userView = view;<br />

}<br />

public void update(){<br />

String state = userState.poll(this);<br />

userView.update(state);<br />

ui.setState(state);<br />

}<br />

Event Notification<br />

When the user performs an action of interest on the workstation,<br />

the user state is updated accordingly. All interested listeners are<br />

responsible for polling for updates at regular intervals. If the<br />

user state has been updated with messages of interest <strong>to</strong> the<br />

listener, that information is returned <strong>to</strong> the listener when the<br />

listener polls. After the listener has received and update, the<br />

listener is responsible for updating the user interface of the<br />

third-party <strong>to</strong>ol.


The quality requirements and satisfying architecture for the<br />

Event Notification scenario is documented by the NotifyEventTest<br />

class.<br />

package architectureRules;<br />

public class NotifyEventTest extends TestCase implements<br />

UserState {<br />

...<br />

}<br />

Location Transparency<br />

To encourage adoption by third-party developers, the ICDE API<br />

has <strong>to</strong> support location transparency for event notification.<br />

Third-party <strong>to</strong>ols should not have <strong>to</strong> be coupled <strong>to</strong> a particular<br />

application distribution, nor should they have <strong>to</strong> rely on specific<br />

users for their updates. The system should allow the event<br />

generation mechanisms <strong>to</strong> be swapped out without disruption <strong>to</strong><br />

the third-party application.<br />

public class NotifyEventTest extends TestCase implements<br />

UserState {<br />

private NotifyEventTest servant;<br />

private StateListener listener;<br />

...<br />

public void testLocationTransparency(){


user<br />

NotifyEventTest();<br />

NotifyEventTest();<br />

//the architecture must allow replacement of<br />

//states <strong>to</strong> be transparent <strong>to</strong> the listeners<br />

//create two versions of the same service<br />

NotifyEventTest servantA = new<br />

NotifyEventTest servantB = new<br />

//use the test as a proxy and configure it<br />

with a servant.<br />

//use the proxy <strong>to</strong> query data and poll for<br />

data<br />

servant = servantA;<br />

String firstPoll = this.poll(listener);<br />

String firstQuery =<br />

this.query(UserState.DEFAULT_EVENT);<br />

//run the same test with a different servant<br />

//and compare the results<br />

servant = servantB;<br />

String secondPoll = this.poll(listener);<br />

String secondQuery =<br />

this.query(UserState.DEFAULT_EVENT);<br />

}<br />

assertTrue(firstPoll.equals(secondPoll));<br />

assertTrue(firstQuery.equals(secondQuery));<br />

...<br />

events){}<br />

}<br />

public String poll(StateListener listener){<br />

return "Poll Successful";<br />

}<br />

public String query(int eventType){<br />

return "Query Successful";<br />

}<br />

public String proxyPoll(StateListener listener){<br />

return servant.poll(listener);<br />

}<br />

public String proxyQuery(int eventType){<br />

return servant.query(eventType);<br />

}<br />

public void register(StateListener listener, int[]<br />

public void update(int eventType, String event){}<br />

Performance<br />

Event notifications should be fast. Once an event notification is<br />

sent out, it should be received rapidly by interested third-party<br />

<strong>to</strong>ols. Any mechanism responsible for disseminating information<br />

must do so without unnecessary delay. The event notification


mechanism should provide sub-second message delivery<br />

performance.<br />

public class NotifyEventTest extends TestCase implements<br />

UserState {<br />

private NotifyEventTest servant;<br />

private StateListener listener;<br />

private static long MAX_TIME = 100;<br />

...<br />

until<br />

and<br />

();<br />

public void testPerformance(){<br />

//event notifications have <strong>to</strong> be fast.<br />

//measure the time from when an event occurs<br />

//the notification is received by a subscriber<br />

//make sure that the time elapse is acceptable<br />

List listeners = new ArrayList<br />

UserState userState = new AbstractUserState();<br />

listeners.add(newRegisteredListener(userState));<br />

long time = timeToUpdateAndNotifyEvent(userState,<br />

listeners);<br />

assertTrue(time < MAX_TIME);<br />

}<br />

...<br />

userState){<br />

private long timeToUpdateAndNotifyEvent<br />

(UserState userState, List<br />

listeners){<br />

//register all listeners with the given state<br />

for (StateListener listener : listeners)<br />

registerListener(userState, listener);<br />

//update the user state and measure how long it takes<br />

//the listeners <strong>to</strong> get an update<br />

return timeToUpdate(userState, listeners);<br />

}<br />

private StateListener newRegisteredListener(UserState<br />

UI userInterface = new AbstractUI();<br />

UserView userView = new AbstractUserStateView();<br />

AbstractStateListener listener =<br />

new AbstractStateListener<br />

(userState, userInterface, userView);<br />

registerListener(userState, listener);<br />

return listener;<br />

}<br />

private void registerListener<br />

(UserState userState, StateListener listener){


int[] events = {UserState.DEFAULT_EVENT};<br />

userState.register(listener, events);<br />

}<br />

private long timeToUpdate<br />

(UserState userState, List<br />

listeners){<br />

Event");<br />

long startTime = System.currentTimeMillis();<br />

userState.update(UserState.DEFAULT_EVENT, "Test<br />

for (StateListener listener : listeners)<br />

userState.poll(listener);<br />

long endTime = System.currentTimeMillis();<br />

return endTime - startTime;<br />

}<br />

...<br />

}<br />

Scalability<br />

The ICDE system must be capable of scaling <strong>to</strong> up <strong>to</strong> 150<br />

concurrent users. A successful architecture will incur minimal<br />

performance degradation due <strong>to</strong> additional users. The<br />

performance of the event notification mechanism must be<br />

capable of keeping up with a growth in the number of users, up<br />

<strong>to</strong> the anticipated maximum.<br />

public class NotifyEventTest extends TestCase implements<br />

UserState {<br />

private NotifyEventTest servant;<br />

private StateListener listener;<br />

private static long MAX_TIME = 100;<br />

private static int MAX_USERS = 150;<br />

...<br />

users<br />

should<br />

<strong>to</strong> the<br />

();<br />

public void testScalability(){<br />

//event notification must scale <strong>to</strong> 100-150<br />

//the performance of the notification design<br />

//scale linearly with the number of users up<br />

//maximum expected capacity.<br />

List listeners = new ArrayList<br />

UserState userState = new AbstractUserState();<br />

for (int i=0; i < MAX_USERS; i++){<br />

listeners.add(newRegisteredListener(userState));<br />

}


long time = timeToUpdateAndNotifyEvent(userState,<br />

listeners);<br />

assertTrue(time < MAX_TIME * MAX_USERS);<br />

}<br />

...<br />

}<br />

Conclusions<br />

With help from a few agile models and some prose, au<strong>to</strong>mated<br />

tests were capable of documenting some fairly detailed software<br />

architecture requirements like location transparency and<br />

scalability. Although it remains <strong>to</strong> be seen whether or not tests<br />

are sufficient for other software qualities, the approach seems<br />

viable so far.<br />

<strong>Using</strong> tests <strong>to</strong> document architectural requirements helped<br />

reduce the tendency <strong>to</strong> over-engineer a solution. The best<br />

solution is the simplest solution that passes the test. Any<br />

complexity beyond that is unnecessary. The tests provide an<br />

objective measure for minimum simplicity required. <strong>Tests</strong> also<br />

make it obvious where the solution's risks are. Any tests failing<br />

indicate qualities that are at risk.<br />

<strong>Tests</strong> make it much easier <strong>to</strong> estimate the cost of deferring a<br />

design decision. When architecture is documented using tests,<br />

the cost of changing a design later will be roughly proportional <strong>to</strong><br />

the cost of the refac<strong>to</strong>ring required <strong>to</strong> make the change. Without<br />

tests, the true cost of a design change is hidden by how easy it<br />

is <strong>to</strong> update models and descriptions.<br />

The certainty that comes with documenting architecture using<br />

tests comes with a price. Generally, it takes longer <strong>to</strong> write a<br />

test (and code that could pass the test) for a software quality<br />

than it does <strong>to</strong> create UML depictions and explana<strong>to</strong>ry text.<br />

However, it is not clear if time saved with less formal<br />

documentation would result in an increase in re-work due <strong>to</strong><br />

missed requirements later.<br />

Also, it is not clear <strong>to</strong> what limit this technique can be extended.<br />

The example presented in this article is more detailed than the<br />

example given in Extreme Programming Explained, but certainly


does not represent the most complex and detailed example<br />

possible.<br />

References<br />

• Beck, K., Extreme Programming Explained, Second Edition,<br />

Pearson Education Inc., NJ, 205<br />

• Gortin, I. Essential <strong>Software</strong> Architecture, Springer-Verlag<br />

Berlin Heidelberg, 2006<br />

• Ambler, S. W., The Elements of UML 2.0 Style, Cambridge<br />

University Press, New York, 2005

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!