5
Information Processing Letters 108 (2008) 127–131 Contents lists available at ScienceDirect Information Processing Letters www.elsevier.com/locate/ipl Managing module dependencies to facilitate continuous testing Brad Long Oracle Corporation, Brisbane, Qld 4000, Australia article info abstract Article history: Received 31 July 2007 Received in revised form 6 March 2008 Available online 18 April 2008 Communicated by J.L. Fiadeiro Keywords: Software design and implementation Software engineering Testing Continuous integration Software licensing Developing large commercial software systems is complex. Techniques have been proposed to deal with such large-scale systems development. One approach that has had some success is the combination of continuous integration and unit testing. Large systems are often divided into modules based on an area of functionality with potentially different teams developing each module. Invariably these modules rely on and interact with each other. Problems arise when teams wishing to test their module depend on code from other teams that is in development or under modification during maintenance. This paper describes an approach to managing module dependencies that allows teams to test their code in isolation or in conjunction with other modules. This facilitates the ability to continually run tests without being negatively impacted by the state of other modules. An additional side-effect is that the approach exceeds the requirements of the GNU Lesser General Public License by allowing a software vendor to easily provide a limited amount of source code, rather than potentially releasing source code for large portions of their product suite, or allowing reverse engineering of potentially large portions of their proprietary product. © 2008 Elsevier B.V. All rights reserved. 1. Introduction Building large-scale systems is complex. Often such systems are built by teams of developers working on functionally separate modules that interact with each other. For this paper, we define a module to be a collection of one or more classes with a well-defined interface. In this context, a module could be a component as defined by Szyperski [6], or simply a collection of classes (potentially underlying a Facade pattern [4]). For example, modules making up a healthcare applica- tion might include Patient Registration, Encounter Management, Staff Management and many others. These are all broadly functionally separate, however, they often require interaction with one or more modules. On large systems development projects it is typical for each team to work on one or more modules. Furthermore, teams are not necessarily geographically co-located. Thus, large scale systems development often involves many teams working on many modules in many locations. Oracle’s healthcare applications development team is an example of this type of large scale development project. Processes such as continuous integration [3] combined with unit testing go a long way to providing order and stability to large-scale systems development. This technique is being used by Oracle’s healthcare development team to provide rapid feedback of the compilation (reported hourly) and testing (reported daily) status of the software under construction and maintenance. Building continuously provides some level of confidence that the product can be built and released at short notice. As with any software project and particularly any long-lived product, as development proceeds, modules invariably re- quire changes that may affect dependent modules. This is especially the case for products that have been released to one or more customers. Any change to the code of any module during development may affect modules that interact with the E-mail address: [email protected]. 0020-0190/$ – see front matter © 2008 Elsevier B.V. All rights reserved. doi:10.1016/j.ipl.2008.04.015

Managing module dependencies to facilitate continuous testing

Embed Size (px)

Citation preview

Page 1: Managing module dependencies to facilitate continuous testing

Information Processing Letters 108 (2008) 127–131

Contents lists available at ScienceDirect

Information Processing Letters

www.elsevier.com/locate/ipl

Managing module dependencies to facilitate continuous testing

Brad Long

Oracle Corporation, Brisbane, Qld 4000, Australia

a r t i c l e i n f o a b s t r a c t

Article history:Received 31 July 2007Received in revised form 6 March 2008Available online 18 April 2008Communicated by J.L. Fiadeiro

Keywords:Software design and implementationSoftware engineeringTestingContinuous integrationSoftware licensing

Developing large commercial software systems is complex. Techniques have been proposedto deal with such large-scale systems development. One approach that has had somesuccess is the combination of continuous integration and unit testing. Large systems areoften divided into modules based on an area of functionality with potentially differentteams developing each module. Invariably these modules rely on and interact with eachother. Problems arise when teams wishing to test their module depend on code fromother teams that is in development or under modification during maintenance. This paperdescribes an approach to managing module dependencies that allows teams to test theircode in isolation or in conjunction with other modules. This facilitates the ability tocontinually run tests without being negatively impacted by the state of other modules.An additional side-effect is that the approach exceeds the requirements of the GNULesser General Public License by allowing a software vendor to easily provide a limitedamount of source code, rather than potentially releasing source code for large portions oftheir product suite, or allowing reverse engineering of potentially large portions of theirproprietary product.

© 2008 Elsevier B.V. All rights reserved.

1. Introduction

Building large-scale systems is complex. Often such systems are built by teams of developers working on functionallyseparate modules that interact with each other. For this paper, we define a module to be a collection of one or more classeswith a well-defined interface. In this context, a module could be a component as defined by Szyperski [6], or simply acollection of classes (potentially underlying a Facade pattern [4]). For example, modules making up a healthcare applica-tion might include Patient Registration, Encounter Management, Staff Management and many others. These are all broadlyfunctionally separate, however, they often require interaction with one or more modules. On large systems developmentprojects it is typical for each team to work on one or more modules. Furthermore, teams are not necessarily geographicallyco-located. Thus, large scale systems development often involves many teams working on many modules in many locations.Oracle’s healthcare applications development team is an example of this type of large scale development project.

Processes such as continuous integration [3] combined with unit testing go a long way to providing order and stabilityto large-scale systems development. This technique is being used by Oracle’s healthcare development team to provide rapidfeedback of the compilation (reported hourly) and testing (reported daily) status of the software under construction andmaintenance. Building continuously provides some level of confidence that the product can be built and released at shortnotice.

As with any software project and particularly any long-lived product, as development proceeds, modules invariably re-quire changes that may affect dependent modules. This is especially the case for products that have been released to oneor more customers. Any change to the code of any module during development may affect modules that interact with the

E-mail address: [email protected].

0020-0190/$ – see front matter © 2008 Elsevier B.V. All rights reserved.doi:10.1016/j.ipl.2008.04.015

Page 2: Managing module dependencies to facilitate continuous testing

128 B. Long / Information Processing Letters 108 (2008) 127–131

changed module. For example, if the interface of a module, M , changes, other modules that use M may need to rework thecode that interacts with it for the code to compile. In addition, changes to the behavior of a module may affect the behaviorof calling modules, thus requiring further rework.

Whilst the module requiring the change is undergoing development, partial or total loss of function often results, thusrendering calling modules partially or totally useless. This has the consequence that tests can no longer run that use modulesthat depend on the module under development. For modules requiring large changes, weeks may pass before the module istotally functioning, thus hobbling the testing phase of the continuous integration process. Clearly, this situation is far fromideal.

This paper presents an approach for dealing with this problem. In addition to allowing modules to be tested whilst othermodules are in development, it provides an access point to modules on which this module depends (i.e., external modules)and provides an obvious place to extend testing functions to include mock objects [5] which simulate the external module.

Another interesting and useful side-effect of this approach is that it provides a simple and effective way to expose avery small amount of source code to meet Lesser Gnu Public Licence (LGPL) [2] obligations requiring a software providerto permit reverse engineering of their proprietary code for anyone wishing to replace an embedded LGPL library with anupgraded version.

2. Dependency Manager

Our approach, the Dependency Manager, was designed to be a single point of access for a module to access other (ex-ternal) modules in a software application. In this context, external module is anything from a module being developed byanother team to a third-party library. Grouping calls to external modules into one class encapsulates complexity, providingdevelopers with a single reference point when looking for external calls. For example, if an external module changes itsinterface, developers can go straight to the Dependency Manager to change the code, rather than searching through theirentire module’s code-base looking for the external calls. External modules that return objects with changed interfaces canalso be intercepted and adapted to conform to what the calling module expects. However, as with any form of integration,significant size and scope changes may force changes deeper within the calling module itself.

Usually, the methods in the Dependency Manager provide access to an external module for validation purposes. Inaddition to grouping external calls, the Dependency Manager provides the developer with a mechanism to return mockvalues in place of the call to the external module. Thus, if an external module is inaccessible or malfunctioning for aperiod of time, mock values can be used until the external module is operational. This approach allows module owners tocontinue running tests to maintain code quality. Dependency Manager could be implemented using a number of existingdesign patterns. For example, the Proxy [4] design pattern is an apt candidate since the Dependency Manager is indeed aproxy for another module. Although it could be described as a design pattern itself, our preference is to describe it as theimplementation of a design pattern for a particular purpose and context (i.e., assisting continuous testing).

The following sections describe implementation details that are covered in more detail elsewhere [1,4,5], but are providedhere to illustrate the general approach.

2.1. Calls to external modules

The method signatures of calls to external modules can be grouped into two categories, (1) simple value return, and(2) complex object return. All methods are structured in a way that when dependencies are turned off an appropriate valueis returned. This allows the calling code to continue processing without being impacted by an external module.

2.1.1. Simple Value ReturnSimple Value Return methods return a simple Java data type, like an integer (int) or a String. The return value may

be used to populate a text field or used in another algorithm. The format of these methods is as follows:

public <simple value> getValue(<context parameters>);

With dependencies turned off, methods of this type would return an appropriate value of the appropriate type. Clearlythe unit tests do not test module integration unless the dependencies are turned on. However, a non-operational modulecannot be tested. Hence, turning off dependencies allows the developer to, at least, test their module.

A call to the following method, with dependencies turned off, would return a valid value such as ‘2’. The correctness ofthis value is determined by the calling code.

public int getNumberOfBeds(String facilityId);

2.1.2. Complex Object ReturnInstead of returning a simple value like a number or a string, Complex Object Return methods return an entire object.

The returned object may contain both data and behavior. When dependencies are turned off a mock implementation ofthe object is returned containing default values. These complex objects would usually be assigned to variables declared as

Page 3: Managing module dependencies to facilitate continuous testing

B. Long / Information Processing Letters 108 (2008) 127–131 129

interfaces (rather than classes) to allow various implementations of the mock objects to be created easily. The structure ofthese methods is as follows:

public <interface> getComplexObject(<context parameters>);

With dependencies turned off, a call to the following method returns a default implementation of the Person interface.

public Person getPerson(String personId);

2.2. Example implementation

This section shows an example implementation of each of the Dependency Manager’s two types of methods describedabove. The important factor to note in these examples is the behavior of the code when dependencies are turned off. Thisbehavior differs for each type of method.

(1) Simple value return.

public int getNumberOfBeds(String facilityId) {// if dependencies are off, return a valid valueif (dependenciesOff()) {

return 42;}

// if dependencies are on, use the external// module (CareSiteService) to determine the correct// number of beds for the specified facility.return CareSiteService.getNumberOfBeds(facilityId);

}

(2) Complex object return.

public Person getPerson(String personId) {// if dependencies are off, return a default implementation// of the Person interface.if (dependenciesOff()) {

// return an anonymous implementation of Person interface.return new Person() {public String getName() { return "Harry Potter"; }public int getAge() { return 14; }public boolean isOld() { return false; }

};}// if dependencies are on, retrieve the person record from// the external module (PersonService).return PersonService.getPerson(personId);

}

The example above is a simple implementation of the Complex Object Return approach. The following implementationillustrates how this approach can be used to provide testing under different conditions and in a variety of scenarios. ThePERSON_TYPE property designates which type of Person object to create. For example, if PERSON_TYPE == "YOUNG"then an instance of YoungPerson will be returned.

public Person getPerson(String personId) {// if dependencies are off, returns an implementation// of the Person interface from a Mock Object Factory.// The type of object constructed depends on the PERSON_TYPE// property.if (dependenciesOff()) {

// return a Person as designated by PERSON_TYPE.return mockObjectFactory.getPerson(TestProperties.PERSON_TYPE);

}

Page 4: Managing module dependencies to facilitate continuous testing

130 B. Long / Information Processing Letters 108 (2008) 127–131

// if dependencies are on, retrieve the person record from// the external module (PersonService).return personService.getPerson(personId);

}

Clearly there are numerous (and more sophisticated and elegant) techniques than the examples illustrated above. It isrecommended that a stubbing or mocking solution is employed that is most appropriate to the situation at hand.

2.3. Enhancements and alternative implementations

2.3.1. Pluggable Dependency ManagersDifferent implementations of a Dependency Manager could be provided for each module to facilitate testing the module

in different scenarios, not necessarily correct scenarios. For example, one implementation of a Dependency Manager couldbe written to test communication failures, with all methods throwing some form of communication exception. This wouldallow tests to run in an environment simulating a communication failure, ensuring the calling code works as expected.

2.3.2. Levels of dependenciesEach method in the Dependency Manager could be associated with a dependency level or dependency list. These de-

pendency levels would act in a similar way to logging levels. The levels could be configured before each test run, allowingcertain dependencies (of a certain level or lower, or entries in the list) to be tested for a particular test run.

2.3.3. Configurable dependenciesGreater flexibility could be attained by allowing each method (or each method referencing a particular external module)

in the Dependency Manager to be turned on or off based upon method-level (or module-level) testing configuration proper-ties supplied before each test run. This approach allows the tester to inactivate specific calls to external modules, providinga mechanism to isolate external dependencies even further, thus being very useful for debugging purposes.

3. Dependency Manager and the GNU Lesser General Public License

Prior to discussing the GNU Lesser General Public License (LGPL) [2], it is useful to briefly describe the GNU GeneralPublic License (GPL) in as much as it relates to the Dependency Manager pattern. The significance of the GPL is thatit requires any software that uses a library under the GPL to also come under the GPL. Therefore, proprietary softwarethat uses a GPL’d library automatically falls under the GPL, meaning source code needs to be available on request for thepreviously proprietary software. This situation is termed copyleft. Some refer to this as a viral licence, since it ‘infects’ anycode using GPL’d code.

The LGPL was created to limit this viral effect. Hence, developers of proprietary software can use libraries under theLGPL without the requirement to distribute source code for their proprietary code, but must allow their code to be reverseengineered in the case that someone wishes to replace the embedded LGPL’d library with a newer version. Allowing reverseengineering in such a case seems awkward and generally unhelpful.

By using a Dependency Manager (or Proxy, for example), all code that accesses an external module is in only a few(usually one) classes. This is useful for a number of reasons, as mentioned earlier in this paper (specifically, for facilitatingcontinuous testing). However, another real benefit is that a proprietary software developer can supply the source code of theDependency Manager, thus allowing anyone wishing to replace software under an LGPL with a later version. Someone doingso only needs to reveal the code in the methods in the Dependency Manager (for the purpose of modification) to use thenew API under the LGPL’d software. If the Dependency Manager is not used, it is not difficult to imagine a situation wherea vendor would need to provide source code to all classes accessing LGPL’d libraries, which could be in several hundredplaces throughout the application.

4. Conclusion

This paper has outlined an approach called Dependency Manager. The purpose of the approach is to allow developersto turn off dependencies between the module they are working on and modules of other teams or products. DependencyManager encapsulates calls to external modules, which makes external calls easier to locate and modify if required. Inpractice, this simple approach is very effective since it allows developers to continue testing whilst other modules are notfully functioning; and allows continuous integration processes to continue running. The technique provides module ownerswith a high level of confidence that their code works well, both in isolation and when calling other modules. In particular,it provides module owners with the ability to test their module in isolation from other modules if necessary.

An additional side-effect of using the Dependency Manager (or Proxy) is that it provides a buffer, or quarantine, betweenlibraries under an LGPL agreement and proprietary software, hence, the design pattern is genuinely useful for situationswhere embedded LGPL’d software is upgraded, and preserves the original intent of the LGPL.

Page 5: Managing module dependencies to facilitate continuous testing

B. Long / Information Processing Letters 108 (2008) 127–131 131

Acknowledgements

Thanks to Paul Strooper and Matt Etheridge for their assistance and comments on earlier versions of this paper.

References

[1] R. Binder, Testing Object-Oriented Systems: Models, Patterns, and Tools, Addison-Wesley, 1999.[2] Free Software Foundation, Lesser GNU general pubic license. Available at http://www.gnu.org/licenses/lgpl.html.[3] M. Fowler, M. Foemmel, Continuous integration, http://www.martinfowler.com/articles/continuousIntegration.html, 2004.[4] E. Gamma, R. Helm, R. Johnson, J. Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, 1999.[5] T. Mackinnon, S. Freeman, P. Craig, Endotesting: Unit testing with mock objects, in: Proceedings of the 1st International Conference on eXtreme Pro-

gramming and Flexible Processes in Software Engineering—XP2000, 2000.[6] C. Szyperski, Component Software: Beyond Object-Oriented Programming, Addison-Wesley, 1998.