Using the Spring AOP Framework with EJB Components
Pages: 1, 2, 3, 4, 5

Testing the Advice Code

As you may notice, your code has no dependency on TradeDao or on YahooFeed. That would let you test this component in complete isolation using mock objects testing. A mock objects testing approach allows you to declare expectations before the component execution, and then verify that these expectations are met during a component call. See the Resources section for more information about mock testing. Here you are going to use the jMock framework that provides a flexible and expressive API for declaring expectations.

It is a good idea to use the same Spring bean configuration for both test and real applications, but for testing a specific component, you can't use real dependencies because this would break component isolation. However, Spring allows you to specify a BeanPostProcessor when creating Spring's application context in order to replace selected beans and dependencies. In this case you can use a Map of mock objects that will be created in the test code and used instead of beans defined in a Spring configuration:

public class StubPostProcessor implements BeanPostProcessor {
  private final Map stubs;

  public StubPostProcessor( Map stubs) {
    this.stubs = stubs;
  }

  public Object postProcessBeforeInitialization(Object bean, String beanName) {
     
                        
if(stubs.containsKey(beanName)) return stubs.get(beanName);
    return bean;
  }

  public Object postProcessAfterInitialization(Object bean, String beanName) {
    return bean;
  }

}
                      

In the setUp() method of your test case class, you will initialize the StubPostProcessor with mock objects for the baseTradeManager and yahooFeed components created with the jMock API. Then we can create the ClassPathXmlApplicationContext (configured to use the BeanPostProcessor) to instantiate a tradeManager component. The resulting tradeManager component will use the mocked dependencies.

Such an approach not only allows you to isolate components for testing, but also ensures that advices are defined correctly in the Spring bean configuration. It is practically impossible to use anything like this to test business logic implemented in EJB components without simulating a lot of the container infrastructure:

public class ForeignTradeAdviceTest extends TestCase {
  TradeManager tradeManager;
  private Mock baseTradeManagerMock;
  private Mock yahooFeedMock;

  protected void setUp() throws Exception {
    super.setUp();

    baseTradeManagerMock = new Mock(TradeManager.class, "baseTradeManager");
    TradeManager baseTradeManager = (TradeManager) baseTradeManagerMock.proxy();
    
    yahooFeedMock = new Mock(TradeManager.class, "yahooFeed");
    TradeManager yahooFeed = (TradeManager) yahooFeedMock.proxy();

    Map stubs = new HashMap();
    stubs.put("yahooFeed", yahooFeed);
    stubs.put("baseTradeManager", baseTradeManager);
    
     
                        
ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext(CTX_NAME);
    ctx.getBeanFactory().addBeanPostProcessor(new StubPostProcessor(stubs));

    tradeManager = (TradeManager) proxyFactory.getProxy();
  }
  ...
                      

In the actual testAdvice() method, you can specify expectations for the mock objects and verify, for example, that if getPrice() on baseTradeManager returns null, then getPrice() on yahooFeed also will be called:

  public void testAdvice() throws Throwable {
    String symbol = "testSymbol";
    BigDecimal expectedPrice = new BigDecimal("0.222");

     
                        
baseTradeManagerMock.expects(new InvokeOnceMatcher()).method("getPrice")
      .with(new IsEqual(symbol)).will(
                        
new ReturnStub(null));
    
     
                        
yahooFeedMock.expects(new InvokeOnceMatcher()).method("getPrice")
      .with(new IsEqual(symbol)).will(
                        
new ReturnStub(expectedPrice));
    
     
                        
BigDecimal price = tradeManager.getPrice(symbol);
    assertEquals("Invalid price", expectedPrice, price);
        baseTradeManagerMock.verify();
    yahooFeedMock.verify();
  }
                      

This code uses jMock constraints to specify that the baseTradeManagerMock expects the method getPrice() to be invoked only once with a parameter equal to symbol, and, that it will return null from that call. Similarly, yahooFeedMock also expects a single invocation of the same method, but will return expectedPrice. This allows you to run the tradeManager component you created in the setUp() method and assert the returned result. Mocked dependencies allow you to verify that all calls to the dependent components meet your expectations.

This test case can be easily parameterized to cover all the possible cases. Notice that you can easily declare expectations when exceptions are thrown by the components:

Test baseTradeManager yahooFeed Expected
call return throw call return throw result exception
1 true 0.22 - false - - 0.22 -
2 true - e1 false - - - e1
3 true null - true 0.33 - 0.33 -
4 true null - true null - null -
5 true null - true - e2 - e2

Using this table you can update the test class to use a parametrized suite that will cover all possible scenarios:

  ...
  
  public static TestSuite suite() {
    BigDecimal v1 = new BigDecimal("0.22");
    BigDecimal v2 = new BigDecimal("0.33");
    
    RuntimeException e1 = new RuntimeException("e1");
    RuntimeException e2 = new RuntimeException("e2");
     
                        
   TestSuite suite = new TestSuite(ForeignTradeAdviceTest.class.getName());
    suite.addTest(new ForeignTradeAdviceTest(true, v1,   null, false, null, null, v1,   null));
    suite.addTest(new ForeignTradeAdviceTest(true, null, e1,   false, null, null, null, e1));
    suite.addTest(new ForeignTradeAdviceTest(true, null, null, true,  v2,   null, v2,   null));
    suite.addTest(new ForeignTradeAdviceTest(true, null, null, true,  null, null, null, null));
    suite.addTest(new ForeignTradeAdviceTest(true, null, null, true,  null, e2,   null, e2));
    return suite;
  }
  
  public ForeignTradeAdviceTest(
      boolean baseCall, BigDecimal baseValue, Throwable baseException,
      boolean yahooCall, BigDecimal yahooValue, Throwable yahooException,
      BigDecimal expectedValue, Throwable expectedException) {
     
                        
super("test");

    this.baseCall = baseCall;
    this.baseWill = baseException==null ? 
        (Stub) new ReturnStub(baseValue) : new ThrowStub(baseException);
    this.yahooCall = yahooCall;
    this.yahooWill = yahooException==null ? 
        (Stub) new ReturnStub(yahooValue) : new ThrowStub(yahooException);
    this.expectedValue = expectedValue;
    this.expectedException = expectedException;
  }
  
  public void test() throws Throwable {
    String symbol = "testSymbol";

     
                        
if(baseCall) {
      baseTradeManagerMock.expects(new InvokeOnceMatcher())
        .method("getPrice").with(new IsEqual(symbol)).will(baseWill);
    }
    
     
                        
if(yahooCall) {
      yahooFeedMock.expects(new InvokeOnceMatcher())
        .method("getPrice").with(new IsEqual(symbol)).will(yahooWill);
    }
    
    try {
      BigDecimal price = tradeManager.getPrice(symbol);
      assertEquals("Invalid price", expectedValue, price);
     
                        
} catch(Exception e) {
      if(expectedException==null) {
        throw e;
      }
    }
        baseTradeManagerMock.verify();
    yahooFeedMock.verify();
  }

  public String getName() {
    return super.getName()+" "+
      baseCalled+" "+baseValue+" "+baseException+" "+
      yahooCalled+" "+yahooValue+" "+yahooException+" "+
      expectedValue+" "+expectedException;
  }
  ...
                      

In more sophisticated cases, the above testing approach can be easily scaled to the much larger set of the input parameters, and it will still run practically in no time and will be easy to manage. Moreover, it would make sense to move all the parameters into an external config or even into the Excel spreadsheet that can be managed by the QA team or directly generated from the requirements.

Pages: 1, 2, 3, 4, 5

Next Page ยป