专栏首页ImportSourceSpring Boot下的TDD(测试驱动开发)

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

本文分享自微信公众号 - ImportSource(importsource),作者:贺卓凡

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2017-11-26

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • java中的Checked Exceptions和Unchecked Exceptions

    在java世界中有两种异常,一种Checked Exceptions ,另一种叫Unchecked Exceptions. 1) Checked Except...

    ImportSource
  • 自己动手写区块链-发起一笔交易(Java版)

    本文我们将会做以下事情: 1、创建一个钱包(wallet)。 2、使用我们的前面创建的区块链发送一笔签名的交易出去。 3、还有其他更叼的事情等等。 听起来是不是...

    ImportSource
  • 使用Kotlin开发一个Spring Boot Webflux Streaming应用(附视频)

    首先我们来看一段视频,然后开始今天的内容: ? 上面这样的效果正是我们使用spring webflux实现的。 整体思路 整个项目分为两部分: 1、servic...

    ImportSource
  • 企业级渗透测试服务思考

    随着企业组织的业务规模不断扩大,信息化运用快速发展,业务与数据安全已经被推上战争的高地,成为企业保护自身安全的重中之重,成为企业信息安全官在战略计划汇报中不得不...

    FB客服
  • 《大话计算机》之:趣味了解浮点数(上)

    由ssdfans团队所著《深入理解SSD》一书已经出版电子版,详情及购买见固态硬盘掉电怎么恢复数据一文结尾。将本帖转发到朋友圈并截图发到本公众号首页窗口,冬瓜哥...

    冬瓜哥
  • 【C#】分享一个弹出容器层,像右键菜单那样召即来挥则去

    ------------------201508261813更新(源码有更新、Demo未更新)------------------

    AhDung
  • Master-Worker模式实现立方和相加

    Master-Worker框架如下,首先实现的Master线程,主要用作分配任务,和返回结果集。

    小鄧子
  • SpringBoot非官方教程 | 第二十二篇: 创建含有多module的springboot工程

    这篇文章主要介绍如何在springboot中如何创建含有多个module的工程,栗子中含有两个 module,一个作为libarary. 工程,另外一个是主工程...

    方志朋
  • monit介绍和配置 原

        monit监控和管理进程、程序、文件、目录和Unix系统的文件的工具。可以进行自动维护和修理,在错误的情况下执行有意义的因果关系的行动。比如,某个进程没...

    拓荒者
  • 走进Java接口测试之效率插件lombok

    我们在开发测试代码过程中,通常都会定义大量的 JavaBean ,然后通过IDE 去生成其属性的构造器、getter、setter、equals、hashcod...

    高楼Zee

扫码关注云+社区

领取腾讯云代金券