- 动机
- 在此步骤中,您将学习如何编写一个 Facade 使用TDD(Test Driven Development 测试驱动开发)。
- 一个 Facade 的作用,可以定义如下:
- Facade 提供商务级别的方法给客户端,隐藏 Service 的任何实现细节
- 它将调用转发到相应的 Service.
- 并从Service中返回到一个数据传输对象包数据。
- 下图显示了StadiumFacades在Trail上使用的其他元素的交互方式:
- 在这条古道的步骤,我们创建了一个集成测试的 Stadium Facade,解决可能出现并最终使测试通过任何潜在的问题。
----------------------------------------------------------------------------------
- 任务/讨论
- Facades 背景
- 创建数据传输对象
- 编写集成测试
- 运行测试 - 尝试1
- 编写这个接口
- 运行测试 - 尝试2
- 编写实现
- 运行测试 - 尝试3
- 编写单元测试
----------------------------------------------------------------------------------
- Facades 背景
The intent of a Facade is to "Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.": see The Facade Design Pattern. In our case, the Facade 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 Facade on the server that itself calls those 3 methods on the 3 services, rather than to call the 3 services itself. And since the Facade is making the calls, we can also ask the Facade 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 facade 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.
- 创建数据传输对象
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.
其主要优点是,你可以合并属性在多个扩展 - 以同样的方式,因为它是可能的类型定义。通过这样做,你让你的外观层容易扩展。
- 添加 cuppytrail/resources/cuppytrail-beans.xml 文件
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> ...
运行ant clean all 并刷新工作空间。看看所创建的数据对象类:StadiumData和MatchSummaryData。
- 编写集成测试
- 创建以下集成测试:
cuppytrail/testsrc/de/hybris/platform/cuppytrail/facades/impl/DefaultStadiumFacadeIntegrationTest.java |
package de.hybris.platform.cuppytrail.facades.impl; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import de.hybris.platform.cuppytrail.data.StadiumData; import de.hybris.platform.cuppytrail.facades.StadiumFacade; import de.hybris.platform.cuppytrail.model.StadiumModel; import de.hybris.platform.servicelayer.ServicelayerTransactionalTest; import de.hybris.platform.servicelayer.exceptions.UnknownIdentifierException; import de.hybris.platform.servicelayer.model.ModelService; import java.util.List; import javax.annotation.Resource; import org.junit.Before; import org.junit.Test; /** * This test file tests and demonstrates the behavior of the StadiumFacade's methods getAllStadiums and getStadium. */ public class DefaultStadiumFacadeIntegrationTest extends ServicelayerTransactionalTest { @Resource private StadiumFacade stadiumFacade; @Resource private ModelService modelService; private StadiumModel stadiumModel; private final String STADIUM_NAME = "wembley"; private final Integer STADIUM_CAPACITY = Integer.valueOf(90000); @Before public void setUp() { // This instance of a StadiumModel will be used by the tests stadiumModel = new StadiumModel(); stadiumModel.setCode(STADIUM_NAME); stadiumModel.setCapacity(STADIUM_CAPACITY); } /** * Tests exception behavior by getting a stadium which doesn't exist */ @Test(expected = UnknownIdentifierException.class) public void testInvalidParameter() { stadiumFacade.getStadium(STADIUM_NAME); } /** * Tests exception behavior by passing in a null parameter */ @Test(expected = IllegalArgumentException.class) public void testNullParameter() { stadiumFacade.getStadium(null); } /** * Tests and demonstrates the Facade's methods */ @Test public void testStadiumFacade() { List<StadiumData> stadiumListData = stadiumFacade.getStadiums(); assertNotNull(stadiumListData); final int size = stadiumListData.size(); modelService.save(stadiumModel); stadiumListData = stadiumFacade.getStadiums(); assertNotNull(stadiumListData); assertEquals(size + 1, stadiumListData.size()); assertEquals(STADIUM_NAME, stadiumListData.get(size).getName()); assertEquals(STADIUM_CAPACITY.toString(), stadiumListData.get(size).getCapacity()); final StadiumData persistedStadiumData = stadiumFacade.getStadium(STADIUM_NAME); assertNotNull(persistedStadiumData); assertEquals(STADIUM_NAME, persistedStadiumData.getName()); assertEquals(STADIUM_CAPACITY.toString(), persistedStadiumData.getCapacity()); } }
- 运行测试 - 尝试1
该测试将无法编译,因为它需要StadiumFacade接口
- 编写这个接口
- 创建接口
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(); }
- 运行测试 - 尝试2
- 运行测试
- 该测试将失败报告没有 bean 为“stadiumFacade”的定义
- 我们将在下面编写的实施。
- 编写实现
创建文件cuppytrail/src/de/hybris/platform/cuppytrail/facades/impl/DefaultStadiumFacade.java
cuppytrail/src/de/hybris/platform/cuppytrail/facades/impl/DefaultStadiumFacade.java |
package de.hybris.platform.cuppytrail.facades.impl; import de.hybris.platform.cuppy.model.MatchModel; import de.hybris.platform.cuppytrail.StadiumService; import de.hybris.platform.cuppytrail.data.MatchSummaryData; import de.hybris.platform.cuppytrail.data.StadiumData; import de.hybris.platform.cuppytrail.facades.StadiumFacade; import de.hybris.platform.cuppytrail.model.StadiumModel; import java.text.DateFormat; import java.util.ArrayList; import java.util.Date; import java.util.Iterator; import java.util.List; import org.springframework.stereotype.Component; import org.springframework.beans.factory.annotation.Required; public class DefaultStadiumFacade implements StadiumFacade { private StadiumService stadiumService; @Override public List<StadiumData> getStadiums() { final List<StadiumModel> stadiumModels = stadiumService.getStadiums(); final List<StadiumData> stadiumFacadeData = new ArrayList<StadiumData>(); for (final StadiumModel sm : stadiumModels) { final StadiumData sfd = new StadiumData(); sfd.setName(sm.getCode()); sfd.setCapacity(sm.getCapacity().toString()); stadiumFacadeData.add(sfd); } return stadiumFacadeData; } @Override public StadiumData getStadium(final String name) { StadiumModel stadium = null; if (name != null) { stadium = stadiumService.getStadiumForCode(name); if (stadium == null) { return null; } } else { throw new IllegalArgumentException("Stadium with name " + name + "not found"); } // Create a list of MatchSummaryData from the matches final List<MatchSummaryData> matchSummary = new ArrayList<MatchSummaryData>(); if (stadium.getMatches() != null) { final Iterator<MatchModel> matchesIterator = stadium.getMatches().iterator(); while (matchesIterator.hasNext()) { final MatchModel match = matchesIterator.next(); final MatchSummaryData summary = new MatchSummaryData(); final String homeTeam = match.getHomeTeam().getName(); final String guestTeam = match.getGuestTeam().getName(); final Date matchDate = match.getDate(); // If no goals are specified provide a user friendly "-" instead of null final String guestGoals = match.getGuestGoals() == null ? "-" : match.getGuestGoals().toString(); final String homeGoals = match.getHomeGoals() == null ? "-" : match.getHomeGoals().toString(); summary.setHomeTeam(homeTeam); summary.setGuestTeam(guestTeam); summary.setDate(matchDate); summary.setGuestGoals(guestGoals); summary.setHomeGoals(homeGoals); final String formatedDate = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT).format(matchDate); final String matchSummaryFormatted = homeTeam + ":( " + homeGoals + " ) " + guestTeam + " ( " + guestGoals + " ) " + formatedDate; summary.setMatchSummaryFormatted(matchSummaryFormatted); matchSummary.add(summary); } } // Now we can create the StadiumData transfer object final StadiumData stadiumData = new StadiumData(); stadiumData.setName(stadium.getCode()); stadiumData.setCapacity(stadium.getCapacity().toString()); stadiumData.setMatches(matchSummary); return stadiumData; } @Required public void setStadiumService(final StadiumService stadiumService) { this.stadiumService = stadiumService; } }
- 添加spring配置
cuppytrail/resources/cuppytrail-spring.xml |
<alias name="defaultStadiumFacade" alias="stadiumFacade"/> <bean id="defaultStadiumFacade" class="de.hybris.platform.cuppytrail.facades.impl.DefaultStadiumFacade"> <property name="stadiumService" ref="stadiumService"></property> </bean>
- 运行测试 - 尝试3
- 运行测试
- 该测试现在应该通过。
- 通过Debug调用Service,看到Facade在行动。
- 编写单元测试
- 创建cuppytrail/testsrc/de/hybris/platform/cuppytrail/facades/impl/DefaultStadiumFacadeUnitTest.java
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 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 testGetAllStadium() { /** * 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(); 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 getStadiums is called. This will be a list of two StadiumModels. */ final StadiumModel wembley = new StadiumModel(); wembley.setCode(STADIUM_NAME); wembley.setCapacity(STADIUM_CAPACITY); // 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()); } }
- 重点提示:
- 通过Mockito出其上的Facade所依赖的服务,我们能够孤立测试Facade。
- 下面列出的Cuppy Facade测试是学习TDD最佳实践一个很好的资源。
- 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
-总结
在这一步,你应该已经学会:
- 如何开发一个Facade和DTO与TDD。
- mock对象和 stubs ,以及它们如何相互配合之间的差异。
- 阅读材料和cuppy源代码可供学习更多。