Even while it changes, it stands still


前言

第三章的阅读之路也算是比较漫长了,和计划的SICP进度可能还是差距很大,但是好歹也是在慢慢推进了..第三章内容算是比较杂的但是挺重要的吧,特别是在这个流和分布式被越来越重视的8102年,而且在工作之后再来看这些问题也是有很多的新收获,确实很多知识虽然过了几十年,其实本质上是没有什么变化的,而我的感觉就是SICP提供给我一种从语言的角度来看待计算机问题的角度,从而使得对很多问题的思考更加全面和多样了.

总结

在前两章的基础上,在我们构建系统的时候,还需要一些知识来使得我们在构建大型的系统时使得系统更具模块性,我们希望这种策略能够在有新的对象或者动作添加进系统的时候不需要修改原有的代码,只需要加一些额外的代码(symbolic analogs)就能够解决问题.

在我们构建系统的时候有两种看待事物的角度:

  • 第一种是把一个大型系统视为对象的集合,而对象的行为可能随时间变化
  • 第二种是像电子工程师一样把系统看作是信号处理系统,把系统看成是信号在元件之间流动.

赋值和局部状态

看待系统中状态变化的一种观点是:状态变化可以被分组成几个紧密相关的子系统并且子系统之间的关系要保持松散.这也就导致我们想到要使得每个对象都有其局部的状态(local state varibles)

引入set!

比如在取钱的时候,我们的流程应该是下面这样的:假设我们有100块钱

1
2
3
4
5
6
7
8
(withdraw 25)
75
(withdraw 25)
50
(withdraw 60)
"Insufficient funds"
(withdraw 15)
35

而这样的求值方式前面没有状态的函数肯定是做不到的了,因为每次调用相同的函数都会产生不同的结果了.这时候就需要引入赋值,也就是set!.代码的实现如下:

1
2
3
4
5
6
(define balance 100)
(define (withdraw amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient founds"))

而set!的引入也导致前面的替换模型的规则就失效了.因为balance变成了一个绑定在全局环境里的变量.

引入赋值的好处

It is tempting to conclude this discussion by saying that, by introducing assignment and the technique of hiding state in local variables, we are able to structure systems in a more modular fashion than if all state had to be manipulated explicitly, by passing additional parameters.

通过局部状态可以使得我们更加模块化的来构建系统.

引入赋值的代价

那么,代价是什么呢?

  1. 替换模型在这里不再适用了
  2. 由于引入了赋值,函数的执行顺序变得重要了,我们也失去了函数式编程的一部分优雅
  3. 我们不得不接受命令式编程一样的复杂性

programming with assignment forces us to carefully consider the relative orders of the assignments to make sure that each statement is using the correct version of the variables that have been changed.This issue simply does not arise in functional programs

求值的环境模型

为了应付set!这个需求,我们需要引入一个叫做环境的结构,环境就是一系列的帧(frames)构成的.而相对应的,环境模型的求值规则是:

  1. Evaluate the subexpressions of the combination.
  2. Apply the value of the operator subexpression to the values of the operand subexpressions.

大致就是为每一个子表达式创建一个自己的环境,然后绑定它的父表达式,子表达式内的变量自己用,求值的时候的感觉就是自底向上求值.

用帧来存储局部状态

根据上面的规则翻译一下上面从银行取钱的函数就是这样的:

1
2
3
4
5
6
7
8
9
10
(define (make-withdraw balance)
(lambda (amount)
(if (> amount balance)
"Insufficient"
(begin
(set! balance (- balance amount))
balance))))

(define W1 (make-withdraw 100))
(W1 50)

求值(W1 50)的过程如下图:

环境模型解释了使得局部过程定义变成模块化编程中实用技巧的两个关键特性:

  • 局部过程被绑定在自己的帧中,只在运行时被创建,优于全部绑定在全局变量中的方式
  • 局部过程可以轻松访问绑定环境中的参数,因为这是一个对其封闭的过程.

对可变的数据进行模块化

主要还是讲一些数据结构,但是使用了新的技术.

Mutable List Structure

主要是借助set!来实现对List进行可变的操作.下图就是把x的cdr变成y的图片(set-cdr! x y)

表示队列

队列的概念和操作就不写了,大致就是用set!来修改队首和队尾来实现一个队列.

表示表格(Tables)

key-value的形式,和字典或者JSON比较相似.

模拟数字电路

这也就是开始提到的使用电子工程师的眼光来看待问题的例子,在课程上老师也讲到了他认为电气系统是最完美的系统.

用与非门来构造一个半加器:


数字电路的优势就在于可以使用简单的模块构造出具有复杂功能的模块,然后使用复杂的模块构造出更复杂的模块,比较类似于第一章讲到pair时pair的闭包性质一样.

而数字电路设计的难点就在于信号在电路中的传输是有延迟的.

约束传播

在之前我们都把代码组织成单向的计算,而这里就举了一个可以从任何方向来求值的约束传播的例子.

而这其实也是一种新的抽象问题的方式.

并发:时间是本质问题

The central issue lurking beneath the complexity of state, sameness, and change is that by introducing assigment we forced to admit time to our computational models.

what makes this complicated is that more than one process may be trying to manipulate the shared state at the same time.

一个两个人同时从银行中取钱的例子:

使得事情变得复杂的原因就在于多个进程可以同时操作同一个共享的状态,重要的就是如何保证操作的原子性,一种解决的办法就是使用Serializer(串行化),也就是类似于锁的机制.

而一旦引入了锁也就会有deadlock(死锁)的存在(这里就略过).

并发,时间和交流

The basic phenomenon here is that synchronizing different processes, establishing shared state, or imposing an order on events requires communication among the process.

一旦涉及到并发也就涉及到了通讯的问题,也就是进程之间分享同一个状态的方式,当然这就有很多的取舍可以做了,是要大家都能同时获取状态的变化还是可以延时获取状态的变化,而这个问题到了分布式中就会变得越发的明显,而Hadoop最大的性能瓶颈其实也就是在于网络的IO.

从本质上看,在并发控制中,任何时间概念都必然与通信有内在的密切联系.有意思的是,时间与通信之间的这种联系也出现在相对论里,在那里的光速(可能用于同步事件的最快信号)是与时间和空间有关的基本常量.在处理时间和状态时,我们在计算模型领域所遭遇的复杂性,事实上,可能就是物理世界中最基本的复杂性的一种反映.

可能这就是科学的共性吧.虽然感觉有点玄乎,但是这也是SICP的魅力所在吧.

Streams(流)

streams are a clever idea that allows one to use sequence manipulations without incurring the cost of manipulating sequence as lists.

简单的说流就是延时求值,也可以说是需求驱动(demand-driven)的,就是你要一个就给你一个,Python中的yield就是这种感觉.

Infinite Streams

而使用delayforce来实现延时求值也使得我们可以做一些以前办不到的事,比如一个表示正整数的无穷流:

1
2
3
(define (integers-starting-from n)
(cons-stream n (integers-starting-from (+ n 1))))
(define integers (integers-starting-from 1))

信号系统中的流

把积分过程看作是一个信号处理系统:

1
2
3
4
5
6
(define (integral integrand initial-value dt)
(define int
(cons-stream initial-value
(add-streams (scale-stream integrand dt)
int)))
int)

函数式编程的模块化和对象的模块化

如果我们把合并请求交易模块化的话,就像下面这样:

问题在于我们每次都要接收Peter和Paul的请求才能进行一次合并,如果两个人请求的频率相差很大的话,那么频繁请求的人就会堆积很多请求不能处理,而一旦引入显式的同步来保证事件发生顺序的正确性的话,又会引入函数式编程中的一个大问题(因为显式同步引入了赋值和时间,这是函数式编程想避免的):也就是在没有时间这个属性的情况下如何去”公平”的合并两个请求.

总结

We can model the world as a collection of separate, time-bound, interacting objects with state, or we can model the world as a single, timeless, stateless unity.

我们面临的不是编程方法的挑战,而是两种不同的世界观的挑战,看待世界不同的方法导致了OO和FP两种分支,如果两个分支在将来能够融合的话是坠吼的.

你问我支不支持,我肯定是支持的

第三章的总结也到此结束了,可以说在SICP的这一段历程是玄幻的和充满乐趣的,这也是这本魔法书的魅力所在吧.而在项目中遇到的包括并发,分布式的问题也增加了对这些CS中基本的问题的思考:

吾尝终日而思矣,不如须臾之所学也

application/x-www-form-urlencoded

这是比较常用的提交数据的方式,在项目中也使用的是这一种,在MockMvc测试的参数准备时可以这样使用:

1
2
3
4
5
6
7
8
9
UrlEncodedFormEntity form = new UrlEncodedFormEntity(Arrays.asList(
new BasicNameValuePair("参数1", value1),
new BasicNameValuePair("参数2", value2),
), "utf-8");
MvcResult result = mvc.perform(MockMvcRequestBuilders.post("/test")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.content(EntityUtils.toString(form))
)
.andExpect(status().isOk()).andReturn();

主要使用的是UrlEncodedFormEntity类,要注意的是需要设置字符集否则传参的时候中文会变成???.

application/json

我使用的是用阿里开源的fastjson直接转换的形式:

1
2
3
4
5
6
7
8
9
Parm parm = new Parm(); //你要传的参数的对象
//设置参数对象的值
String requestJson = JSONObject.toJSONString(parm);
MvcResult result = mockMvc.perform(post("/softs")
.contentType(MediaType.APPLICATION_JSON)
.content(requestJson)
)
.andDo(print())
.andExpect(status().isOk()).andReturn();

multipart/form-data

这个没有使用过,就借鉴Stack Overflow上的一个答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
MockMultipartFile file = new MockMultipartFile("data", "dummy.csv",
"text/plain", "Some dataset...".getBytes());

MockMultipartHttpServletRequestBuilder builder =
MockMvcRequestBuilders.fileUpload("/test1/datasets/set1");
builder.with(new RequestPostProcessor() {
@Override
public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
request.setMethod("PUT");
return request;
}
});
mvc.perform(builder
.file(file))
.andExpect(status().ok());

get请求传参

这个虽然不是POST也很简单,但是也记录一下

1
2
3
4
5
MvcResult result = mvc.perform(MockMvcRequestBuilders.post("/v4/address/deleteAddress")
.param("参数1", value1)
.param("参数2", value2)
)
.andExpect(status().isOk()).andReturn();

也可以使用params的方式使用Map来传参,但是我还是习惯这样来初始化,因为毕竟每个参数还是需要自己设置的,这样看起来也清晰一点.

前言

国庆回来之后算是正是开始了Spring Boot的工作,而Java组里因为没有写任何的单元测试,每次测试都是靠Swagger的web页面自己填参数来测试,于是在这次新版本的迭代中我变成了第一个被要求贯彻实施每个controller都要写测试的人,不过单元测试,从我做起,真的很重要啊.

然后在选择的时候发现了MockMvc这个Spring自带的测试利器,个人认为主要的优势就在于可以直接在Java中模拟HTTP请求,而且对于模拟请求和参数的解析也是一把好手,当然就是选择它啊!,我当然是赞成的.然后在寻找资料的时候因为一开始没有看官方文档也是被搞懵了很久,在看了官方文档之后才豁然开朗,于是就为了自己,翻译一下记录一下,以防健忘的自己以后重复找轮子.

这里是文档地址:https://docs.spring.io/spring/docs/current/spring-framework-reference/testing.html#testing-introduction,因为这次只测试和服务器端的单元测试,所以文档也就翻了这部分.

Spring MVC Test Framework

Spring MVC Test框架为使用流畅的API测试Spring MVC代码提供了最高级的支持,你可以使用JUnit,TestNG或者其他任何的测试框架.它被构建在spring-test模块的Servlet API mock 对象之上,因此不使用运行中的Servlet容器.它使用DispatcherServlet来提供完整的Spring MVC运行时的行为并且除了独立的模块之外还提供了使用TestContext框架来加载实际的Spring配置的支持,也就是说你可以手动执行Controller的实例并且一次测试一个.

Spring MVC Test还使用RestTemplate来提供客户端的代码测试.客户端的测试模拟服务端的响应所以也不会使用正在运行的服务.

Spring Boot提供了一个选项来写一个完整的,端到端的包含运行中服务的集成测试.如果这是你的目标,查看Spring Boot reference page.获取更多关于容器外和端到端集成测试的区别的信息,查看Differences Between Out-of-Container and End-to-End Integration Tests

Server-Side Tests

你可以使用JUnit或者TestNG为Spring MVC controller编写一个普通的单元测试.如果要这么做的话,实例化conreoller,使用mocked或者stubbed依赖注入,然后使用它们的方法就行了(例如:MockHttpServletRequest,MockHttpServletResponse,和其他有必要用到的).然而,当你这样来写单元测试的时候,很多东西是测试不到的,例如:RequestMapping,数据绑定,类型转换,数据的合法性验证等等.此外,其他的controller方法,例如@InitBinder,@ModelAttribute@ExceptionHandler也会作为请求过程生命周期中的一部分被请求到.

Spring MVC Test的目标是提供一个高效的方式来测试controller通过执行请求和生成response通过实际的DispatcherServlet.

Spring MVC Test构建在熟悉的spring-test模块的“mock” implementations of the Servlet API之上.这也就允许我们不需要运行一个Servlet容器就可以模拟请求和生成response.在大部分情况下,一切都会像真实环境一样工作但是也有几个例外:[Differences Between Out-of-Container and End-to-End Integration Tests](### Differences Between Out-of-Container and End-to-End Integration Tests).下面的代码是基于JUnit Jupiter使用Spring MVC Test的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringJUnitWebConfig(locations = "test-servlet-context.xml")
class ExampleTests {

private MockMvc mockMvc;

@BeforeEach
void setup(WebApplicationContext wac) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}

@Test
void getAccount() throws Exception {
this.mockMvc.perform(get("/accounts/1")
.accept(MediaType.parseMediaType("application/json;charset=UTF-8")))
.andExpect(status().isOk())
.andExpect(content().contentType("application/json"))
.andExpect(jsonPath("$.name").value("Lee"));
}
}

上面的测试依赖于TestContext框架提供支持的WebApplicationContext来从位于相同的package的xml配置文件中加载Spring的配置,基于Java和Groovy的配置文件也是支持的.看这里

MockMVC实例模拟了一个GET请求了/accounts/1并且验证response的结果状态码是200,content-type是application/json,并且response body有一个JSON属性名name的key值是Lee.jsonPath的语法通过JaywayJsonPath project提供支持.更多其他的关于验证结果和模拟请求的选项在后面的文档中会讨论.

Static Imports

上面例子代码中的API需要几个静态的imports,例如MockMvcRequestBuilders.*,MockMvcResultMatchers.*MockMvcBuilders.*.下面导包操作就省略了.

Setup Choices

你有2种主要的方式来创建MockMVC实例.第一种是通过TestContext框架加载Spring MVC配置文件,这样会加载Spring的配置信息并且通过注入WebApplicationContext到测试中来构建一个MockMVC实例.下面是示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RunWith(SpringRunner.class)
@WebAppConfiguration
@ContextConfiguration("my-servlet-context.xml")
public class MyWebTests {

@Autowired
private WebApplicationContext wac;

private MockMvc mockMvc;

@Before
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
}

// ...

}

第二种选择是手动创建一个controller实例而不加载Spring的配置信息.相对的,通过大致比较MVC JavaConfig或者MVC namespace来自动的创建一个默认的配置.你可以在一定程度上自定义.下面是实例代码:

1
2
3
4
5
6
7
8
9
10
11
12
public class MyWebTests {

private MockMvc mockMvc;

@Before
public void setup() {
this.mockMvc = MockMvcBuilders.standaloneSetup(new AccountController()).build();
}

// ...

}

你会选择哪一种方式呢?#反正我是第二种hhh

webAppContextSetup会加载你实际的Spring MVC配置信息,导致一个更完整的集成测试.自从TestContext框架缓存加载Spring配置信息之后会让测试运行的更快,甚至在你引入了更多的测试的时候.此外,你可以通过Spring configuration注入mock service到你的controller以便于专注地在web层进行测试.下面的例子声明了一个mock service使用Mockito:

1
2
3
<bean id="accountService" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.example.AccountService"/>
</bean>

然后你可以把mock service注入到测试中并且配置成你想要的样子.下面是示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RunWith(SpringRunner.class)
@WebAppConfiguration
@ContextConfiguration("test-servlet-context.xml")
public class AccountTests {

@Autowired
private WebApplicationContext wac;

private MockMvc mockMvc;

@Autowired
private AccountService accountService;

// ...

}

这种单独的设置,从另一个方面看更接近一个单元测试,每次只测试一个controller.你可以使用mock依赖手动注入controller,并且这不涉及到加载Spring配置信息.这种测试更加关注于”style”并且让看到哪个controller正在被测试变得更容易,是否需要特定的Spring MVC配置文件才能运行,等等.这种单独设置的方式也是一种便利的写ad-hoc测试来验证具体的行为或者debug一个issue的方式.

在集成和单元测试的争论下,没有对的或者错的答案.然而使用单独设置就意味着需要额外的webAppContextSetup测试来验证你的Sring配置文件.或者你也可以用webAppContextSetup来写你的所有的测试,这样可以始终测试你的Spring配置信息.

Setup Features

无论你使用那种方式来构建MockMVC,所有的MockMvcBuilder实现都提供了一些常见并且很实用的特性.例如,你可以为所有的请求声明一个Accept头并且对于所有的response希望返回200的状态码和Content-Type头.下面是示例代码:

1
2
3
4
5
6
7
// static import of MockMvcBuilders.standaloneSetup

MockMvc mockMvc = standaloneSetup(new MusicController())
.defaultRequest(get("/").accept(MediaType.APPLICATION_JSON))
.alwaysExpect(status().isOk())
.alwaysExpect(content().contentType("application/json;charset=UTF-8"))
.build();

此外,第三方框架(和应用)可以预先打包设置声明,比如在MockMvcConfigurer.Spring框架有一个内置的实现可以通过请求来保存和重用HTTP session.你可以像下面一样使用:

1
2
3
4
5
6
7
// static import of SharedHttpSessionConfigurer.sharedHttpSession

MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TestController())
.apply(sharedHttpSession())
.build();

// Use mockMvc to perform requests...

查看Javadoc for ConfigurableMockMvcBuilder,列出了所有MockMvc内建的特性或者使用ide来查看.

Performing Requests

你可以使用任何的HTTP方法来模拟请求:

1
mockMvc.perform(post("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON));

你也可以使用MockMultipartHttpServletRequest内的模拟文件上传请求,这样就不会实际的解析一个多部分的请求.

1
mockMvc.perform(multipart("/doc").file("a1", "ABC".getBytes("UTF-8")));

你可以添加一个请求参数使用URI template风格:

1
mockMvc.perform(get("/hotels?thing={thing}", "somewhere"));

你还可以添加Servlet请求参数不管是请求参数还是表单参数.

1
mockMvc.perform(get("/hotels").param("thing", "somewhere"));

如果应用的编码依赖于Servlet请求参数并且没有明确检查请求的字符串,你使用那个选项都没关系.但是请记住,提供的请求参数使用URI template的时候被编码,在请求参数经过param()方法的时候是预计已经被编码的.

在大多数的情况下,最好是把context path和Servlet path放在请求的URI外面.如果你必须要在完整的URI下测试,确定设置了contextPathservletPath,request mapping才能正常工作.

1
mockMvc.perform(get("/app/main/hotels/{id}").contextPath("/app").servletPath("/main"))

在上面的例子里,给每个模拟请求设置contextPathservletPath是很笨重的.你可以设置默认的请求参数来代替:

1
2
3
4
5
6
7
8
9
10
11
public class MyWebTests {

private MockMvc mockMvc;

@Before
public void setup() {
mockMvc = standaloneSetup(new AccountController())
.defaultRequest(get("/")
.contextPath("/app").servletPath("/main")
.accept(MediaType.APPLICATION_JSON)).build();
}

上面的设置会影响所有通过这个MockMvc实例模拟的请求.如果相同的属性也在给定的请求里出现了,会覆盖默认的值.这也就是为什么HTTP方法和URI在默认的请求下没有影响,因为必须在每个请求中指定它们.

Defining Expectations

你可以定义预计的结果通过添加一个或者更多的.andExpect(..)在模拟请求之后:

1
mockMvc.perform(get("/accounts/1")).andExpect(status().isOk());

MockMvcResultMatchers.*提供了大量的expectations,其中一些嵌套了更详细的expectations.

Expectations分为两大类,第一类断言验证响应的属性,这些都是最重要的结果断言.

第二类断言超出了response的范围,这些断言可以让你检查Spring MVC的特定方面,例如哪个controller的方法处理了请求,是否一个异常被抛出和处理,模型的content是什么,哪个view被选择,哪些属性被添加,等等..他们也运行检查Servlet的特定方面,例如请求和会话属性.

下面的测试断言绑定或者验证失败:

1
2
3
mockMvc.perform(post("/persons"))
.andExpect(status().isOk())
.andExpect(model().attributeHasErrors("person"));

很多时候,当你在写测试,打印出模拟请求的结果是很有帮助的,可以使用MockMvcResultHandlers的静态方法print()来做到:

1
2
3
4
mockMvc.perform(post("/persons"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(model().attributeHasErrors("person"));

只要请求过程没有造成一个没有处理的异常,print()方法就会打印出所有可得到的结果数据到System.out.Spring Framework 4.2引入了一个log()方法和两个附加的变种的print()方法,一个接收OutputStream一个接收Writer.例如,请求print(System.err)打印结果到System.err.当请求print(myWriter)的时候会打印数据到规定的地方.如果你想要使用结果数据日志来代替打印,使用log()方法,会把数据使用DEBUG打到org.springframework.test.web.servlet.result配置下.

在某些情况下,你也许想直接访问并且不想验证其他结果可以在后面加.andReturn():

1
2
MvcResult mvcResult = mockMvc.perform(post("/persons")).andExpect(status().isOk()).andReturn();
// ...

如果所有的测试有使用相同的expectations,你可以设置一个公共的expectations构建在MockMvc实例里面:

1
2
3
4
standaloneSetup(new SimpleController())
.alwaysExpect(status().isOk())
.alwaysExpect(content().contentType("application/json;charset=UTF-8"))
.build()

注意公用的expectations总是会被使用并且无法被覆盖.

当JSON响应内容包含使用Spring HATEOAS创建的超媒体链接时.你可以使用JsonPath表达式来验证结果连接:

1
2
mockMvc.perform(get("/people").accept(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.links[?(@.rel == 'self')].href").value("http://localhost:8080/people"));

当xml被包含的时候可以使用xpath表达式来验证:

1
2
3
Map<String, String> ns = Collections.singletonMap("ns", "http://www.w3.org/2005/Atom");
mockMvc.perform(get("/handle").accept(MediaType.APPLICATION_XML))
.andExpect(xpath("/person/ns:link[@rel='self']/@href", ns).string("http://localhost:8080/people"));

Filter Registrations

当设置一个MockMvc实例的时候,你可以注册一个或多个Servlet Filter实例:

1
mockMvc = standaloneSetup(new PersonController()).addFilters(new CharacterEncodingFilter()).build();

注册过滤通过spring-testMockFilterChain来调用,并且把最后一个过滤器委托给DispatcherServlet.

Differences Between Out-of-Container and End-to-End Integration Tests

就像之前提到的,Spring MVC Test构建在spring-test的Servlet API mock objects之上并且没有使用运行中的Servlet容器.因此,这和在客户端和服务器运行的完整的端到端的集成测试有一些很重要的区别.

最简单的方式就是从思考一个空白的MockHttpServletRequest开始.在这里面你添加什么,请求就会变成什么样.令你吃惊的也许是这里没有完整的上下文路径在默认情况下,没有jsessionid的cookie,没有转发,错误或是异步调度并且因此,没有实际的JSP转义.与此相对的,转发和重定向URL被保存在MockHttpServletResponse里并且可以使用表达式来断言它.

这也就意味着,如果你使用JSP,你可以验证请求转发到的JSP页面,但是没有HTML被呈现.换句话说,JSP不会被调用.但请注意,所有其他不依赖转发的渲染技术,例如Thymeleaf和Freemarker,会在response body里像期待的那样呈现HTML内容.相同的,JSON,XML和其他一切通过@ResponseBody注解传递的格式都可以.

另外,你也可以考虑通过使用Spring Boot的@WebIntegrationTest来实现完整的端到端的集成测试.查看Spring Boot Reference Guide.

每种方法都有利弊.Spring MVC Test提供的选项和传统的单元测试的完整集成测试所停留的面是不一样的.可以肯定的是,Spring MVC中没有任何属于传统单元测试的选项,但是有一点接近它.例如,你可以通过把mock service注入到controller中的方式来隔离web层,在这种情况下你测试web层只需要通过DispatcherServlet但是实际上你是使用了Spring配置信息的,就像你可以隔离掉数据访问层之上的层面来测试数据访问层一样.当然,你也可以使用单独的配置,每次只关注一个controller并且手动提供使其运行所必须的配置文件.

使用Spring MVC的另一个重大的区别是,从概念上讲,这些测试是服务端的,所以你可以检查哪个Handler被使用了,如果一个异常被HandlerExceptionResolver处理了,这时候模型的内容是什么,绑定的是什么错误和其他的信息(可以很轻松的获取).这也就意味着写expectations会变得简单,因为server这时候就不是一个黑盒了,就像是通过实际的HTTP客户端去测试一样.传统的单元测试的优势是:易于编写,推理和调试但是不能完全取代完整的集成测试.同时,重要的是不要忽略了最重要的是要去检查response这件事.简而言之,即使在一个项目中,也存在多种的测试的风格和策略(条条大路通罗马).

Further Server-Side Test Examples

这个框架自己的测试包含了很多测试的例子为了展示如何使用Spring MVC Test.你可以打开这些例子来寻找更多的灵感.同时,spring-mvc-showcase项目覆盖了完整的基于Spring MVC Test的测试.

0%