Technical Blog

3 Posts authored by: Tony McAffee

Creating unit tests for Spring based web objects such as Controllers,  Filters and Servlets is straightforward when using Spring's Mock  objects.  Spring provides a number of Mock objects which support key  J2EE interfaces, such as MockHttpServletRequest,  MockHttpServletResponse, MockHttpSession, MockFilterConfig and  MockServletConfig. These mock objects allow tests to manipulate and  control many aspects of these interfaces. As such, they are not merely  proxied interfaces, but have enough internal functionality to simulate  these important interfaces to allow for both initialization, with  respect to MockHttpServletRequest and MockHttpSession, and response data  verification, with respect to MockHttpServletResponse.

 

This post will focus on providing concrete examples of how to use these objects by examining existing Elastic Path test classes.

 

EditAccountFormControllerImpl

 

For  the first example we will go through the some test cases necessary to  test an existing form controller, EditAccountFormControllerImpl. There  are two aspects of this controller we should test, the population of the  form backing object and the account update on submit. Spring's Mock objects will help in both cases by setting up the session with a mocked  shopping cart.

 

Form Backing Object Test (Setting request attributes)

The  following test class makes use of MockHttpSession to hold a reference  to a mocked shopping cart from which the Customer object will be  obtained and used to populate the EditAccountFormBean whose values will  be asserted.

 

     /**
      * Tests that the form backing object returns the right customer data in
      * the form bean.
      */
     @Test
     public void testFormBackingObjectSetsFormBean() {
          MockHttpServletRequest request = new MockHttpServletRequest();
          request.getSession().setAttribute(WebConstants.SHOPPING_CART, cart);
          context.checking(new Expectations() {
               {
                    oneOf(customer).getFirstName(); will(returnValue(FIRST_NAME));
                    oneOf(customer).getLastName(); will(returnValue(LAST_NAME));
                    oneOf(customer).getEmail(); will(returnValue(EMAIL));
                    oneOf(customer).getPhoneNumber(); will(returnValue(PHONE_NUMBER));
               }
          });
          
          Object object = controller.formBackingObject(request);
          assertTrue("Form backing object is not instance of EditAccountFormBean", 
                    object instanceof EditAccountFormBean);
          EditAccountFormBean bean = (EditAccountFormBean) object;
          assertEquals("First name does not match", FIRST_NAME, bean.getFirstName());
          assertEquals("Last name does not match", LAST_NAME, bean.getLastName());
          assertEquals("Email does not match", EMAIL, bean.getEmail());
          assertEquals("Phone number does not match", PHONE_NUMBER, bean.getPhoneNumber());
     }

 

On Submit Test (Testing for errors)

In  this test we will again use the Spring mock objects to setup the  session with the shopping cart in order to test submitting a duplicate  email address.  We also make use of a BindException to capture and test  errors generated as well as inspecting the returned ModelAndView to  ensure the view is correct.

 

     /** 
      * Tests the situation where the modified email already exists.
      */
     @Test
     public void testOnSubmitWithDuplicateException() {
          MockHttpServletRequest request = new MockHttpServletRequest();
          MockHttpServletResponse response = new MockHttpServletResponse();
          request.getSession().setAttribute(WebConstants.SHOPPING_CART, cart);
          EditAccountFormBean bean = createFormBean();
          final BindException bindException = new BindException(bean, "editAccount");
          context.checking(new Expectations() {
               {
                    oneOf(customer).setFirstName(with(FIRST_NAME));
                    oneOf(customer).setLastName(with(LAST_NAME));
                    oneOf(customer).setEmail(with(EMAIL));
                    oneOf(customer).setPhoneNumber(with(PHONE_NUMBER));
                    oneOf(customerService).update(customer); will(throwException(new UserIdExistException(TEST_MSG)));
               }
          });
          ModelAndView view = controller.onSubmit(request, response, bean, bindException);
          assertEquals("View name was not form view.", FORM_VIEW, view.getViewName());
          assertTrue("Field error is missing.", bindException.hasFieldErrors(EMAIL));
     }

 

LocaleControllerImpl (Setting request headers)

 

This  example will show how to set up request parameters and a request header  in the Mock request.  It also demonstrates how to extract the URL of a  RedirectView for assertion purposes.

 

     /**
      * Tests setting locale, currency and referrer parameters.
      * 
      * @throws Exception a controller exception
      */
     @Test
     public void testSettingLocaleParameters() throws Exception {
          final MockHttpServletRequest request = new MockHttpServletRequest();
          final MockHttpServletResponse response = new MockHttpServletResponse();

          request.getSession().setAttribute(WebConstants.SHOPPING_CART, cart);
          request.addParameter(WebConstants.LOCALE_ID, Locale.CANADA.toString());
          request.addParameter(WebConstants.CURRENCY, Currency.getInstance(Locale.CANADA).getCurrencyCode());
          request.addHeader("Referer", REFERER_URL);

          context.checking(new Expectations() {
               {
                    allowing(requestHelper).getShoppingCart(request); will(returnValue(cart));
                    allowing(requestHelper).setShoppingCart(request, cart);

                    oneOf(cart).setLocale(Locale.CANADA);
                    oneOf(cart).setCurrency(Currency.getInstance(Locale.CANADA));
                    oneOf(shoppingCartService).saveOrUpdate(cart); will(returnValue(cart));
                    oneOf(requestHelper).updateSessionLocale(request, response, Locale.CANADA);
               }
          });

          ModelAndView view = controller.handleRequestInternal(request, response);
          assertTrue("View is not redirect view", view.getView() instanceof RedirectView);
          assertEquals("Redirect url is not correct.", REFERER_URL, ((AbstractUrlBasedView) view.getView()).getUrl());
     }

TargetUrlTaggerTest (MockHttpServletRequest constructor)

 

The  following test demonstrates that the MockHttpServletRequest provides  convenience constructors and in this case it accepts the type of request  and the request URI.  In addition this test sets the query string and  address information.  The MockHttpServletRequest allows for a large  degree of control over the request and can be used for testing any  object that requires an HttpServletRequest parameter, not just Spring  controllers.

 

     /**
      * Tests if the execute method puts the TARGET_URL into the tag set.
      */
     @Test
     public void testExecutePutsTheTargetURLIntoTagSet() {
          MockHttpServletRequest request = new MockHttpServletRequest("GET", "/index.ep");
          request.setQueryString("coupon=go");
          request.setLocalAddr("demo.elasticpath.com");
          request.setLocalPort(Integer.parseInt("8080"));
          
          targetUrlTagger.execute(session, request);
          
          StringBuffer requestURL = request.getRequestURL();
          requestURL.append(QUESTION_MARK);
          requestURL.append(request.getQueryString());

          assertEquals(1, tagSet.getTags().size());
          assertEquals(tagSet.getTagValue("TARGET_URL").getValue(), requestURL.toString());
     }

StoreSelectionFilterTest (Using MockHttpServletResponse and MockFilterChain)

 

The testFilterWithNoCandidates method uses the MockFilterChain simply as a means for invoking the filter.doFilter method.  The MockFilterChain object merely asserts that the  request/response objects are not null.  It also makes use of the  response mock object to verify that the response status and content  types have been set correctly.

     @Before
     public void setUp() throws Exception {
          request = new MockHttpServletRequest();
          response = new MockHttpServletResponse();
          filterChain = new MockFilterChain();
                ...
        }

       /**
      * Test that the filter with no selection candidates returns an error in the response.
      */
     @Test
     public void testFilterWithNoCandidates() {
          try {
               filter.doFilter(request, response, filterChain);
          } catch (IOException e) {
               fail(UNEXPECTED_IO_ERROR + e);
          } catch (ServletException e) {
               fail(UNEXPECTED_SERVLET_EXCEPTION + e);
          }
          assertEquals("Reponse should return 200 OK", HttpServletResponse.SC_OK, response.getStatus());
          assertEquals("Reponse should be empty", 0, response.getContentLength());
     }

Other Spring Testing Objects

 

We  have looked at the Mock objects within Spring's  org.springframework.mock.web package, but Spring also provides Mock  objects for testing JNDI and Portlets.  In addition, Spring also  provides the ModelAndViewAssert class which provides convenience  methods to test model attribute types and values.  The methods on this  class can be accessed statically or by extending the test class from AbstractModelAndViewTests.

 

It  must also be mentioned that although outside the scope of this  document, Spring also provides a wealth of testing classes available for  integration testing.  This could be the subject of a future article.

0 Comments Permalink

One of the pain points that can arise during a development project is the payment gateway's support of recycled order numbers. Live production gateways will track all historical order numbers and generate a duplicate order number error if an existing order number is reused. This is fine in production but troublesome when the payment gateway test servers also behave in this manner. Most payment gateway providers I have worked with in the past offer duplicate checks within an hour or two on their test servers, but beyond that, order numbers can be recycled which is a good thing for development purposes. Some providers, however, appear to extend this duplicate check to a day, week, or may even provide no support for order number reuse.


If you use the database population scripts that come with Elastic Path to facilitate schema/data changes, this limitation can frustrate your development team, who will most likely end up manually tracking used order numbers and making frequent updates to TORDERNUMBERGENERATOR.


If your payment gateway provider doesn't support order number reuse or at least not to the frequency you desire, what can you do?

 

Some Alternatives

The simplest approach would be to externalize the initial order number to the env.config and assign everyone a unique order number block. The problem with this is that each environment would have to keep track of the last order number used and update the env.config with the next number in the block with every new database build.


Another approach would be to externalize the initial order number (or perhaps even the order number incrementer) to a central database. This database would have a simple mapping of environment to next order number and each environment would have a sufficient block assigned to last the duration of the project. The problem with this approach is that many teams/environments are distributed and you may not be able to set up a central database which can be accessed by all environments.

 

A Better Solution

A better solution (though not 100% foolproof) is to construct the initial order number during the database build process using a unique element of the environment as well as a random element to get around frequent rebuilds on the same environment. To do this, we can create a custom Ant task that takes a fixed component (derived from either a fixed prefix or the environment's IP address) and appends a randomly generated number. If the random number is sufficiently large, the likelihood of duplicate hits will be minimal.

 

Creating the Ant Task

The following is an example of a custom Ant task that accepts min and max parameters for the random number component and a prefix. Regardless of whether a fixed prefix or an environment's IP address was provided, the task will use the hash code of the fixed value for the resulting order number in order to work with the out of the box numerical order number incrementer.

public class OrderNumberGeneratorTask  extends Task {
    private String min = null;
    private String max = null;
    private String property = null;
    private String prefix = null;
    private Random random = new Random(System.currentTimeMillis());
 
    @Override
    public void execute() throws BuildException {
         String localPrefix = null;
        if (min == null || min.equals(""))
            throw new BuildException("Min property is missing.");
 
        if (max == null || max.equals(""))
            throw new BuildException("Max property is missing.");
 
        int minInt = Integer.parseInt(min);
        int maxInt = Integer.parseInt(max);
 
        if (minInt > maxInt)
            throw new BuildException("Min is bigger than max.");
 
        localPrefix = prefix;
        if (localPrefix == null) {
             try {
                  localPrefix = InetAddress.getLocalHost().getHostAddress();
               } catch (UnknownHostException e) {
                    throw new BuildException("Unable to get local host address.", e);
               }
        }
        int randomInt = calculateRandom(minInt, maxInt);
 
        getProject().setNewProperty(property, String.valueOf(localPrefix.hashCode()) + String.valueOf(randomInt));
    }
 
    protected int calculateRandom(final int minInt, final int maxInt) {
        return minInt + random.nextInt(maxInt - minInt + 1);
    }

 

Updating the Build Process

To use this task, we need to package the compiled class in a jar and add it to the Maven repository along with an entry in the ant/setup/pom.xml so that it gets included in the ant setup script. Once the class is available on the Ant class path, we add the following task definition and task invocation to the ant/default.xml setup file. This will create the order number and store it in the ep.initial.order.number variable.

<?xml version="1.0" encoding="UTF-8"?>

<project name="ant_default" xmlns:artifact="antlib:org.apache.maven.artifact.ant" xmlns:contrib="antlib:net.sf.antcontrib">

  <taskdef name="ordernumbergenerator" classname="com.elasticpath.antextension.OrderNumberGeneratorTask"></taskdef>
... 
  <ordernumbergenerator min="1" max="1000000" property="ep.initial.order.number"></ordernumbergenerator>
...

 

Now we can use this variable in the database/src/base-insert.xml.vm file as the initial order number of the TORDERNUMBERGENERATOR table.

     <Tordernumbergenerator Uidpk="1" NextOrderNumber="${ep_initial_order_number}" />
1 Comments Permalink

Automated Import Jobs

Posted by Tony McAffee Mar 3, 2009

Elastic Path Commerce includes out-of-the-box import tools, which can add or update products, inventories, prices, etc. Unfortunately, import job execution is manual and requires user effort every time the import is to be run. Fortunately, automating the execution process is a fairly straightforward customization.

 

For example, assume that you want to run an import job to import inventory from an ERP system every day at three in the morning. The following high level steps will be necessary to meet this requirement.

 

  1. Create an import job.
  2. Expose the import job execution facility for ease of configuration.
  3. Define and configure a quartz job for automation.
  4. Schedule the data file transfer via FTP.

 

Creating an import job

 

Before you can run an import job, either manually or automatically, you need to define the import job in Commerce Manager.You need to map the columns of the CSV data files to data fields in Elastic Path. This is documented in the Commerce Manager User Guide, which you can get from the Elastic Path documentation site.

 

Exposing the import job execution facility

 

Let's define a class called AutomatedImportServiceImpl, which will serve as our interface to the import job facility. This class will have a few attributes and a trigger method. The following snippet illustrates the heart of the customization.

 

    private ImportService importService;
    private CmUserService cmUserService;
    private String importJobName;
    private String importJobOwnerUsername;

       public void triggerImport() {
        CmUser cmUser = cmUserService.findByUserName(importJobOwnerUsername);
        final ImportJob importJob = importService.findImportJob(importJobName);
        importJob.setCmUser(cmUser);    
          importService.runImportJob(importJob);
       }

 

The importService and cmUserService attributes will be injected by Spring. The importJobName and importJobOwnerName properties can be configured to allow different import jobs. The actual code required to invoke the import job is quite minimal as can be seen in the triggerImport method. (Note that the cmUser lookup is necessary because part of the job post processing is to email a status report to the user running the job.)

 

The next step is to define the Spring service. The best place to do this is in the cmserver web project's serviceCM.xml Spring configuration file.

 

    <bean id="updateInventoryService" class="com.example.service.dataimport.impl.AutomatedImportServiceImpl">
        <property name="importService">
            <ref bean="importService" />
        </property>
        <property name="cmUserService">
            <ref bean="cmUserService" />
        </property>
        <property name="importJobName">
            <value>UpdateInventory</value>
        </property>
        <property name="importJobOwnerUsername">
            <value>admin</value>
        </property>
    </bean>

 

Additional services could be defined with different import job names to automate other import jobs.

 

Defining the quartz job

 

The quartz job configuration is straightforward. Define a quartz job to invoke the service previously created along with a trigger to activate the quartz job.

 

    <bean id="updateInventoryJob" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
        <property name="targetObject">
            <ref bean="update
InventoryService" />
        </property>
        <property name="targetMethod">
            <value>triggerImport</value>
        </property>
        <property name="concurrent">
            <value>false</value>
        </property>
    </bean>
 
          <bean id="update
InventoryTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
        <property name="jobDetail">
            <ref bean="
updateInventoryJob" />
        </property>
        <property name="cronExpression">
            <value>0 0 3 * * ?</value>
        </property>
    </bean>

 

Scheduling the data file transfer via FTP

 

When an import job is executed, the data file needs to be transfered to the assets/import directory on the server. When an import job is executed manually, the Commerce Manager takes the user specified data file and sends it to server, usually via FTP. Your automated import process needs to do this as well. One way to do this by creating a cron job on the ERP system to FTP the data file before the import job is scheduled to run.

 

Other considerations

 

Before implementing an automated import process, the following should be taken into consideration:

 

  • Automated jobs should usually be scheduled during low-activity periods to avoid affecting the performance of the cmserver web application.
  • Failure conditions are not well represented in the example. What should happen if the FTP fails?  Should the import job be deleted after processing to prevent reprocessing?  If the import fails should an email be sent to an administrator?  Beefing up AutomatedImportServiceImpl to be smarter about failure conditions would be a wise thing to do.
  • Logging is always a good thing. Think about logging start/end processing times and perhaps even number of records processed.
  • Other automation techniques can be applied. For instance, instead of quartz, a polling method could be implemented such that files are processed as soon as they are available.
0 Comments 0 References Permalink