单元测试”这个词在不同场景下可能有不同意思。我更偏向于将大家大致理解的“单元测试”称为“开发阶段的自动化测试“。这样就可以体现三个点:
一是,这个测试是开发同学自己搞的。开发人员搞定代码后,为新代码添加测试,并且保证新老测试都能通过,才能提测。开发同学不能将满是bug的代码就丢给测试同学。
二是,这个测试是自动化的,不是“手工调用接口做一次测试“那种形式的。并且执行起来要非常简单。比如任何人拿到代码,只要机器上装了对应的开发工具,都可以轻松运行测试,看到报告(比如对于java,一句mvn test, 或者对nodejs,一句npm test就能跑)。
三是,开发人员要做的测试,真的不一定是只验证一个“单元”的正确性
单元测试的意义
- 单元测试集中注意力于程序的基本组成部分,首先保证每个单元测试通过,才能使下一步把单元组装成部件并测试其正确性具有基础。单元是整个软件的构成基础,像硬件系统中的零部件一样,只有保证零部件的质量,这个设备的质量才有基础,单元的质量也是整个软件质量的基础。因此,单元测试的效果会直接影响软件的后期测试,最终在很大程度上影响到产品的质量。
- 单元测试可以平行开展,这样可以使多人同时测试多个单元,提高了测试的效率。
- 单元规模较小,复杂性较低,因而发现错误后容易隔离和定位,有利于调试工作。
- 单元的规模和复杂性特点,使单元测试中可以使用包括白盒测试的覆盖分析在内的许多测试技术,能够进行比较充分细致的测试,是整个程序测试满足语句覆盖和分支覆盖要求的基础。
- 单元测试的测试效果是最显而易见的。做好单元测试,不仅后期的系统集成联调或集成测试和系统测试会很顺利,节约很多时间;而且在单元测试过程中能发现一些很深层次的问题,同时还会发现一些很容易发现而在集成测试和系统测试很难发现的问题;更重要的是单元测试不仅仅是证明这些代码做了什么,是如何做的,而且证明是否做了它该做的事情而没有做不该做的事情。
- 单元测试的好与坏不仅直接关系到测试成本(因为如果单元测试中易发现的问题拖到后期测试发现,那么其成本将成倍数上升),而且也会直接影响到产品质量,因为可能就是由于代码中的某一个小错误就导致了整个产品的质量降低一个指标,或者导致更严重的后果。
单元测试详细简介如下图:
Spring Boot中引入单元测试
在maven项目pom文件中加入以下引用:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
创建测试类
创建测试类需要注意以下几点:
- 测试方法上面必须使用@Test注解进行修饰。
- 测试方法必须使用public void 进行修饰,不能带有任何参数。
- 测试类的包应该与被测试类的包保持一致。
- 测试单元中的每一个方法必须独立测试,每个测试方法之间不能有依赖。
- 测试类最好使用Test做为类名的后缀
常用注解
- @BeforeClass所修饰的方法在所有方法加载前执行,而且他是静态的在类加载后就会执行该方法,在内存中只有一份实例,适合用来加载配置文件。
- @AfterClass所修饰的方法在所有方法执行完毕之后执行,通常用来进行资源清理,例如关闭数据库连接。
- @Before和@After在每个测试方法执行前都会执行一次。
- @Test(excepted=XX.class)在运行时忽略某个异常。
- @Transactional和@Rollback(true)所修饰的方法开启事务,失败时自动回滚。默认为true,可以缺省。
- @Test(timeout=毫秒) 允许程序运行的时间。
- @Ignore 所修饰的方法被测试器忽略。
创建测试类时要加上@RunWith(SpringRunner.class)
和@SpringBootTest(classes = RestApiApplication.class)
注解。
这里使用MockMVC来测试当前系统RestFul接口,使用MockMvcRequestBuilders构建post或get请求,并设置请求头和参数;在接口返回相应中判断状态码是否正确,以及是否包含期待的结果。
具体操作见代码。
1、新建一个BaseControllerTest类,添加如下代码:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = RestApiApplication.class)
public class BaseControllerTest {
@Resource
public WebApplicationContext wac;
protected MockMvc mvc;
@Before
public void setupMockMvc() {
//初始化MockMvc对象
mvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
}
2、新建单元测试类继承BaseControllerTest
get接口测试类如下:
package net.linksfield.fms;
import org.junit.Test;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.transaction.annotation.Transactional;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
/**
* Created by lirui on 2020/4/8
*/
public class TestOneControllerTest extends BaseControllerTest {
//获取TestOnecontroller路径
private String TestOneControllerUrl = "/testOne";
//获取测试get接口1路径
private String get1Url = "/get1?id=1";
//获取测试get接口2路径
private String get2Url = "/get2?id=001&&name=李锐";
//获取测试get接口2路径(添加事务)
private String get2UrlTransactional = "/get2?id=002&&name=张三";
@Test
public void test_N_get1() {
try {
mvc.perform(MockMvcRequestBuilders.get(TestOneControllerUrl+get1Url)
.accept(MediaType.APPLICATION_JSON_UTF8)
.contentType(MediaType.APPLICATION_JSON_UTF8)
)
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(jsonPath("$.code").value("0"))
.andDo(MockMvcResultHandlers.print());
} catch (Exception e) {
System.out.println(e.toString());
}
}
@Test
public void test_N_get2() {
try {
mvc.perform(MockMvcRequestBuilders.get(TestOneControllerUrl+get2Url)
.accept(MediaType.APPLICATION_JSON_UTF8)
.contentType(MediaType.APPLICATION_JSON_UTF8)
)
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(jsonPath("$.code").value("0"))
.andDo(MockMvcResultHandlers.print());
} catch (Exception e) {
System.out.println(e.toString());
}
}
@Test
@Transactional
@Rollback(true)// 事务自动回滚,默认是true。可以不写
public void test_N_get2_Transactional() {
try {
mvc.perform(MockMvcRequestBuilders.get(TestOneControllerUrl+get2UrlTransactional)
.accept(MediaType.APPLICATION_JSON_UTF8)
.contentType(MediaType.APPLICATION_JSON_UTF8)
)
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(jsonPath("$.code").value("0"))
.andDo(MockMvcResultHandlers.print());
} catch (Exception e) {
System.out.println(e.toString());
}
}
}
post接口测试如下:
import com.alibaba.fastjson.JSONObject;
import net.linksfield.fms.mdb.entity.FmsTestUser;
import net.linksfield.fms.rest.controller.testcontroller.TestTwoController;
import org.junit.Test;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.transaction.annotation.Transactional;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
public class TestTwoControllerTest extends BaseControllerTest{
//获取TestTwoController路径
private String TestTwoControllerUrl = "/testTwo";
//获取测试get接口1路径
private String post1Url = "/post1";
//获取测试get接口2路径
private String post2Url = "/post2";
@Test
public void test_N_post1() {
FmsTestUser fmsTestUser = new FmsTestUser();
fmsTestUser.setId("1");
fmsTestUser.setName("李锐");
String parm = JSONObject.toJSONString(fmsTestUser);
try {
mvc.perform(MockMvcRequestBuilders.post(TestTwoControllerUrl +post1Url)
.content(parm)
.accept(MediaType.APPLICATION_JSON_UTF8)
.contentType(MediaType.APPLICATION_JSON_UTF8)
)
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(jsonPath("$.code").value("0"))
.andDo(MockMvcResultHandlers.print());
} catch (Exception e) {
System.out.println(e.toString());
}
}
@Test
public void test_N_post2() {
try {
mvc.perform(MockMvcRequestBuilders.post(TestTwoControllerUrl + post2Url)
.accept(MediaType.APPLICATION_JSON_UTF8)
.contentType(MediaType.APPLICATION_JSON_UTF8)
)
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(jsonPath("$.code").value("0"))
.andDo(MockMvcResultHandlers.print());
} catch (Exception e) {
System.out.println(e.toString());
}
}
@Test
@Transactional
@Rollback(true)// 事务自动回滚,默认是true。可以不写
public void test_N_post2_Transactional() {
try {
mvc.perform(MockMvcRequestBuilders.get(TestTwoControllerUrl+ post2Url)
.accept(MediaType.APPLICATION_JSON_UTF8)
.contentType(MediaType.APPLICATION_JSON_UTF8)
)
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(jsonPath("$.code").value("0"))
.andDo(MockMvcResultHandlers.print());
} catch (Exception e) {
System.out.println(e.toString());
}
}
}
批量运行测试类
如果想要批量运行多个测试类,提高测试效率,可以使用@RunWith(Suite.class)
注解。在@Suite.SuiteClasses({})
注解中填入批量测试类。
代码如下:
package net.linksfield.fms;
import org.junit.ClassRule;
import org.junit.rules.ExternalResource;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
/**
* Created by lirui on 2020/4/7
*/
@RunWith(Suite.class)
@Suite.SuiteClasses({TestOneControllerTest.class, TestTwoControllerTest.class})
public class AllTest {
@ClassRule
public static ExternalResource testRule = new ExternalResource(){
/*
初始化,创建必要的测试资源
*/
@Override
protected void before() throws Throwable{
System.out.println("Master setup" );
};
/*
测试用例执行完毕,清扫临时资源
*/
@Override
protected void after(){
System.out.println("Master tearDown");
};
};
}