概念
单元测试(unit testing),指对代码的最小可测试单元进行检查和验证
UT的目的
- 验证代码的正确性
- 修改后并提交是进行验证
- 验证代码设计是否合理
桩
什么是桩
桩代码(stub),是指用来代替关联代码或者未实现代码的代码。
如果函数B用B1来代替,那么,B称为原函数,B1称为桩函数。打桩就是编写或生成桩代码。
意图是为了让测试对象可以正常的执行,其实现一般会硬编码一些输入和输出。
打桩的目的
- 隔离
- 控制
- 补齐
- 隔离,指将测试任务从产品项目中分离出来,使之能够独立编译、链接,并独立运行。隔离的基本方法就是打桩,将测试任务之外的,并且与测试任务相关的代码,用桩来代替,从而实现分离测试任务。例如函数A调用了函数B,函数B又调用了函数C和D,如果函数B用桩来代替,函数A就可以完全割断与函数C和D的关系。
- 控制是指在测试时,人为设定相关代码的行为,使之符合测试需求。设置返回结果。
- 补齐是指用桩来代替未实现的代码,例如,函数A调用了函数B,而函数B由其他程序员编写,且未实现,那么,可以用桩来代替函数B,使函数A能够运行并测试。补齐在并行开发中很常用。
模拟
什么是模拟
模拟(mock)除了保证stub的功能之外,还可深入的模拟对象之间的交互方式。
因此mock可以理解为是一种功能丰富的stub。
Java常用的UT框架
- Junit3/Junit4
- Spring mockMvc
- EasyMock
- Mockito
JUNIT
JUnit是Java单元测试框架,已经在Eclipse中默认安装。目前主流的有JUnit3和JUnit4。JUnit3中,测试用例需要继承TestCase类。JUnit4中,测试用例无需继承TestCase类,只需要使用@Test等注解。
Junit3
// 测试java.lang.Math
// 必须继承TestCase
public class Junit3TestCase extends TestCase {
public Junit3TestCase() {
super();
}
// 传入测试用例名称
public Junit3TestCase(String name) {
super(name);
}
// 在每个Test运行之前运行
@Override
protected void setUp() throws Exception {
System.out.println("Set up");
}
// 测试方法。
// 方法名称必须以test开头,没有参数,无返回值,是公开的,可以抛出异常
// 也即类似public void testXXX() throws Exception {}
public void testMathPow() {
System.out.println("Test Math.pow");
Assert.assertEquals(4.0, Math.pow(2.0, 2.0));
}
public void testMathMin() {
System.out.println("Test Math.min");
Assert.assertEquals(2.0, Math.min(2.0, 4.0));
}
// 在每个Test运行之后运行
@Override
protected void tearDown() throws Exception {
System.out.println("Tear down");
}
}
运行测试方法
在Eclipse中,可以直接在类名或测试方法上右击,在弹出的右击菜单中选择Run As -> JUnit Test。
在Mvn中,可以直接通过mvn test命令运行测试用例。
也可以通过Java方式调用,创建一个TestCase实例,然后重载runTest()方法,在其方法内调用测试方法(可以多个)。
TestCase test = new Junit3TestCase("mathPow") {
// 重载
protected void runTest() throws Throwable {
testMathPow();
};
};
test.run();
更加便捷地,可以在创建TestCase实例时直接传入测试方法名称,JUnit会自动调用此测试方法,如
TestCase test = new Junit3TestCase("testMathPow");
test.run();
Junit TestSuite
TestSuite是测试用例套件,能够运行过个测试方法。如果不指定TestSuite,会创建一个默认的TestSuite。默认TestSuite会扫描当前内中的所有测试方法,然后运行。
如果不想采用默认的TestSuite,则可以自定义TestSuite。在TestCase中,可以通过静态方法suite()返回自定义的suite。
import junit.framework.Assert;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
public class Junit3TestCase extends TestCase {
//...
public static Test suite() {
System.out.println("create suite");
TestSuite suite = new TestSuite();
suite.addTest(new Junit3TestCase("testMathPow"));
return suite;
}
}
JUnit4
JUnit4通过注解的方式来识别测试方法。目前支持的主要注解有:
-
@BeforeClass
全局只会执行一次,而且是第一个运行 -
@Before
在测试方法运行之前运行 -
@Test
测试方法 -
@After
在测试方法运行之后允许 -
@AfterClass
全局只会执行一次,而且是最后一个运行 -
@Ignore
忽略此方法
执行次序是@BeforeClass
-> @Before
-> @Test
-> @After
-> @Before
-> @Test
-> @After
-> @AfterClass
。@Ignore
会被忽略。
Assert
Junit3和Junit4都提供了一个Assert类(虽然package不同,但是大致差不多)。Assert类中定义了很多静态方法来进行断言。列表如下:
- assertTrue(String message, boolean condition) 要求condition == true
- assertFalse(String message, boolean condition) 要求condition == false
- fail(String message) 必然失败,同样要求代码不可达
- assertEquals(String message, XXX expected,XXX actual) 要求expected.equals(actual)
- assertArrayEquals(String message, XXX[] expecteds,XXX [] actuals) 要求expected.equalsArray(actual)
- assertNotNull(String message, Object object) 要求object!=null
- assertNull(String message, Object object) 要求object==null
- assertSame(String message, Object expected, Object actual) 要求expected == actual
- assertNotSame(String message, Object unexpected,Object actual) 要求expected != actual
- assertThat(String reason, T actual, Matcher matcher) 要求matcher.matches(actual) == true
Spring中用MockMvc
MockMvcBuilder
MockMvcBuilder是用来构造MockMvc的构造器,其主要有两个实现:StandaloneMockMvcBuilder和DefaultMockMvcBuilder,分别对应两种测试方式,即独立安装和集成Web环境测试(此种方式并不会集成真正的web环境,而是通过相应的Mock API进行模拟测试,无须启动服务器)。对于我们来说直接使用静态工厂MockMvcBuilders创建即可。
MockMvcBuilders.webAppContextSetup(WebApplicationContext context):指定WebApplicationContext,将会从该上下文获取相应的控制器并得到相应的MockMvc;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:config/IncotermsRestServiceTest-context.xml")
@WebAppConfiguration
public class IncotermsRestServiceTest {
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@Before
public void setup() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); //构造MockMvc
}
...
}
一些常用的测试
得到MvcResult自定义验证
MvcResult result = mockMvc.perform(get("/user/{id}", 1))//执行请求
.andReturn(); //返回MvcResult
Assert.assertNotNull(result.getModelAndView().getModel().get("user")); //自定义断言
文件上传
byte[] bytes = new byte[] {1, 2};
mockMvc.perform(fileUpload("/user/{id}/icon", 1L).file("icon", bytes)) //执行文件上传
.andExpect(model().attribute("icon", bytes)) //验证属性相等性
.andExpect(view().name("success")); //验证视图
JSON请求/响应验证
String requestBody = "{\"id\":1, \"name\":\"zhang\"}";
mockMvc.perform(post("/user")
.contentType(MediaType.APPLICATION_JSON).content(requestBody)
.accept(MediaType.APPLICATION_JSON)) //执行请求
.andExpect(content().contentType(MediaType.APPLICATION_JSON)) //验证响应contentType
.andExpect(jsonPath("$.id").value(1)); //使用Json path验证JSON 请参考http://goessner.net/articles/JsonPath/
String errorBody = "{id:1, name:zhang}";
MvcResult result = mockMvc.perform(post("/user")
.contentType(MediaType.APPLICATION_JSON).content(errorBody)
.accept(MediaType.APPLICATION_JSON)) //执行请求
.andExpect(status().isBadRequest()) //400错误请求
.andReturn();
Assert.assertTrue(HttpMessageNotReadableException.class.isAssignableFrom(result.getResolvedException().getClass()));//错误的请求内容体