一、概述
单元测试的优点
- 提升软件质量
- 促进代码优化
单元测试的基本原则
- 单元测试要符合AIR原则
- A : Automatic (自动化)
- I : Independent (独立性)
- R : Repeatable (可重复)
- 单元测试的代码层面要符合BCDE原则
- B: Border 边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等
- C: Correct 正确的输入,并得到预期的结果
- D: Design 与设计文档相结合,来编写单元测试
- E : Error 单元测试的目标是证明程序有错,而不是程序无错。为了发现代码中潜在的错误,我们需要在编写测试用例时有一些强制的错误输入(如非法数据、异常流程、非业务允许输入等)来得到预期的错误结果
二、Junit5注解
注解 | 用法说明 |
---|
@Test | 表明方法是一个测试方法 | @DisplayName | 为测试类或者测试方法自定义一个名称 | @BeforeAll | 在所有测试方法运行前运行,并且只能修饰静态方法(除非修改测试实例生命周期) | @BeforeEach | 每个测试方法运行前运行 | @AfterEach | 每个测试方法运行完毕后运行 | @AfterAll | 在所有测试方法运行完毕后运行 | @Disabled | 这个测试不会运行 | @RepeatedTest | 重复测试 | @TestMethodOrder | 指定测试顺序排序方式 | @Order | 注解定义测试顺序 |
package com.flamingo.junit5test;
import com.flamingo.junit5test.util.Add;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@DisplayName("Common annotation test")
public class AnnotationsTest {
private static Add add;
@BeforeAll
public static void beforeAll() {
add = new Add();
System.out.println("Run before all test methods run");
}
@BeforeEach
public void beforeEach() {
System.out.println("Run before each test method runs");
}
@AfterEach
public void afterEach() {
System.out.println("Run after each test method finishes running");
}
@AfterAll
public static void afterAll() {
System.out.println("Run after all test methods have finished running");
}
@Disabled
@Test
@DisplayName("Ignore the test")
public void disabledTest() {
System.out.println("This test will not run");
}
@Test
@DisplayName("Test Methods 1+1")
public void testAdd1() {
System.out.println("Running test method1+1");
Assertions.assertEquals(2, add.add(1, 1));
}
@Test
@DisplayName("Test Methods 2+2")
public void testAdd2() {
System.out.println("Running test method2+2");
Assertions.assertEquals(4,add.add(2,2));
}
}
三、断言
常用断言 | 断言说明 |
---|
assertEquals | 断言预期值和实际值相等 | assertAll | 分组断言,执行其中包含的所有断言 | assertArrayEquals | 断言预期数组和实际数组相等 | assertFalse | 断言条件为假 | assertNotNull | 断言不为空| | assertSame | 断言两个对象相等 | assertTimeout | 断言超时 | assertThrows | 断言异常,抛出指定的异常,测试才会通过 | assertTimeoutPreemptively | 断言超时,如果在定时内任务没有执行完毕,会立即返回断言失败 |
package com.flamingo.junit5test;
import org.junit.jupiter.api.Test;
import static java.time.Duration.ofMillis;
import static java.time.Duration.ofMinutes;
import static org.junit.jupiter.api.Assertions.*;
public class Assert {
@Test
void standardAssertions() {
assertEquals(2, 2);
assertEquals(4, 4, "error message");
assertTrue(2 == 2, () -> "error message");
}
@Test
void groupedAssertions() {
assertAll("person", () -> assertEquals("John", "John"), () -> assertEquals("Doe", "Doe"));
}
@Test
void dependentAssertions() {
assertAll("properties", () -> {
String firstName = "John";
assertNotNull(firstName);
assertAll("first name", () -> assertTrue(firstName.startsWith("J")),
() -> assertTrue(firstName.endsWith("n")));
}, () -> {
String lastName = "Doe";
assertNotNull(lastName);
assertAll("last name", () -> assertTrue(lastName.startsWith("D")),
() -> assertTrue(lastName.endsWith("e")));
});
}
@Test
void exceptionTesting() {
Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("a message");
});
assertEquals("a message", exception.getMessage());
}
@Test
void timeoutNotExceeded() {
assertTimeout(ofMinutes(2), () -> {
});
}
@Test
void timeoutNotExceededWithResult() {
String actualResult = assertTimeout(ofMinutes(2), () -> {
return "result";
});
assertEquals("result", actualResult);
}
@Test
void timeoutExceeded() {
assertTimeout(ofMillis(10), () -> {
Thread.sleep(1000);
});
}
@Test
void timeoutExceededWithPreemptiveTermination() {
assertTimeoutPreemptively(ofMillis(10), () -> {
Thread.sleep(1000);
});
}
}
3.2 AssertJ
AssertJ 的最大特点是流式断言(Fluent Assertions),与Build Chain 模式或Java stream&filter 写法类似。它允许一个目标对象通过各种Fluent Assert API连接判断进行多次断言并且对IDE更友好。
package com.flamingo.junit5test;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
public class AssertJSampleTest {
@Test
public void testUsingAssertJ() {
String s = "abcde";
assertThat(s).as("字符串判断,判断首尾及长度").startsWith("a").endsWith("e").hasSize(5);
Integer i = 50;
assertThat(i).as("数字判断,数字大小比较").isGreaterThan(10).isLessThan(100);
Date date1 = new Date();
Date date2 = new Date(date1.getTime() + 100);
Date date3 = new Date(date1.getTime() - 100);
assertThat(date1).as("日期判断:日期大小比较").isBefore(date2).isAfter(date3);
List<String> list = Arrays.asList("a", "b", "c", "d");
assertThat(list).as("list的首尾元素及长度").startsWith("a").endsWith("d").hasSize(4);
Map<String, Object> map = new HashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
assertThat(map).as("Map的长度及键值测试").hasSize(3).containsKeys("x cA","B","C");
}
}
四、Junit条件和假设
条件假设 | 说明 |
---|
assumeFalse | 假设为false时才会执行,如果为true,那么将会直接停止执行 | assumeTrue | 假设为true时才会执行,如果为false,那么将会直接停止执行 | assumingThat | assumingThat接受一个函数式接口Executable,假设为true时执行,将会执行Executable,否则不会执行Executable |
package com.flamingo.junit5test;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assumptions.assumeFalse;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.api.Assumptions.assumingThat;
public class Assumption {
@Test
void assumeTrueTest() {
assumeTrue(false);
System.out.println("This will not be implemented.");
}
@Test
void assumeFalseTest() {
assumeFalse(true);
System.out.println("This will not be implemented.");
}
@Test
void assumingThatTest() {
assumingThat(false, () -> {
System.out.println("This will not be implemented.");
});
System.out.println("This will be implemented.");
}
}
五、重复测试
通过@RepeatedTest注解可以完成重复测试的工作;通过TestInfo对象可以获得当前测试类或方法的信息;通过RepetitionInfo对象可以获取到重复测试中的信息。
package com.flamingo.junit5test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;
import org.junit.jupiter.api.TestInfo;
public class Repeated {
@RepeatedTest(value = 10, name = RepeatedTest.LONG_DISPLAY_NAME)
@DisplayName("repeatTest")
void repeatedTest(TestInfo testInfo, RepetitionInfo repetitionInfo) {
System.out.println(testInfo.getDisplayName());
System.out.println("currentRepetition:"+repetitionInfo.getCurrentRepetition());
}
}
六、自定义测试顺序
@TestMethodOrder注解启用测试顺序,并且可指定三种测试顺序。
设置值 | 说明 |
---|
MethodOrderer.OrderAnnotation.class | 使用@Order注解指定的值,值小先执行 | MethodOrderer.MethodName.class | 根据方法名称的字典序来执行 | MethodOrderer.Random.class | 随机顺序执行 |
package com.flamingo.junit5test;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static org.junit.jupiter.api.Assertions.assertEquals;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class OrderedTest {
@Order(7)
@Test
public void aMethod() {
System.out.println("A Method");
assertEquals(1, 1);
}
@Order(2)
@Test
@Disabled
public void bMethod() {
System.out.println("B Method");
assertEquals(1, 1);
}
@Order(3)
@Test
public void cMethod() {
System.out.println("C Method");
assertEquals(1, 1);
}
@Order(4)
@Test
public void dMethod() {
System.out.println("D Method");
assertEquals(1, 1);
}
}
七、MockMvc测试控制器
为了可以对Controller进行测试,可以通过引入MockMVC进行解决。 MockMvc实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,这样可以使得测试速度快、不依赖网络环境,而且提供了一套验证的工具,这样可以使得请求的验证统一而且很方便。
MockMvc测试过程如下
- mockMvc.perform执行一个RequestBuilder请求,会自动执行SpringMVC的流程并映射到相应的控制器执行处理
- MockMvcRequestBuilders.post(“http://127.0.0.1:8888/login“)构造一个请求
- ResultActions.andExpect添加执行完成后的断言
- ResultActions.andDo添加一个结果处理器,表示要对结果做点什么事情
- ResultActions.andReturn表示执行完成后返回相应的结果
MockMvc涉及的API有很多,这里记下来以作查验
RequestBuilder
mockMvc.perform执行一个RequestBuilder请求,RequestBuilder主要有两个子类MockHttpServletRequestBuilder和MockMultipartHttpServletRequestBuilder(如文件上传使用).可以用MockMvcRequestBuilders 的静态方法构建RequestBuilder对象。 MockHttpServletRequestBuilder API:
MockHttpServletRequestBuilder header(String name, Object… values)/MockHttpServletRequestBuilder headers(HttpHeaders httpHeaders):添加头信息; MockHttpServletRequestBuilder contentType(MediaType mediaType):指定请求的contentType头信息; MockHttpServletRequestBuilder accept(MediaType… mediaTypes)/MockHttpServletRequestBuilder accept(String… mediaTypes):指定请求的Accept头信息; MockHttpServletRequestBuilder content(byte[] content)/MockHttpServletRequestBuilder content(String content):指定请求Body体内容; MockHttpServletRequestBuilder cookie(Cookie… cookies):指定请求的Cookie; MockHttpServletRequestBuilder locale(Locale locale):指定请求的Locale; MockHttpServletRequestBuilder characterEncoding(String encoding):指定请求字符编码; MockHttpServletRequestBuilder requestAttr(String name, Object value) :设置请求属性数据; MockHttpServletRequestBuilder sessionAttr(String name, Object value)/MockHttpServletRequestBuilder sessionAttrs(Map sessionAttributes):设置请求session属性数据; MockHttpServletRequestBuilder flashAttr(String name, Object value)/MockHttpServletRequestBuilder flashAttrs(Map flashAttributes):指定请求的flash信息,比如重定向后的属性信息; MockHttpServletRequestBuilder session(MockHttpSession session) :指定请求的Session; MockHttpServletRequestBuilder principal(Principal principal) :指定请求的Principal; MockHttpServletRequestBuilder contextPath(String contextPath) :指定请求的上下文路径,必须以“/”开头,且不能以“/”结尾; MockHttpServletRequestBuilder pathInfo(String pathInfo) :请求的路径信息,必须以“/”开头; MockHttpServletRequestBuilder secure(boolean secure):请求是否使用安全通道; MockHttpServletRequestBuilder with(RequestPostProcessor postProcessor):请求的后处理器,用于自定义一些请求处理的扩展点; MockMultipartHttpServletRequestBuilder继承自MockHttpServletRequestBuilder,又提供了如下API: MockMultipartHttpServletRequestBuilder file(String name, byte[] content)/MockMultipartHttpServletRequestBuilder file(MockMultipartFile file):指定要上传的文件;
MockMvcRequestBuilders的主要方法
MockHttpServletRequestBuilder get(String urlTemplate, Object… urlVariables):根据uri模板和uri变量值得到一个GET请求方式的MockHttpServletRequestBuilder;如get("/user/{id}", 1L); MockHttpServletRequestBuilder post(String urlTemplate, Object… urlVariables):同get类似,但是是POST方法; MockHttpServletRequestBuilder put(String urlTemplate, Object… urlVariables):同get类似,但是是PUT方法; MockHttpServletRequestBuilder delete(String urlTemplate, Object… urlVariables) :同get类似,但是是DELETE方法; MockHttpServletRequestBuilder options(String urlTemplate, Object… urlVariables):同get类似,但是是OPTIONS方法; MockHttpServletRequestBuilder request(HttpMethod httpMethod, String urlTemplate, Object… urlVariables):提供自己的Http请求方法及uri模板和uri变量,如上API都是委托给这个API; MockMultipartHttpServletRequestBuilder fileUpload(String urlTemplate, Object… urlVariables):提供文件上传方式的请求,得到MockMultipartHttpServletRequestBuilder; RequestBuilder asyncDispatch(final MvcResult mvcResult):创建一个从启动异步处理的请求的MvcResult进行异步分派的RequestBuilder;
ResultActions
调用MockMvc.perform(RequestBuilder requestBuilder)后将得到ResultActions,通过ResultActions完成如下三件事:
ResultActions andExpect(ResultMatcher matcher) :添加验证断言来判断执行请求后的结果是否是预期的; ResultActions andDo(ResultHandler handler) :添加结果处理器,用于对验证成功后执行的动作,如输出下请求/结果信息用于调试; MvcResult andReturn() :返回验证成功后的MvcResult;用于自定义验证/下一步的异步处理;
ResultMatcher
ResultMatcher用来匹配执行完请求后的结果验证,其就一个match(MvcResult result)断言方法,如果匹配失败将抛出相应的异常;spring mvc测试框架提供了很多xxxResultMatchers来满足测试需求。注意这些ResultMatchers并不是ResultMatcher的子类,而是返回ResultMatcher实例的。Spring mvc测试框架为了测试方便提供了MockMvcResultMatchers静态工厂方法方便操作;具体的API如下:
HandlerResultMatchers handler():请求的Handler验证器,比如验证处理器类型/方法名;此处的Handler其实就是处理请求的控制器; RequestResultMatchers request():得到RequestResultMatchers验证器; ModelResultMatchers model():得到模型验证器; ViewResultMatchers view():得到视图验证器; FlashAttributeResultMatchers flash():得到Flash属性验证; StatusResultMatchers status():得到响应状态验证器; HeaderResultMatchers header():得到响应Header验证器; CookieResultMatchers cookie():得到响应Cookie验证器; ContentResultMatchers content():得到响应内容验证器; JsonPathResultMatchers jsonPath(String expression, Object … args)/ResultMatcher jsonPath(String expression, Matcher matcher):得到Json表达式验证器; XpathResultMatchers xpath(String expression, Object… args)/XpathResultMatchers xpath(String expression, Map namespaces, Object… args):得到Xpath表达式验证器; ResultMatcher forwardedUrl(final String expectedUrl):验证处理完请求后转发的url(绝对匹配); ResultMatcher forwardedUrlPattern(final String urlPattern):验证处理完请求后转发的url(Ant风格模式匹配,@since spring4); ResultMatcher redirectedUrl(final String expectedUrl):验证处理完请求后重定向的url(绝对匹配); ResultMatcher redirectedUrlPattern(final String expectedUrl):验证处理完请求后重定向的url(Ant风格模式匹配,@since spring4); 得到相应的xxxResultMatchers后,接着再调用其相应的API得到ResultMatcher,如ModelResultMatchers.attributeExists(final String… names)判断Model属性是否存在。具体请查看相应的API。再次就不一一列举了。
MvcResult
即执行完控制器后得到的整个结果,并不仅仅是返回值,其包含了测试时需要的所有信息,如:
MockHttpServletRequest getRequest():得到执行的请求; MockHttpServletResponse getResponse():得到执行后的响应; Object getHandler():得到执行的处理器,一般就是控制器; HandlerInterceptor[] getInterceptors():得到对处理器进行拦截的拦截器; ModelAndView getModelAndView():得到执行后的ModelAndView; Exception getResolvedException():得到HandlerExceptionResolver解析后的异常; FlashMap getFlashMap():得到FlashMap; Object getAsyncResult()/Object getAsyncResult(long timeout):得到异步执行的结果;
一个简单的例子
package com.flamingo.junit5test;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
@SpringBootTest
@AutoConfigureMockMvc
public class MockMvcTest {
@Autowired
private MockMvc mockMvc;
@Test
public void getTest() throws Exception{
MvcResult mvcResult = mockMvc.perform(
MockMvcRequestBuilders.get("/hello/sayHi/{name}", "ming")
).andDo(MockMvcResultHandlers.print()).andReturn();
System.out.println(mvcResult.getResponse().getContentAsString());
}
}
|