Home > Articles > Programming > Java

This chapter is from the book

The Role of Refactoring

A crucial point we touched on in a number of places is the importance of refactoring when testing enterprise applications.

The fundamental issue with many of these systems is that they are heavyweight processes that have many expectations about their environment. The environment doesn't just include containers and servers; it also includes APIs. For example, code that is coupled to javax.servlet.HttpSession is tricky to test because we'd have to lug in an implementation of that API along with its idiosyncrasies.

Likewise, a servlet is not easy to test. It needs to be deployed into a container, and we'd need to find a request and response to feed to it, and then even if we did manage all that in a test environment, the next problem we'd have to deal with is interrogating the response to a sufficient degree to obtain meaningful assertions that aren't fragile and susceptible to breaking from minor UI changes.

This issue crops up time and time again in almost all the enterprise APIs. EJB2 is a great (and horrific) example of components that are tightly controlled and managed by an expensive container (in terms of start-up and how difficult it is to embed). Similarly, JMS suffers from the same issues and requires a message broker to control the message flow.

The problem isn't restricted just to Java APIs. Enterprise systems often have to work with legacy applications, applications that are expensive and cumbersome to start up or use in a testing scenario.

Refactoring is that great wrench in our toolbox that can help attack all of these problems. Of course, there are many times when it's just not possible to sidestep that ugly, difficult API we need to work with. Refactoring might help minimize the pain, but sometimes there isn't really an easy way to avoid expensive, time-consuming tests, and that's fine, despite what you might read elsewhere!

A Concrete Example

Let's have a look at a concrete problem shown in Listing 3-18. We'd like to test a login servlet. The servlet takes in a request, checks the user name and password, and if they're valid, puts a token in the session denoting that the user is logged in.

Listing 3-18. Example of a login servlet

protected void doPost(HttpServletRequest req,
                      HttpServletResponse res)
            throws ServletException {
  String username = req.getParameter("username");
  String password = req.getParameter("password");
  LoginToken token;
  HttpSession session = req.getSession(false);
  if(session != null) {
    token = (LoginToken)session.getAttribute("logintoken");
    // if user is logged in, then we ignore the login attempt
    if(token.getUserName().equals(username)) {
      return;
    }
  }
  Connection c = null;
  try {
    Object o = new InitialContext().lookup("jdbc/MyDS");
    DataSource ds = (DataSource)o;
    c = ds.getConnection();
    // some SQL calls
    // manipulate the result set
    // extract data from the result set and verify password
    // assuming login is valid...
    token = new LoginToken(username);
  }
  catch(NamingException e) {
    throw new ServletException("Error looking up data source", e);
  }
  catch(SQLException e) {
    throw new ServletException("Error obtaining connection", e);
  }
  finally {
    if(c != null) {
      try {
        c.close();
      }
      catch(SQLException e) {
        // just log it, not much else we can do
        log.error("Error closing connection " + c, e);
      }
    }
  }
  if(token != null) {
    session = req.getSession(true);
    session.setAttribute("logintoken", token);
    // record other info too
    session.setAttribute("loginTime",
  System.currentTimeMillis());
  }
}

This code in Listing 3-18 is quite common. It should be fairly obvious what it does from just glancing at it. The test approaches at our disposal are these.

  • Embed a servlet container.
  • Test it in the container by remote invoking it once it's deployed.
  • Refactor!

No prizes awarded for choosing the last option, given that that is what this section is about!

What's so bad about the other two options? They'd both work equally well; the problem is their overhead. For the embedding solution, we'd still have to go through all the servlet code just to get at the meat of our business functionality. The second approach is even worse; it's not possible to use it during rapid development, and it incurs a very expensive overhead for servlet container start-up. Also, both of these solutions have a lot of collateral damage, in terms of testing APIs and functionality, that's incidental to the desired testing.

We know we need to refactor; how do we do it?

Let's examine what the code does at a high level:

  • Takes in a number of parameters
  • Performs a check against the environment (in this case, the session)
  • Uses the parameters to query a data source
  • Creates a value to place in the environment, along with some extra information

The objective of the refactoring is to capture these goals without resorting to a servlet-specific implementation so that, having done so, the servlet can act as a simple shell to the object that will now encapsulate the functionality.

The object we'll create is a LoginManager. A first stab at this appears in Listing 3-19.

Listing 3-19. Initial attempt at a refactored LoginManager

public class LoginManager {
  public LoginToken login(String username, String password) {
    try {
      Object o = new InitialContext().lookup("jdbc/MyDS");
      DataSource ds = (DataSource)o;
      Connection c = ds.getConnection();
      // some SQL calls
      // manipulate the result set
      // extract data from the result set
      // assuming login is valid...
      return new LoginToken(username);
    }
    catch(Exception e) {
      throw new RuntimeException("Error looking up DataSource",
                                  e);
    }
  }
}

We ripped out the database calls from the servlet into this new class, which takes in the parameters from the request. Note that this new class is quite testable—we can create an instance of it without any servlet or container dependency.

When we try to do so, Listing 3-20 shows the next container dependency.

Listing 3-20. Lookup code for the data source

new InitialContext().lookup("jdbc/MyDS");

This works only in an environment where JNDI is already configured. While this is easily doable in the test (as we'll see in Chapter 4), it's another tangential concern that we shouldn't need to worry about for the purposes of our test. The solution here is to switch from the Service Locator pattern (where objects look up their dependency) to a Dependency Injection pattern. This is achieved by adding a DataSource property (a field with a getter/setter pair) in our UserManager and having the caller set it.

In terms of database interaction, our test right now is good enough. There's no need to try to abstract away the database. We could employ any of the approaches we discussed earlier for handling database connectivity inside of tests. We'll focus the remainder of our discussion on the role of refactoring and how to identify abstractions through testing.

The test still doesn't quite handle all of the functionality we require. The servlet still has to take care of populating the environment (the HTTP session) with the right information. Whether this is a problem or not depends on our use case. Do we expect the implementation of the login method to require more information from its environment in the future? Are we expecting that components other than the servlet will need to invoke LoginManager?

If we don't, then we're done, and we can stop refactoring now and start testing. However, assuming we do need to worry about the environment and abstracting that away, how do we achieve that?

The key issue here is that we're not really doing anything special with the HTTP session; we're simply treating it as a map of contextual information. It's not type safe, and pretty much anything can read attributes and dump others in. Ideally, we'd like to just pass in the session to the login method, but that would impose a servlet API burden.

Since all we do is use it as a map, we could instead add a Map parameter to the login method. This allows the implementation to manipulate the contents as needed. Listing 3-21 shows the new method signature.

Listing 3-21. Refactored signature of the login method

public LoginToken login(Map session, String username,
                         String password)

The method could even return void and deal with putting the login token in the session map itself.

However, this approach presents a problem for the servlet. How does it deal with this map and ensure that it corresponds to the session map? In an ideal world, HttpSession would implement Map, or we'd be able to make it extend Map as well; neither is realistic at this point, so instead we create a proxy wrapper that handles all the messy work of synchronizing the contents for us.

The proxy in Listing 3-22 is a map implementation that wraps the javax.servlet.HttpSession object.

Listing 3-22. HttpSession-backed Map implementation

public class SessionMap extends AbstractMap {
  private final HttpSession session;
  private Set entries;

  public SessionMap(HttpSession s) {
    this.session = s;
  }

  public Set entrySet() {
    if(entries == null) {
      entries = new HashSet();
      // loop over session attribute names
      // create new Map.Entry anonymous inner class
      // with attribute name/value
      // ensure setValue on the entry modifies underlying session
      // add to entries
    }
    return entries;
  }

  public Object put(Object key, Object value) {
    entries = null;
    Object originalValue =
  session.getAttribute(key.toString());
    session.setAttribute(key.toString(), value);
    return originalValue;
  }

  public Object get(Object key) {
    return session.getAttribute(key.toString());
  }

  public Object remove(Object key) {
    entries = null;

    Object value = get(key);
    session.removeAttribute(key.toString());

    return value;
  }

  public void clear() {
    entries = null;
    session.invalidate();
  }
}

The entrySet() method is not implemented here (since it's quite verbose), but the idea is more important than the details.

Using the SessionMap class, our servlet can now easily pass in a map to the LoginManager, which can then make any reads/writes to the map that will be automatically reflected in the underlying HTTP session. This change now means that during testing, we can easily have the test supply a HashMap instead of the SessionMap, populate it with any values that need to be tested, and make assertions about its contents after invoking the LoginManager.

It can certainly be debated whether the SessionMap refactoring is the right approach or not; one could argue that we're mixing concerns here and having the login method do too much work by manipulating a map. It's also possible to argue that the loss of typing and weaker contract for callers means that it's more error prone. These are valid arguments, but the point of this exercise is to highlight an approach, not to specify a particular solution.

This example shows the power of refactoring as a testing aid and illustrates how even seemingly awkward APIs that need to be interacted with in complex ways can be abstracted away. The abstraction means that during testing, we can use simpler objects that are trivial to construct and thus test. A side effect of increasing testability is an improvement in the design. Refactoring for testability helps identify roles and concerns in a practical "in the code" way that's often much harder to spot when drawing design diagrams.

While the wrapper we created is verbose, it's something that we would write just once, and from then on it would become a valuable tool that could be reused many times. The time invested in writing it will pay off quickly in better code that is more testable.

An In-Container Approach

Servlets specifically can have an interesting in-container testing mechanism using the servlet API. This mechanism is Servlet Filters. It is possible, for example, to specify a test filter that can act on any requests to the servlet.

This approach allows us to noninvasively test a given servlet. The filter has access to the request before the servlet is invoked, so it can verify that the right parameters are specified. It can also interact with the request and response after the servlet has manipulated them and so is able to also make assertions about the state of the request, response, or session.

While this might not be as convenient from a testing standpoint as the earlier refactoring, it nevertheless introduces an important tool in our testing toolbox: the callback approach. Many APIs allow for custom code to be run before or after a component is invoked. Tests can take advantage of this mechanism and can thus get into locations where it's often difficult or tricky to have test code. A filter's access to HttpServletRequest and HttpServletResponse is one example of this since both of these objects are difficult to get outside of the servlet environment.

InformIT Promotional Mailings & Special Offers

I would like to receive exclusive offers and hear about products from InformIT and its family of brands. I can unsubscribe at any time.

Overview


Pearson Education, Inc., 221 River Street, Hoboken, New Jersey 07030, (Pearson) presents this site to provide information about products and services that can be purchased through this site.

This privacy notice provides an overview of our commitment to privacy and describes how we collect, protect, use and share personal information collected through this site. Please note that other Pearson websites and online products and services have their own separate privacy policies.

Collection and Use of Information


To conduct business and deliver products and services, Pearson collects and uses personal information in several ways in connection with this site, including:

Questions and Inquiries

For inquiries and questions, we collect the inquiry or question, together with name, contact details (email address, phone number and mailing address) and any other additional information voluntarily submitted to us through a Contact Us form or an email. We use this information to address the inquiry and respond to the question.

Online Store

For orders and purchases placed through our online store on this site, we collect order details, name, institution name and address (if applicable), email address, phone number, shipping and billing addresses, credit/debit card information, shipping options and any instructions. We use this information to complete transactions, fulfill orders, communicate with individuals placing orders or visiting the online store, and for related purposes.

Surveys

Pearson may offer opportunities to provide feedback or participate in surveys, including surveys evaluating Pearson products, services or sites. Participation is voluntary. Pearson collects information requested in the survey questions and uses the information to evaluate, support, maintain and improve products, services or sites, develop new products and services, conduct educational research and for other purposes specified in the survey.

Contests and Drawings

Occasionally, we may sponsor a contest or drawing. Participation is optional. Pearson collects name, contact information and other information specified on the entry form for the contest or drawing to conduct the contest or drawing. Pearson may collect additional personal information from the winners of a contest or drawing in order to award the prize and for tax reporting purposes, as required by law.

Newsletters

If you have elected to receive email newsletters or promotional mailings and special offers but want to unsubscribe, simply email information@informit.com.

Service Announcements

On rare occasions it is necessary to send out a strictly service related announcement. For instance, if our service is temporarily suspended for maintenance we might send users an email. Generally, users may not opt-out of these communications, though they can deactivate their account information. However, these communications are not promotional in nature.

Customer Service

We communicate with users on a regular basis to provide requested services and in regard to issues relating to their account we reply via email or phone in accordance with the users' wishes when a user submits their information through our Contact Us form.

Other Collection and Use of Information


Application and System Logs

Pearson automatically collects log data to help ensure the delivery, availability and security of this site. Log data may include technical information about how a user or visitor connected to this site, such as browser type, type of computer/device, operating system, internet service provider and IP address. We use this information for support purposes and to monitor the health of the site, identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents and appropriately scale computing resources.

Web Analytics

Pearson may use third party web trend analytical services, including Google Analytics, to collect visitor information, such as IP addresses, browser types, referring pages, pages visited and time spent on a particular site. While these analytical services collect and report information on an anonymous basis, they may use cookies to gather web trend information. The information gathered may enable Pearson (but not the third party web trend services) to link information with application and system log data. Pearson uses this information for system administration and to identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents, appropriately scale computing resources and otherwise support and deliver this site and its services.

Cookies and Related Technologies

This site uses cookies and similar technologies to personalize content, measure traffic patterns, control security, track use and access of information on this site, and provide interest-based messages and advertising. Users can manage and block the use of cookies through their browser. Disabling or blocking certain cookies may limit the functionality of this site.

Do Not Track

This site currently does not respond to Do Not Track signals.

Security


Pearson uses appropriate physical, administrative and technical security measures to protect personal information from unauthorized access, use and disclosure.

Children


This site is not directed to children under the age of 13.

Marketing


Pearson may send or direct marketing communications to users, provided that

  • Pearson will not use personal information collected or processed as a K-12 school service provider for the purpose of directed or targeted advertising.
  • Such marketing is consistent with applicable law and Pearson's legal obligations.
  • Pearson will not knowingly direct or send marketing communications to an individual who has expressed a preference not to receive marketing.
  • Where required by applicable law, express or implied consent to marketing exists and has not been withdrawn.

Pearson may provide personal information to a third party service provider on a restricted basis to provide marketing solely on behalf of Pearson or an affiliate or customer for whom Pearson is a service provider. Marketing preferences may be changed at any time.

Correcting/Updating Personal Information


If a user's personally identifiable information changes (such as your postal address or email address), we provide a way to correct or update that user's personal data provided to us. This can be done on the Account page. If a user no longer desires our service and desires to delete his or her account, please contact us at customer-service@informit.com and we will process the deletion of a user's account.

Choice/Opt-out


Users can always make an informed choice as to whether they should proceed with certain services offered by InformIT. If you choose to remove yourself from our mailing list(s) simply visit the following page and uncheck any communication you no longer want to receive: www.informit.com/u.aspx.

Sale of Personal Information


Pearson does not rent or sell personal information in exchange for any payment of money.

While Pearson does not sell personal information, as defined in Nevada law, Nevada residents may email a request for no sale of their personal information to NevadaDesignatedRequest@pearson.com.

Supplemental Privacy Statement for California Residents


California residents should read our Supplemental privacy statement for California residents in conjunction with this Privacy Notice. The Supplemental privacy statement for California residents explains Pearson's commitment to comply with California law and applies to personal information of California residents collected in connection with this site and the Services.

Sharing and Disclosure


Pearson may disclose personal information, as follows:

  • As required by law.
  • With the consent of the individual (or their parent, if the individual is a minor)
  • In response to a subpoena, court order or legal process, to the extent permitted or required by law
  • To protect the security and safety of individuals, data, assets and systems, consistent with applicable law
  • In connection the sale, joint venture or other transfer of some or all of its company or assets, subject to the provisions of this Privacy Notice
  • To investigate or address actual or suspected fraud or other illegal activities
  • To exercise its legal rights, including enforcement of the Terms of Use for this site or another contract
  • To affiliated Pearson companies and other companies and organizations who perform work for Pearson and are obligated to protect the privacy of personal information consistent with this Privacy Notice
  • To a school, organization, company or government agency, where Pearson collects or processes the personal information in a school setting or on behalf of such organization, company or government agency.

Links


This web site contains links to other sites. Please be aware that we are not responsible for the privacy practices of such other sites. We encourage our users to be aware when they leave our site and to read the privacy statements of each and every web site that collects Personal Information. This privacy statement applies solely to information collected by this web site.

Requests and Contact


Please contact us about this Privacy Notice or if you have any requests or questions relating to the privacy of your personal information.

Changes to this Privacy Notice


We may revise this Privacy Notice through an updated posting. We will identify the effective date of the revision in the posting. Often, updates are made to provide greater clarity or to comply with changes in regulatory requirements. If the updates involve material changes to the collection, protection, use or disclosure of Personal Information, Pearson will provide notice of the change through a conspicuous notice on this site or other appropriate way. Continued use of the site after the effective date of a posted revision evidences acceptance. Please contact us if you have questions or concerns about the Privacy Notice or any objection to any revisions.

Last Update: November 17, 2020