Spring Boot下的TDD(测试驱动开发)

时间:2022-05-06
本文章向大家介绍Spring Boot下的TDD(测试驱动开发),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

首先来看下TDD三原则吧:

  1. You are not allowed to write any production code unless it is to make a failing unit test pass.
  2. You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
  3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test.

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)等。