Using Automated Tests to Document Software Architectures - ASPE
Using Automated Tests to Document Software Architectures - ASPE
Using Automated Tests to Document Software Architectures - ASPE
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