SpringBoot单元测试

Spring Boot Test

Spring Boot提供了一些注解和工具去帮助开发者测试他们的应用。相较于SpringBoot1.3,SpringBoot1.4对测试有了大的改进,以下示例适用SpringBoot1.4.1以及以上版本。在项目中使用Spring Boot Test支持,只需要在pom.xml引入如下配置即可:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

1. spring-boot-start-test

SpringBoot提供了spring-boot-start-test启动器,该启动器提供了常见的单元测试库:

  • JUnit: 一个Java语言的单元测试框架

  • Spring Test & Spring Boot Test:为Spring Boot应用提供集成测试和工具支持

  • AssertJ:支持流式断言的Java测试框架

  • Hamcrest:一个匹配器库

  • Mockito:一个java mock框架

  • JSONassert:一个针对JSON的断言库

  • JsonPath:JSON XPath库

2. 常用注解

这里介绍一些Spring Boot单元测试常用的注解,更多详细请到Spring Boot官网[查看]
(http://docs.spring.io/spring-boot/docs/1.4.1.RELEASE/reference/htmlsingle/#boot-features-testing)。

  • @RunWith(SpringRunner.class)

    JUnit运行使用Spring的测试支持。SpringRunner是SpringJUnit4ClassRunner的新名字,这样做的目的
    仅仅是为了让名字看起来更简单一点。

  • @SpringBootTest

    该注解为SpringApplication创建上下文并支持Spring Boot特性,其webEnvironment提供如下配置:

    Mock-加载WebApplicationContext并提供Mock Servlet环境,嵌入的Servlet容器不会被启动。

    RANDOM_PORT-加载一个EmbeddedWebApplicationContext并提供一个真实的servlet环境。嵌入的Servlet容器将被启动并在一个随机端口上监听。

    DEFINED_PORT-加载一个EmbeddedWebApplicationContext并提供一个真实的servlet环境。嵌入的Servlet容器将被启动并在一个默认的端口上监听
    (application.properties配置端口或者默认端口8080)。

    NONE-使用SpringApplication加载一个ApplicationContext,但是不提供任何的servlet环境。

  • @MockBean

    在你的ApplicationContext里为一个bean定义一个Mockito mock。

  • @SpyBean

    定制化Mock某些方法。使用@SpyBean除了被打过桩的函数,其它的函数都将真实返回。

  • @WebMvcTest

    该注解被限制为一个单一的controller,需要利用@MockBean去Mock合作者(如service)。

测试用例设计

1. 测试用例设计方法

根据目前现状,单元测试主要用来进行程序核心逻辑测试。逻辑覆盖测试是通过对程序逻辑结构的遍历来实现程序逻辑覆盖。从对源代码的覆盖程度不同分为以下六种标准,本文只对其中的五种进行分析(路径覆盖除外),下面从一段代码开始。

    public int example(int x, int y, int z){
        if (x>1 && z>2){
            x = x + y;
        }
        if (y == 3 || x > 5){
            x = x - 2;
        }
        return x;
    }

一般单元测试不会根据代码来写用例,而是会根据流程图来编写测试用例,以上代码画出的流程图如下:


函数流程图
  • 语句覆盖

    1. 概念

    设计足够多的测试用例,使得被测试程序中的每条可执行语句至少被执行一次。

    2. 测试用例

| 数据 | 执行路径 |
|: ------------------ |:--------------------------:|
| {x=6;y=3;z=3} | a->c->b->d->e->f |

3. 测试的充分性
 假设语句`x1&&z>2`中的`&&`写成了`||`上面的测试用例是检查不出来的。
  • 判定覆盖

    1. 概念

    设计足够的测试用例使得代码中的判断分支至少被执行一次。我们标记x>1&&z>2 为P1 y==3 || x>5为P2。

2. 测试用例

| 数据 | P1 | P2 | 执行路径 |
|: ------------------ |:--------------------------:|:------------------:|:------------------:|
| {x=3;y=3;z=3} |T| T | a->c->b->d->e->f |
| {x=0;y=2;z=3} |F|F| a->c->d->f |

3. 测试的充分性
假设语句`y==3 || x>5`中的`||`写成了`&&`上面的测试用例是检查不出来的。和语句覆盖相比:由于判定覆盖不是在判断假分支就是在判断真分支,所以满足了判定覆盖就一定会满足语句覆盖。
  • 条件覆盖
1. 概念
 设计足够多的测试用例,使得被测试程序每个判断语句中的每个逻辑条件的可能值至少满足一次。在本例中有两个判断分支`(x>1&&z>2)`和` (y == 3 || x > 5)`分别记为P1和P2。总共有三个条件`x>1`、`z>2`、`y==3`和`x>5`分别记为B1、B2、B3、B4。
2. 测试用例
数据 P1 P2 B1 B2 B3 B4 执行路径
{x=0;y=2;z=3} F F F T F F a->c->d->f
{x=3;y=3;z=1} F T T F T T a->c->d->f
3. 测试的充分性
从上面的结论看,条件覆盖没法满足100%的语句覆盖,当然没法满足100%的判定覆盖。
  • 判定/条件覆盖
1. 概念
同时满足100%的条件覆盖和100%的判定覆盖。
2. 测试用例
数据 P1 P2 B1 B2 B3 B4 执行路径
{x=0;y=2;z=1} F F F F F F a->c->d->f
{x=3;y=3;z=3} T T T T T T a->c->b->d->e->f
##### 3. 测试的充分性
达到100%判定-条件覆盖标准一定能够达到100%条件覆盖、100%判定覆盖和100%语句覆盖。
  • 条件组合覆盖

    1. 概念

    设计足够多的测试用例,使得被测试程序中的每个判断的所有可能条件取值的组合至少被满足一次。
    注意:

    • 条件组合只针对同一个判断语句内存在多个判断条件,让这些条件的取值进行笛卡尔乘积组合。
    • 不同判断语句内的条件无需组合
    • 对于单条件语句,只需要满足自己的所有取值即可

    本例中判断(x>1&&z>2)有如下组合:(1)x>1&&z>2 (2)x>1&&z<=2 (3)x<=1&&z>2 (4) x<=1&&z<=2;判断 (y == 3 || x > 5)有如下组合(1)y==3||x>5 (2)y==3||x<=5 (3)y!=3||x>5 (4)y!=3||x<=5

2. 测试用例
条件组合 数据 执行路径
x>1 and z>2 y==3 or x>5 {x=3;z=3;y=3} a->c->b->d->e->f
x>1 and z<=2 y==3 or x<=5 {x=2;z=3;y=3} a->c->b->d->f
x<=1 and z>2 y!=3 or x>5 {x=1;z=3;y=5} a->c->d->e->f
x<=1 and z<=2 y!=3 or x<=5 {x=1;z=2;y=3} a->c->d->f
3. 测试的充分性
100%满足条件组合标准一定满足100%条件覆盖标准和100%判定覆盖标准。

在单元测试中替代Get和Post请求测试方式

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ScoreControllerTestNew {
    @Autowired
    private TestRestTemplate restTemplate;
    @Test
    public void testScore(){
        String jsonStr = "{\"data\":{\"debit_account_balance_code\":40,\"credit_consume_count\":1,\"debit_start_age\":1,\"debit_consume_sum_code\":2,\"age\":38},\"modelProductId\":55}";
        String content = this.restTemplate.postForObject("/scoreApi/score", jsonStr, String.class );
        assertThat(content).isEqualTo("{\"result\":{\"score\":\"300\",\"logit\":21.144779999999997},\"response_code\":\"00\",\"response_msg\":\"success\"}");
    }
}

测试Controller

在测试Controller时需要进行隔离测试,这个时候需要Mock Service层的服务。

@RunWith(SpringRunner.class)
@WebMvcTest(ScoreController.class)
public class ScoreControllerTestNew {
    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private ICalculateService calculateService;
    @MockBean
    private IModelMonitorService modelMonitorService;
    @MockBean
    private IScoreConfigService scoreConfigService;
    @MockBean
    private IModelProductService modelProductService;
    @Before
    public void setUp(){
    }
    @Test
    public void testScore() throws Exception {
        given(this.modelProductService.get(anyLong()))
                .willReturn(null);
        String jsonStr = "{\"data\":{\"debit_account_balance_code\":40,\"credit_consume_count\":1,\"debit_start_age\":1,\"debit_consume_sum_code\":2,\"age\":38},\"modelProductId\":5}";
        RequestBuilder requestBuilder = null;
        requestBuilder = post("/scoreApi/score").contentType(MediaType.APPLICATION_JSON).content(jsonStr);
        this.mockMvc.perform(requestBuilder).andExpect(status().isOk()).andExpect(MockMvcResultMatchers.content().string("{}"));
    }
}

测试Service

测试Service和测试Controller类似,同样采用隔离法。

@RunWith(SpringRunner.class)
@SpringBootTest
public class ServiceTest {
    @MockBean
    private ModelMonitorMapper modelMonitorMapper;
    @Autowired
    private IModelMonitorService modelServiceServiceImpl;
    @Test
    public void testModelServiceServiceImpl(){
        given(modelMonitorMapper.insert(anyObject()))
                .willReturn(0);
        int n =  modelServiceServiceImpl.insert(new ModelMonitor());
        assertThat(n).isEqualTo(0);
    }
}

测试Dao

测试的时候为了防止引入脏数据使用注解@Transactional和@Rollback在测试完成后进行回滚。

@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class ScoreControllerTestNew {

    @Autowired
    private ModelMonitorMapper modelMonitorMapper;
    @Test
    @Rollback
    public void testDao() throws Exception {
        ModelMonitor modelMonitor = new ModelMonitor();
        modelMonitor.setModelProductId(Long.parseLong("5"));
        modelMonitor.setLogit(21.144779999999997);
        modelMonitor.setDerivedVariables("{\"debit_account_balance_code\":1.0,\"credit_consume_count\":1.0,\"debit_start_age\":1.0,\"debit_consume_sum_code\":1.0,\"age\":1.0}");
        modelMonitor.setScore("300");
        modelMonitor.setSrcData("{\"data\":{\"debit_account_balance_code\":40,\"credit_consume_count\":1,\"debit_start_age\":1,\"debit_consume_sum_code\":2,\"age\":38},\"modelProductId\":5}");
        int n = modelMonitorMapper.insert(modelMonitor);
        assertThat(n).as("检查数据是否成功插入").isEqualTo(0);
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,706评论 4 366
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,002评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,462评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,375评论 0 216
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,763评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,849评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,033评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,768评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,490评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,734评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,204评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,566评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,227评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,137评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,934评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,926评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,774评论 2 274

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,105评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,380评论 6 343
  • SpringBoot-单元测试 一、对service进行测试: 在service中建立要测试的方法: 在test文...
    我可能是个假开发阅读 2,071评论 0 8
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,296评论 18 399
  • Bistro Bon Bon的的确确是魔都版的深夜食堂,晚上7:30才营业到凌晨,我第一次遇到有这么一家店老板与顾...
    桂花糕Quincy阅读 166评论 0 2