首先来看下TDD三原则吧:
1、除非为了使一个失败的unit test通过,否则不允许编写任何产品代码。
2.在一个单元测试中只允许编写刚好能够导致失败的内容(编译错误也算失败)。
3、只允许编写刚好能够使一个失败的unit test通过的产品代码。
上面是三原则。
好,接下来介绍下在Spring Boot下各层的单元测试如何更快捷的编写,Spring Boot为我们进行单元测试,提供了很多方便的工具和能力。
实体类准备:
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
public class Reservation {
@Id
@GeneratedValue
private Long id;
private String reservationName;
}
本案例使用了lombok(@data等注解)。另外通过@Entity、@Id等等jpa注解来做数据库关系映射。
本文主要介绍如下几方面:
普通测试方法。
jpa测试方法。
repository测试方法。
controller测试方法。
1、model层测试方法
还是从最基本的测试开始吧。
你可以使用Assert或Assertions来进行断言。其中Assert是junit,而Assertions则是AssertJ提供的功能。
Junit不赘述了,来了解下AssertJ,这个是一个号称流式神器,在设计自动化cases时,遵守的核心原则是3A(Arrange-> Actor ->Assert)原则; 断言工具的强大直接影响到用例的执行效率。所以AssertJ备受喜欢。
在spring boot下默认已经为我们引入了:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
而这个starter中则为我们引入了很多方便的断言和测试的功能包。其中就包括
Junit和AssertJ:
事实上,只要我们加入了spring-boot-starter-test这个Starter依赖后(使用test scope),我们就自动为我们的应用添加了如下库:
JUnit —单元测试Java应用程序的事实标准。
AssertJ —流公断言库
Hamcrest — 一个书写匹配器对象时允许直接定义匹配规则的框架.有大量的匹配器是侵入式的,例如UI验证或者数据过滤,但是匹配对象在书写灵活的测试是最常用。Hamcrest从一开始就试图适配不同的单元测试框架.例如,Hamcrest可以使用JUnit3和4和TestNG。在一个现有的测试套件中迁移到使用Hamcrest风格的断言是很容易的,因为其他断言风格可以和Hamcrest的共存。
Mockito — 一个Java mock 框架。
JSONassert — 一个针对JSON进行断言的库。
JsonPath —适用于JSON的XPath。
好,先来看看Junit和AssertJ的写法吧:
public class ReservationTest {
@Test
public void creation(){
Reservation reservation=new Reservation(1L,"Jane");
Assert.assertEquals(reservation.getId(),(Long)1L);
/* Assert.assertThat(reservation.getId(), new BaseMatcher<Long>() {
@Override
public boolean matches(Object o) {
return false;
}
@Override
public void describeTo(Description description) {
}
});*/
//Assert.assertThat(reservation.getId(), Matchers.eq(1L));
Assertions.assertThat(reservation.getReservationName()).isEqualTo("Jane");
}
}
Junit写法:
Assert.assertEquals(reservation.getId(),(Long)1L);
AssertJ写法:
Assertions.assertThat(reservation.getReservationName()).isEqualTo("Jane");
二者风格有什么不同呢?自然即使AssertJ更加的流式一点。
2、Jpa层测试方法
接下来我们介绍一个新的测试工具。
@DataJpaTest
该注解可以与@RunWith(SpringRunner.class)结合使用,用于典型的JPA测试。当你要测试JPA组件的时候适合使用这个注解。
使用这个注解的时候,会禁用完整的自动配置,而只使用与JPA测试相关的配置。
默认情况下,使用@DataJpaTest注解的测试将使用嵌入式内存数据库(替换任何显式或通常自动配置的DataSource)。 @AutoConfigureTestDatabase注解可以用来覆盖这些设置。
如果您正在寻找加载完整的应用程序配置,而不是使用嵌入式数据库,则应将@SpringBootTest与@AutoConfigureTestDatabase结合使用,这时候就不要使用这个注解了。
有关JPA
JPA是Java Persistence API的简称,中文名Java持久层API,是JDK 5.0注解或XML描述对象-关系表的映射关系,并将运行期的实体对象持久化到数据库中。 Sun引入新的JPA ORM规范出于两个原因:其一,简化现有Java EE和Java SE应用开发工作;其二,Sun希望整合ORM技术,实现天下归一。
在spring-boot-starter-test中已为我们提供了@DataJpaTest注解。我们来看看这个注解的具体源码吧:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@OverrideAutoConfiguration(
enabled = false //禁用完整的自动配置,而是仅仅引入JPA相关的配置
)
@TypeExcludeFilters({DataJpaTypeExcludeFilter.class})
@Transactional //支持事务
@AutoConfigureCache //自动配置缓存
@AutoConfigureDataJpa //自动配置数据JPA
@AutoConfigureTestDatabase //自动配置测试数据库,默认是内存内嵌数据库
@AutoConfigureTestEntityManager //自动配置TestEntityManager
@ImportAutoConfiguration
public @interface DataJpaTest {
@PropertyMapping("spring.jpa.show-sql")
boolean showSql() default true;
boolean useDefaultFilters() default true;
Filter[] includeFilters() default {};
Filter[] excludeFilters() default {};
@AliasFor(
annotation = ImportAutoConfiguration.class,
attribute = "exclude"
)
Class<?>[] excludeAutoConfiguration() default {};
}
通过查看源码,我们发现只要我们加了这个注解,就意味着
自动支持了缓存能力(@AutoConfigureCache);
自动支持了事务能力(@Transactional);
自动配置了测试数据库(@AutoConfigureTestDatabase),
自动配置测试数据库(@AutoConfigureTestDatabase)
public @interface AutoConfigureTestDatabase {
EmbeddedDatabaseConnection connection() default EmbeddedDatabaseConnection.NONE;
通过上面的@AutoConfigureTestDatabase源码片段,我们发现了一个EmbeddedDatabaseConnection方法,进去看看吧:
public enum EmbeddedDatabaseConnection {
/**
* No Connection.
*/
NONE(null, null, null),
/**
* H2 Database Connection.
*/
H2(EmbeddedDatabaseType.H2, "org.h2.Driver",
"jdbc:h2:mem:%s;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"),
/**
* Derby Database Connection.
*/
DERBY(EmbeddedDatabaseType.DERBY, "org.apache.derby.jdbc.EmbeddedDriver",
"jdbc:derby:memory:%s;create=true"),
/**
* HSQL Database Connection.
*/
HSQL(EmbeddedDatabaseType.HSQL, "org.hsqldb.jdbcDriver", "jdbc:hsqldb:mem:%s");
发现在spring boot中已经默认为我们内嵌了几个测试数据库连接的支持,分别是h2和hsql。
在本案例中我们是使用的h2内嵌数据库,所以我们只需要在pom中加入h2依赖就可以使用h2了,而不需要我们在本地安装:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
注:
这里我们把scope设置为runtime,原因就是runntime表示被依赖项目无需参与项目的编译,不过后期的测试和运行周期需要其参与。比较常见的如JSR×××的实现,对应的API jar是compile的,具体实现是runtime的,compile只需要知道接口就足够了。oracle jdbc驱动架包就是一个很好的例子,一般scope为runntime。
继续看@DataJpaTest上的其他的注解吧。
@AutoConfigureTestEntityManager
通过这个注解,我们就自动注入了TestEntityManager。一会写单元测试的时候我们要用到。
现在来看看如何测试Jpa吧。
@DataJpaTest
@RunWith(SpringRunner.class)
public class ReservationJapTest {
@Autowired
TestEntityManager testEntityManager;
@Test
public void mapping(){
Reservation jane=testEntityManager.persistAndFlush(
new Reservation(null,"Jane"));
Assertions.assertThat(jane.getId()).isNotNull();
Assertions.assertThat(jane.getReservationName()).isEqualTo("Jane");
}
}
首先我们引入了@DataJpaTest注解,把上面介绍的各种能力配置进来。
然后我们加上@RunWith注解指定为SpringRunner。
然后我们注入刚才通过@DataJpaTest注解配置进来的TestEntityManager:
@Autowired
TestEntityManager testEntityManager;
然后使用testEntityManager的方法persistAndFlush新增一条Jane的数据:
Reservation jane=testEntityManager.persistAndFlush(
new Reservation(null,"Jane"));
然后我们看看有没有保存进去:
Assertions.assertThat(jane.getId()).isNotNull();
Assertions.assertThat(jane.getReservationName()).isEqualTo("Jane");
跑测试,自然是通过了。这个数据就是存储在了内嵌的h2数据库。同样是使用了AssertJ。
3、repository层测试方法。
先来新建一个repository接口:
public interface ReservationRepository extends JpaRepository<Reservation,Long> {
Collection<Reservation> findByReservationName(String reservationName);
}
接口继承自JpaRepository并指定实体类类型和主键类型。
然后新建一个接口方法findByReservationName,一会会用到,就是通过名称查询实体列表:
Collection<Reservation> findByReservationName(String reservationName);
来看看整段代码:
@DataJpaTest
@RunWith(SpringRunner.class)
public class ReservationRepositoryTest {
@Autowired
ReservationRepository reservationRepository;
@Test
public void findByReservationName(){
this.reservationRepository.save(new Reservation(null,"Jane"));
Collection<Reservation> reservations=this.reservationRepository
.findByReservationName("Jane");
Assertions.assertThat(reservations.size()).isEqualTo(1);
Assertions.assertThat(reservations.iterator().next().getId())
.isGreaterThan(0);
Assertions.assertThat(reservations.iterator().next()
.getReservationName().equals("Jane"));
}
}
同样使用@DataJpaTest。然后我们把刚才新建的reservationRepository注入进来。
然后使用reservationRepository保存一个数据:
this.reservationRepository.save(new Reservation(null,"Jane"));
然后使用findByReservationName查询刚刚保存的数据:
Collection<Reservation> reservations=this.reservationRepository
.findByReservationName("Jane");
然后断言看看有没有保存进去:
Assertions.assertThat(reservations.size()).isEqualTo(1);
Assertions.assertThat(reservations.iterator().next().getId())
.isGreaterThan(0);
Assertions.assertThat(reservations.iterator().next()
.getReservationName().equals("Jane"));
跑测试运行通过。另外你也体会到了AssertJ果然是流式断言神器。
4、Controller层测试方法
先创建一个rest controller:
@RestController
public class ReservationRestController {
protected final ReservationRepository reservationRepository;
public ReservationRestController(ReservationRepository reservationRepository) {
this.reservationRepository = reservationRepository;
}
@GetMapping(
value = "/reservations",
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Collection<Reservation> reservations(){
return reservationRepository.findAll();
//return Collections.emptyList();
}
}
通过构造函数注入ReservationRepository。
private final ReservationRepository reservationRepository;
public ReservationRestController(ReservationRepository reservationRepository) {
this.reservationRepository = reservationRepository;
}
然后新建一个get方法:
@GetMapping(
value = "/reservations",
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Collection<Reservation> reservations(){
return reservationRepository.findAll();
//return Collections.emptyList();
}
@WebMvcTest、MockMvc、@MockBean
测试Controller怎么测试呢?Spring Boot也为我们提供了支持。我们只需要在测试类上添加@WebMvcTest自动就配置了MockMvc类。通过MockMvc我们就可以模拟Controller请求了。
下面看看全部代码吧:
@WebMvcTest
@RunWith(SpringRunner.class)
public class ReservationRestControllerTest {
@Autowired
MockMvc mockMvc;
@MockBean
ReservationRepository reservationRepository;
@Test
public void getReservations(){
Mockito.when(reservationRepository.findAll()).
thenReturn(Collections.singletonList(new Reservation(1L,"Jane")));
try {
this.mockMvc.perform(MockMvcRequestBuilders.get("/reservations"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.jsonPath("@.[0].id").value(1L))
.andExpect(MockMvcResultMatchers.jsonPath("@.[0].reservationName").value("Jane"));
} catch (Exception e) {
e.printStackTrace();
}
}
}
通过@MockBean来模拟配置一个bean:ReservationRepository。
然后我们使用Mockito来模拟查询。当我们执行reservationRepository.findAll()时,就返回一条数据:
Collections.singletonList(new Reservation(1L,"Jane"))
接下来我们就使用MockMvc来模拟Controller请求吧。
this.mockMvc.perform(MockMvcRequestBuilders.get("/reservations"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(MockMvcResultMatchers.jsonPath("@.[0].id").value(1L))
.andExpect(MockMvcResultMatchers.jsonPath("@.[0].reservationName").value("Jane
跑测试通过。
本文示例代码在https://github.com/importsource/bootiful-test,或点击“阅读原文”。
总结
本文主要向你介绍了基于Junit以及AssertJ来进行基本的断言,然后向你介绍了如何使用@DataJpaTest对Jpa和Repository进行测试,然后向你介绍了使用@WebMvcTest对Controller进行测试,通过此我们也知道了如何使用@MockBean以及通过MockMvc来模拟一个请求。除了以上这些,还有@JdbcTest让你来测试基于jdbc的代码,以及@DataMongoTest可以测试MongoDB,以及@RestClientTest来测试rest客户端(默认会包含Jackson and GSON)等。
本文分享自 ImportSource 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!