SpringBoot单元测试的两种形式

@

前言

最近公司要求2021年所有的项目代码单元测试覆盖率要达到90%,作为刚毕业的小白来说这简直就是噩梦啊,springboot都没搞清楚呢,就要上手单元测试了。组里大佬说,单元测试有下面的各种好处:

  1. 发现逻辑中遗漏的数据结构及粗心错误
  2. 发现代码逻辑中90%可能会发生但是容易被忽略的NPE错误
  3. 检测代码逻辑是否能正常运行
  4. 检测代码结果是否符合预期
  5. 发现其他错误

既然领导和大佬都这么说了,小白只能突击学习单元测试了!当然,单元测试是一个开发人员必备的技能,不仅能够提升自身的开发能力,也可以提升自身的BUG改正能力。

Junit是目前使用最广泛、最多的单元测试工具类,IDEA在创建了springboot项目之后,可以自动集成Junit,可以通过创建单元测试类来测试。

SpringbootTest是集成于Springboot中的一个单元测试工具,它可以使开发人员在测试各种接口、业务代码时不用关心外部依赖是如何注入的,只需要关心代码本身。

本文具体就展现这两种测试方式有何不同,以及总结一些个人的观点。
由于项目中有比较多的业务代码,所以特地写了一个demo,里面的代码也是能省则省,之展现了单元测试具体的表现形式,逻辑方面还请大家多担待!

demo环境

使用的springboot项目,junit4,因为懒所以用的jpa没有用mybatis,但是用法还是那个用法。测试的项目数据库中没有数据。项目分为controller、dao、entity、service,下面是各层的代码,按介绍顺序放出各层对应的demo:

controller:

/**
 * 创建时间:2021/1/7 11:32
 * 单元测试控制类
 * @author wyb
 */
@RestController
@RequestMapping("/student")
public class TestDemoController {

	@Resource
	private TestDemoService testDemoService;

	@GetMapping("/list")
	public List<Student> list(StudentQuery studentQuery){
		return testDemoService.isExist(studentQuery);
	}
}

dao:

/**
 * 创建时间:2021/1/7 11:38
 *
 * @author wyb
 */
@Repository
public interface StudentProperty extends JpaRepository<Student,Integer> {

	/**
	 * 根据姓名模糊查找student
	 * @param name 姓名
	 * @return
	 */
	List<Student> findByNameLike(String name);
}

entity:

@Entity
@Table(name="student")
public class Student {

    @Id
    private Integer uid;

    @Column(name = "name")
    private String name;

    @Column(name = "age")
    private Integer age;

    @Column(name = "remarks")
    private String remarks;

    public Integer getUid() {
        return uid;
    }

    public void setUid(Integer uid) {
        this.uid = uid;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getRemarks() {
        return remarks;
    }

    public void setRemarks(String remarks) {
        this.remarks = remarks;
    }

    @Override
    public String toString() {
        return "Student{" +
                "uid=" + uid +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", remarks='" + remarks + '\'' +
                '}';
    }
}

service:

/**
 * 创建时间:2021/1/7 11:33
 * 单元测试类service
 * @author wyb
 */
@Service
public class TestDemoService {

	@Resource
	private StudentProperty studentProperty;

	/**
	 * 查询学生列表
	 * @return
	 */
	public List<Student> listStudent(){
		//查询学生列表
		//简单的逻辑
		return studentProperty.findAll();
	}

	public List<Student> isExist(StudentQuery studentQuery){
		Student student = new Student();
		if(!Objects.isNull(studentQuery)){
			student.setName(studentQuery.getName());
		}

		return studentProperty.findByNameLike(studentQuery.getName());
	}

}

同时为了方便,我还写了一个student的查询类

/**
 * 创建时间:2021/1/7 11:48
 *
 * @author wyb
 */
public class StudentQuery {


	/**
	 * 学生姓名
	 */
	private String name;


	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	@Override
	public String toString() {
		return "StudentQuery{" +
				"name='" + name + '\'' +
				'}';
	}
}

springbootTest

因为是springboot中的单元测试,所以先讲SpringbootTest吧!
建立test类之后,在类上加上注解

@SpringBootTest
@RunWith(SpringRunner.class)

用此以表示该类在运行测试时使用SpringbootTest的形式运行。该类让测试类在运行时直接运行整个项目框架,换句话说,他会去寻找application启动类,运行整个项目后运行测试代码,单元测试代码就是整个项目中的一部分。
该测试注解可以在任何测试类中添加,controller、service、mapper、dao都可以添加该注释,这里使用controller给大家演示:

@SpringBootTest
@RunWith(SpringRunner.class)
@AutoConfigureMockMvc
@WebAppConfiguration
public class TestDemoControllerTest {

	@Autowired
	private TestDemoController testDemoController;

	@Autowired
	private MockMvc mockMvc;

	@Test
	public void list() {
		//通过注入的方式直接测试controller中list方法
		StudentQuery studentQuery = new StudentQuery();
		studentQuery.setName("xx");
		List<Student> studentList = testDemoController.list(studentQuery);
		System.out.println(studentList);
	}

	@Test
	public void listByWeb() throws Exception {
		//通过接口的方式测试接口
		this.mockMvc.perform(get("/student/list")
				.contentType(MediaType.APPLICATION_FORM_URLENCODED)
				.param("name","xx"))
				.andReturn();
	}
}

使用常规的Autowired注入controller,类中可直接调用controller(类似于service调用),这样的话可以直接测试controller中的代码逻辑,但会启动整个springboot项目。第二个测试方式是通过webMvc的形式,以解控调用的环境测试controller,使用的是mockMvc,其在Mock工具类中。该工具类模拟了各种接口调用方式,这里只展现了get方法的调用方式。

list方法测试通过结果:
list方法测试结果
listByWeb方法测试通过结果:
listByWeb方法测试结果
可以看出,两种方法不一样的只是调用方式,结果上都是一样的。
那为什么会有两种调用方式?看最后!

Junit

Junit的测试,更注重于代码逻辑,这里进行的单元测试不能识别框架是否能启动,只能识别测试的代码逻辑能否通过,我使用了service进行junit单元测试:

public class TestDemoServiceTest {

	@Test
	public void listStudent() {
		TestDemoService testDemoService = new TestDemoService();

		StudentProperty studentProperty = mock(StudentProperty.class);

		// 构建对象中指定的字段属性
		ReflectionTestUtils.setField(testDemoService,"studentProperty", studentProperty);

		when(studentProperty.findAll()).thenReturn(null);
		List<Student> studentList = testDemoService.listStudent();
		//断言不可能为null
		//Assert.assertNotNull(studentList);
		System.out.println(studentList);

	}

	@Test
	public void isExist() {
	}
}

这里使用了原生的junit,同时也使用了mock工具类,它可以解决在原生单元测试中外部依赖无法引入的问题,同时也可以模拟各项参数以及返回数据,可以测试数据为空,参数为空等场景。

这样测试的好处是只关注该方法的逻辑正确性,并且不在数据库中添加多余的脏数据(甚至不会链接数据库)。例如

// 构建对象中指定的字段属性
		ReflectionTestUtils.setField(testDemoService,"studentProperty", studentProperty);

		when(studentProperty.findAll()).thenReturn(null);

这里就模仿了dao层查询数据后可能会返回的数据,我们可以指定返回什么数据。

每一次返回数据后,请多用assert断言,不符合断言的数据会立即报错,这样会解决很多空指针错误。

更多的mock用法请参考官网mokito

总结

springbootTest和原生的junit各有各的好,我们刚刚完成的项目中单元测试就同时使用了这两种测试。

在service中我们使用junit测试保证代码逻辑的正确性,同时在controller层使用spring boot保证接口的可用性及接口方法逻辑的正确性,同时也确保了在引入外部依赖后框架依然能够正常启动。

前文提到在测试controller中可以用注入,也可以模拟接口,为什么这么做呢,因为直接注入之后无法确定该接口是否能够正常被外部调用,如果传参不对是否会报错,报错是否需要处理,我们无法得知,所以需要一个模拟环境去测试该接口的可用性!

直接注入则是测试从接口到数据库是否是正常的,但前提是service的单元测试已经完成并通过。

posted @ 2021-01-20 12:03  check_bug  阅读(426)  评论(0编辑  收藏  举报