10010---Trail ~ Testing the Façades

Motivation

In this step you will learn how to write a façade using TDD (Test Driven Development).
The role of a Façade can be defined as follows:

  • A Façade provides business level methods to the client, hiding any implementation details of services
  • It forwards calls to the appropriate services
  • and packs data returned from the services into a data transfer object.

The following diagram shows how StadiumFacades interacts with other elements used in the trail:

In this trail step, we create an integration test for the Stadium Façade, 

resolve any potential problems that may arise and finally make the test pass.

Background on Façades

The intent of a Façade is to "Provide a unified interface to a set of interfaces in a subsystem. The Façade pattern defines a higher-level interface that makes the subsystem easier to use.": see The Façade Design Pattern. In our case, the Façade is the front-most API to which the client (web-pages) has access.

Consider for example a rich client in which the communication between the client and server should be kept to a minimum. If the client needs data from methodA in ServiceA, methodB in serviceB and methodC in serviceC, it would be more efficient for the client to be able to make one call to a Façade on the server that itself calls those 3 methods on the 3 services, rather than to call the 3 services itself. And since the Façade is making the calls, we can also ask the Façade to package the particular data we need (which might be somewhat duplicated in the 3 return values from the 3 services, or might not yet be complete), and to pass that back to the Client. This is the purpose of the Data Transfer Object: see Transfer Object Design Pattern and Data Transfer Object Assembler. Our façade will be performing both roles itself. You might choose to have a separate DTOAssembler in your own code (in accordance with the design principle Separation of Concerns), and we may modify this trail to also do that.

Create the data transfer objects

We create the Data objects in a declarative way, i.e. define beans and enumerations in an xml file used as input for code generating.
The main advantage is that you can
merge attributes over several extensions - in the same way as it is possible with type definitions.

 By doing so you make your façade layer easily extensible.


1.Add to  cuppytrail/resources/cuppytrail-beans.xml

    <bean class="de.hybris.platform.cuppytrail.data.MatchSummaryData">
        <description>Data object for MatchSummary which has no equivalent on the type system</description>
        <property name="guestTeam" type="String"/>
        <property name="homeTeam" type="String"/>
        <property name="homeGoals" type="String"/>
        <property name="guestGoals" type="String"/>
        <property name="date" type="java.util.Date"/>
        <property name="matchSummaryFormatted" type="String"/>
    </bean>
     
    <bean class="de.hybris.platform.cuppytrail.data.StadiumData">
        <description>Data object representing a Stadium</description>
        <property name="name" type="String"/>
        <property name="capacity" type="String"/>
        <property name="matches" type="java.util.List<de.hybris.platform.cuppytrail.data.MatchSummaryData>"/>
    </bean>
2.  Run  ant clean all  and refresh the workspace. (ant all is OK)

  Have a look at the created data object classes: StadiumData and MatchSummaryData.

Write the interface

Create the interface:

cuppytrail/src/de/hybris/platform/cuppytrail/facades/StadiumFacade.java
package de.hybris.platform.cuppytrail.facades;
 
import de.hybris.platform.cuppytrail.data.StadiumData;
 
import java.util.List;
 
 
public interface StadiumFacade
{
    StadiumData getStadium(String name);
 
    List<StadiumData> getStadiums();
 
}

Key points to note:

  • the interface is similar to the StadiumService, with the key difference that 
          the methods are returning StadiumData POJO s, rather than StadiumModel s.

Write the implementation

cuppytrail/src/de/hybris/platform/cuppytrail/facades/impl/DefaultStadiumFacade.java
And in cuppytrail's Spring configuration:

<alias name="defaultStadiumFacade" alias="stadiumFacade"/>
<bean id="defaultStadiumFacade" class="de.hybris.platform.cuppytrail.facades.impl.DefaultStadiumFacade">
  <property name="stadiumService" ref="stadiumService" />
</bean>

Write the unit test

The integration test is useful for testing and demonstrating the expected behavior of any class that implements the StadiumFacade interface. But to test this particular implementation of the interface in detail we need a unit test that mocks out the classes on which the implementation depends. Again this is done with Mockito.

Copy the following code to cuppytrail/testsrc/de/hybris/platform/cuppytrail/facades/impl/DefaultStadiumFacadeUnitTest.java

/**
 *
 */
package de.hybris.platform.cuppytrail.facades.impl;
 
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
 
import de.hybris.platform.cuppytrail.StadiumService;
import de.hybris.platform.cuppytrail.data.StadiumData;
import de.hybris.platform.cuppytrail.model.StadiumModel;
 
import java.util.ArrayList;
import java.util.List;
 
import org.junit.Before;
import org.junit.Test;
 
 
public class DefaultStadiumFacadeUnitTest
{
    private DefaultStadiumFacade stadiumFacade;
 
    private StadiumService stadiumService;
 
    private final static String STADIUM_NAME = "wembley";
    private final static Integer STADIUM_CAPACITY = Integer.valueOf(12345);
 
    // Convenience method for returning a list of Stadium
    private List<StadiumModel> dummyDataStadiumList()
    {
        final StadiumModel wembley = new StadiumModel();
        wembley.setCode(STADIUM_NAME);
        wembley.setCapacity(STADIUM_CAPACITY);
        final List<StadiumModel> stadiums = new ArrayList<StadiumModel>();
        stadiums.add(wembley);
        return stadiums;
    }
 
    // Convenience method for returning a Stadium with code and capacity set for wembley
    private StadiumModel dummyDataStadium()
    {
        final StadiumModel wembley = new StadiumModel();
        wembley.setCode(STADIUM_NAME);
        wembley.setCapacity(STADIUM_CAPACITY);
        return wembley;
    }
 
    @Before
    public void setUp()
    {
        // We will be testing the POJO DefaultStadiumFacade - the implementation of the StadiumFacade interface.
        stadiumFacade = new DefaultStadiumFacade();
 
        /**
         * The facade is expected to make calls to an implementation of StadiumService but in this test we want to verify
         * the correct behaviour of DefaultStadiumFacade itself and not also implicitly test the behaviour of a
         * StadiumService. In fact as of writing this class, we do only have the interface StadiumService and no
         * implementation. This requires that we mock out the StadiumService interface. There are several strong arguments
         * for following this practice:
         *
         * If we were to include a real implementation of StadiumService rather than mocking it out..
         *
         * 1) we will not get "false failures" in DefaultStadiumFacade due to errors in the StadiumService implementation.
         * Such errors should be caught in tests that are focusing on StadiumService instead.
         *
         * 2) The condition could arise where an error in the facade gets hidden by a complimentary error in the
         * StadiumService implementation - resulting in a "false positive".
         *
         * By mocking out the interface StadiumService..
         *
         * 3) we do not actually need an implementation of it. This therefore helps us to focus our tests on this POJO
         * before having to implement other POJOs on which it depends - allowing us to write tests early.
         *
         * 4) by focusing on the behaviour of the facade and the interfaces it uses, we are forced to focus also on the
         * those interface, improving them before writing their implementation.
         *
         *
         * Therefore we create a mock of the StadiumService in the next line.
         */
        stadiumService = mock(StadiumService.class);
        // We then wire this service into the StadiumFacade implementation.
        stadiumFacade.setStadiumService(stadiumService);
    }
 
    /**
     * The aim of this test is to test that:
     *
     * 1) The facade's method getStadiums makes a call to the StadiumService's method getStadiums
     *
     * 2) The facade then correctly wraps StadiumModels that are returned to it from the StadiumService's getStadiums
     * into Data Transfer Objects of type StadiumData.
     */
    @Test
    public void testGetAllStadiums()
    {
        /**
         * We instantiate an object that we would like to be returned to StadiumFacade when the mocked out
         * StadiumService's method getStadiums is called. This will be a list of two StadiumModels.
         */
        final List<StadiumModel> stadiums = dummyDataStadiumList();
        // create wembley stadium for the assert comparison
        final StadiumModel wembley = dummyDataStadium();
        // We tell Mockito we expect StadiumService's method getStadiums to be called, and that when it is, stadiums should be returned
        when(stadiumService.getStadiums()).thenReturn(stadiums);
 
        /**
         * We now make the call to StadiumFacade's getStadiums. If within this method a call is made to StadiumService's
         * getStadiums, Mockito will return the stadiums instance to it. Mockito will also remember that the call was
         * made.
         */
        final List<StadiumData> dto = stadiumFacade.getStadiums();
        // We now check that dto is a DTO version of stadiums..
        assertNotNull(dto);
        assertEquals(stadiums.size(), dto.size());
        assertEquals(wembley.getCode(), dto.get(0).getName());
        assertEquals(wembley.getCapacity().toString(), dto.get(0).getCapacity());
    }
 
    @Test
    public void testGetStadium()
    {
        /**
         * We instantiate an object that we would like to be returned to StadiumFacade when the mocked out
         * StadiumService's method getStadium is called. This will be the StadiumModel for wembley stadium.
         */
        // create wembley stadium
        final StadiumModel wembley = dummyDataStadium();
 
        // We tell Mockito we expect StadiumService's method getStadiumForCode to be called, and that when it is, wembley should be returned
        when(stadiumService.getStadiumForCode("wembley")).thenReturn(wembley);
 
        /**
         * We now make the call to StadiumFacade's getStadium. If within this method a call is made to StadiumService's
         * getStadium, Mockito will return the wembley instance to it. Mockito will also remember that the call was made.
         */
        final StadiumData stadium = stadiumFacade.getStadium("wembley");
        // We now check that stadium is a correct DTO representation of the ServiceModel wembley
        assertEquals(wembley.getCode(), stadium.getName());
        assertEquals(wembley.getCapacity().toString(), stadium.getCapacity());
    }
 
}

Key notes:

  • By mocking out the service on which the façade depends, we are able to test the façade in isolation.
  • With Mockito, we can mock out the service, and verify that the expected calls are made from the façade to the service interface.
  • These tests are not just useful when writing the façade, but when maintaining it on an ongoing basis - they remain in the test suite, and run as part of Continuous Integration.
  • The Cuppy Façade Tests listed below are a good resource for learning TDD best practices.
    • cuppy/web/testsrc/de/hybris/platform/cuppy/web/facades/DefaultMatchFacadeTest.java
    • cuppy/web/testsrc/de/hybris/platform/cuppy/web/facades/DefaultPlayerFacadeTest.java
    • cuppy/web/testsrc/de/hybris/platform/cuppy/web/facades/DefaultStatisticsFacadeTest.java

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值