翻译自 http://spring.io/guides/tutorials/web/2/。
首发于 http://my.oschina.net/u/179755/blog/233176。
第2步:实现URL和返回数据
我们已经决定了我们的Web域URL,将它们作为Web域组件捕获到我们的救生圈中
现在是时候来实现我们的Yummy面馆Web前端了。用Spring MVC构建服务的第一步是构建和测试一个或多个控制器,这些控制器负责处理我们在前一步骤中的定义的HTTP请求。
从一个失败的测试开始
测试驱动开发告诉我们,如果我们没有失败的测试,那么我们无需写任何代码。所以在我们深入实现服务前,我们需要创建一些测试,这些测试将驱使鼓励我们去写代码以通过这些测试。
将查询从命令中分离出来
在我们开始创建测试之前,我们需要考虑我们的服务将要响应的请求的分类。我们将编写测试,查找所有我们在第1步中所设计的HTTP交互。
这些交互可以分为3类:
读取或者查询菜单的请求
更新篮的请求
创建订单的请求
我们进一步将这些分成2类:
改变资源状态的请求(命令)
查询资源状态的请求(查询)
我们可以为每个资源创建一个控制器来实现这两类交互。但是命令-查询-责任分离模式建议我们通过应用程序来拆分这些责任到不同的路由。在本教程中,我们将分别实现这些。
在正确的层面测试
编写代码时,我们可以选择编写什么测试,和多少来隔离我们正在测试的代码。我们应用的隔离度定义了我们正在写的测试的类型。这里有三个主要的层面,单元,集成和功能。
正如Mike Cohn和Martin Fowler所说,这些组成了测试金字塔。
我们应该写尽可能多的单元测试,作为在金字塔的底层,少一点的集成测试,更少的功能测试。
用Spring MockMvc测试
Spring提供了编写金字塔各层测试的全面支持。
我们需要写一个Spring MVC控制器,它包含了众多注解来定义它自己的行为。这些行为需要测试,我们才能确定它的正确性。
Spring提供了MockMVC以作为测试方案,它允许我们编写如Martin Fowler所说的皮下测试,获得跟在一个完整的Web容器中一样的执行效果。
定义行为,创建测试
我们第一个实现的URL是“/”。它是Yummy面馆网站的根,后面称为网站URL。它将包含有效的菜单项列表,允许用户增加菜单项到篮中,提供了一个机制来存放订单。
第一件事情是让这个网站URL有效。
第一步,我们需要创建一个全新的空类,com.yummynoodlebar.web.controller.SiteController,来实现这个控制器。
在我们实现这个控制器之前,我们创建一个测试com.yummynoodlebar.web.controller.SiteIntegrationTest。
package com.yummynoodlebar.web.controller;
import com.yummynoodlebar.core.services.MenuService; import com.yummynoodlebar.events.menu.RequestAllMenuItemsEvent; import com.yummynoodlebar.web.controller.fixture.WebDataFixture; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.Matchers.any; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;
public class SiteIntegrationTest {
private static final String RESPONSE_BODY = "Yummy Noodles,Special Yummy Noodles,Low cal Yummy Noodles";
MockMvc mockMvc;
@InjectMocks SiteController controller;
MenuService menuService;
@Before public void setup() { MockitoAnnotations.initMocks(this);
mockMvc = standaloneSetup(controller).build();
when(menuService.requestAllMenuItems(any(RequestAllMenuItemsEvent.class))).thenReturn(WebDataFixture.allMenuItems());
}
public void thatTextReturned() throws Exception { mockMvc.perform(get("/")) .andDo(print()) .andExpect(content().string(RESPONSE_BODY));
}
}
|
执行该单个测试
$ ./gradlew -Dtest.single=SiteIntegrationTest test |
这个测试毫无疑问是失败的。
:compileJava :processResources UP-TO-DATE :classes :compileTestJava :processTestResources UP-TO-DATE :testClasses :test
com.yummynoodlebar.web.controller.SiteIntegrationTest > thatTextReturned STARTED
com.yummynoodlebar.web.controller.SiteIntegrationTest > thatTextReturned FAILED java.lang.AssertionError at SiteIntegrationTest.java:44
1 test completed, 1 failed :test FAILED
FAILURE: Build failed with an exception.
|
因为我们并没有实现和配置这个控制器。现在我们可以安全地实现这个控制器。
创建控制器和映射
我们正在创建一个交互的HTML网站给用户使用,但是我们还需要有一个基本的控制器返回文本给用户。
如前所述,我们需要构建控制器查询菜单项。现在,我们期望这个测试让菜单项显示在一个明文文件中,以逗号分隔。
src/main/java/com/yummynoodlebar/web/controller/SiteController.java
package com.yummynoodlebar.web.controller;
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody;
import com.yummynoodlebar.core.services.MenuService; import com.yummynoodlebar.events.menu.AllMenuItemsEvent; import com.yummynoodlebar.events.menu.MenuItemDetails; import com.yummynoodlebar.events.menu.RequestAllMenuItemsEvent;
@RequestMapping("/") public class SiteController {
private static final Logger LOG = LoggerFactory.getLogger(SiteController.class);
@Autowired private MenuService menuService;
@RequestMapping(method = RequestMethod.GET) @ResponseBody public String getCurrentMenu() { LOG.debug("Yummy Menu directly to ResponseBody"); return prettyPrint(menuService.requestAllMenuItems(new RequestAllMenuItemsEvent())); }
private String prettyPrint(AllMenuItemsEvent requestAllMenuItems) { StringBuffer sb = new StringBuffer(); String delim = ""; for (MenuItemDetails menuItemDetails : requestAllMenuItems.getMenuItemDetails()) { sb.append(delim).append(menuItemDetails.getName()); delim = ","; }
return sb.toString(); }
}
|
再次执行测试,将通过。
:compileJava :processResources :classes :compileTestJava :processTestResources UP-TO-DATE :testClasses :test
com.yummynoodlebar.web.controller.SiteIntegrationTest > thatTextReturned STARTED
com.yummynoodlebar.web.controller.SiteIntegrationTest > thatTextReturned PASSED
BUILD SUCCESSFUL
|
这个控制器目前从服务返回菜单并把它转换成了文本。
总结
祝贺。我们已经创建了一个控制器,它实现了我们网站的一部分。我们已经使用MockMVC,而不是在容器中,来测试了这个控制器,确保这个处理映射是正确的。
我们现在的救生圈图现在在Web域包含了新组件,SiteController。
整体的救生圈图将看起来如下:
下一步:部署服务。