searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

单元测试实践

2023-08-03 07:30:15
4
0

单元测试实践(junit+mockito)

 

单元测试是保证代码质量和正确性的第一道防线,在开发过程中编写必要的单元测试用例既能保证代码的质量,也有利于项目整体的开发效率。然而在实际开发过程中,存在测试数据难构造,一个方法实现的逻辑较为复杂,有些逻辑对其他模块有依赖等问题,使得编写单测比较困难,有些情况编写单测耗费的精力甚至超过实际的业务逻辑开发。从而让开发人员望而却步,最终放弃单测。

本文介绍基于junit+mockito的单测框架使用,帮助开发人员提升编写单测的效率。

  • Junit4:单元测试框架

  • Mockito:生成模拟对象(造测试数据)

 

依赖配置

<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.12</version>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-core</artifactId>
  <version>3.2.0</version>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <version>1.4.199</version>
  <scope>test</scope>
</dependency>

<!-- mockito static method -->
<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-inline</artifactId>
  <version>4.3.1</version>
  <scope>test</scope>
</dependency>

 

Junit注解说明

junit中提供了一些注解来辅助单测的编写,通过这些注解可以方便的实现数据模拟,逻辑测试等功能。

方法相关注解

注解 描述
@Test 将方法标识为测试方法
@Before 在每次测试之前执行。用于准备测试环境(例如,读取输入数据,初始化类)
@After 每次测试之后执行。用于清理测试环境(例如,删除临时数据,恢复默认值)
@BeforeClass 用于 public static void方法,在所有测试开始之前执行一次。它用于执行耗时的活动,例如:连接数据库
@AfterClass 用于 public static void方法,在完成所有测试之后,执行一次。它用于执行清理活动,例如:与数据库断开连接
@Ignore 指定要忽略的测试
@Test(expected = Exception.class) 如果该方法未引发命名异常,则失败
@Test(timeout=100) 如果该方法花费的时间超过100毫秒,则失败
@RunWith(xxx) 修饰测试类,用于指定运行器,如果不指定则会采用默认的运行器。

 

Mockito相关注解

要使mockito的注解生效,需指定运行器为MockitoJUnitRunner.class

@RunWith(MockitoJUnitRunner.class)

或在初始化方法中使用MockitoAnnotations.initMocks(this)

@BeforeClass
public static void init() {
MockitoAnnotations.initMocks(this);
}
注解 描述
@Mock 被修饰的对象在对方法进行打桩后,返回打桩后指定的结果。需要先打桩再调用。
@Spy 被修饰的对象默认执行原来的逻辑,在对方法进行打桩后,返回打桩后指定的结果。利用该注解可实现自定义真实对象的部分行为。适用于需要模拟一个对象中的少部分方法,对象的大部分方法需执行实际逻辑的情况。
@InjectMocks 被修饰的对象自动注入被@Spy和@Mock标注的对象。

@Spy和@Mock都能用于模拟对象,区别在于:

@Spy:只对打桩过的方法,调用时返回指定的值,对于该对象中其他方法的调用仍将执行实际的函数体。此外,对于打过桩的方法,是否执行实际函数体与打桩方式也有关系。

  • when(...) thenReturn(...)做了真实调用。只是返回了指定的结果

  • doReturn(...) when(...)不做真实调用

(推荐使用下面的方式,可调用doReturn()|doThrow()| doAnswer()|doNothing()|doCallRealMethod() 系列方法。上一种方法因为执行了真实的方法调用,其中可能会抛出异常 )

@Mock:被修饰的对象中所有函数的调用都不会执行实际的函数体。对未打桩的方法直接调用将返回null。

 

junit常用断言

断言用于验证测试结果与期望结果的对比,junit提供以下声明方式。

声明 描述
fail([message]) 使方法失败。在执行测试代码之前,可用于检查未到达代码的特定部分或测试失败
assertTrue([message,]布尔条件) 检查布尔条件是否为真
assertFalse([message,]布尔条件) 检查布尔条件是否为假
assertEquals([message,]预期,实际) 测试两个值是否相同。注意:对于数组,会检查引用而不是数组的内容
assertNull([message,]对象) 检查对象是否为空
assertNotNull([message,]对象) 检查对象是否不为空
assertSame([message,]预期,实际) 检查两个变量是否引用同一对象
assertNotSame([message,]预期,实际) 检查两个变量是否引用了不同的对象

 

mock应用示例

在编写单测用例时,往往最大的痛点在于如何造出与测试逻辑无关的中间数据。下面介绍几种模拟函数返回值的数据mock方法。

有返回值函数的mock

Class Person {
  private Integer id = 1;
  private String name = "jack";
  private List<String> hobby;
  public String getName() {
      return this.name;
  }
  public String getId() {
      return this.Id;
  }
  public void setHobby(List<String> hobby) {
  this.hobby = hobby;
  }
}

@Spy
Person person;
when(person.getName()).thenReturn("xiaoming");
doReturn("xiaoming").when(person).getName();
String name = person.getName() //name值将为"xiaoming"
Integer id = person.getId() //此时会调用实际的执行逻辑,id值为1

 

viod函数的mock

对于void函数,无法采用上述打桩方式来模拟函数返回值。可采取以下机制:

  1. doNothing() :完全忽略对void方法的调用,这是默认行为

  2. doAnswer() :在调用void方法时执行一些运行时或复杂的操作

  3. doThrow() : 调用模拟的 void方法时引发异常

  4. doCallRealMethod() :不要模拟并调用真实方法

若需对方法输入参数做出模拟修改,可采用doAnswer()模式:

doAnswer(invocation -> {
  Object[] args = invocation.getArguments();
  List<String> strs = (List<String>) args[0];
  strs.add("run");
  return null;}).when(person).setHobby(anyList());

经过以上打桩后,在测试中调用person.change(list)方法后,list列表中将多出“run”元素。

 

静态函数的mock

从3.4.0版本开始,mokito支持mock静态方法。

 

Mockito提供了mockStatic静态方法mock包含静态方法的类,返回一个MockedStatic对象。MockedStatic使用完后需及时释放,mock逻辑只在MockedStatic对象释放之前有效,一旦被释放,就会返回到它的原始调用逻辑。

class StaticClass {
  public static int add(int... args) {
  return IntStream.of(args).sum();
  }
}
 
  @Test
  public void testAdd() {
      //实际的方法调用
      assertEquals(5, StaticClass.add(2, 3));
      try (MockedStatic mockStatic = Mockito.mockStatic(ClassWithStaticMethod.class)) {
          mockStatic.when(() -> ClassWithStaticMethod.add(anyInt(), anyInt())).thenReturn(10);
          // 返回mock的结果10
          assertEquals(10, ClassWithStaticMethod.add(2, 3));
          mockStatic.verify(() -> ClassWithStaticMethod.add(2, 3));
      }
      //实际的方法调用
      assertEquals(5, ClassWithStaticMethod.add(2, 3));
  }

 

 

模拟数据库交互

采用h2内存数据库

private static JdbcTemplate jdbcTemplate;
//xxx.sql为创建数据库的脚本,存放在resource目录下

@BeforeClass
public static void start() {
  DataSource dataSource = new EmbeddedDatabaseBuilder()
          .setType(EmbeddedDatabaseType.H2)
          .addScript("classpath:/H2_TYPE.sql")
          .addScript("classpath:/INIT_DATA_ADD_USER.sql")
          .build();
  jdbcTemplate = new JdbcTemplate(dataSource);
}

H2_TYPE.sql(指定数据库类型)

SET MODE MySQL;

INIT_DATA_ADD_USER.sql(创建库表,初始化数据)

create SCHEMA `mysql`;

CREATE TABLE `mysql`.`user` (
 `Host` char(60) DEFAULT '',
 `User` char(32) DEFAULT '',
 `Select_priv`   char(2) DEFAULT 'N',
 `Insert_priv`   char(2) DEFAULT 'N',
 `Update_priv`   char(2) DEFAULT 'N',
 `Delete_priv`   char(2) DEFAULT 'N',
 `Create_priv`   char(2) DEFAULT 'N',
 `Drop_priv`   char(2) DEFAULT 'N',
 `Reload_priv`   char(2) DEFAULT 'N',
 `Shutdown_priv`   char(2) DEFAULT 'N',
 `Process_priv`   char(2) DEFAULT 'N',
 `File_priv`   char(2) DEFAULT 'N',
 `Grant_priv`   char(2) DEFAULT 'N',
 `References_priv`   char(2) DEFAULT 'N',
 `Index_priv`   char(2) DEFAULT 'N',
 `Alter_priv`   char(2) DEFAULT 'N',
 `Show_db_priv`   char(2) DEFAULT 'N',
 `Super_priv`   char(2) DEFAULT 'N',
 `Create_tmp_table_priv`   char(2) DEFAULT 'N',
 `Lock_tables_priv`   char(2) DEFAULT 'N',
 `Execute_priv`   char(2) DEFAULT 'N',
 `Repl_slave_priv`   char(2) DEFAULT 'N',
 `Repl_client_priv`   char(2) DEFAULT 'N',
 `Create_view_priv`   char(2) DEFAULT 'N',
 `Show_view_priv`   char(2) DEFAULT 'N',
 `Create_routine_priv`   char(2) DEFAULT 'N',
 `Alter_routine_priv`   char(2) DEFAULT 'N',
 `Create_user_priv`   char(2) DEFAULT 'N',
 `Event_priv`   char(2) DEFAULT 'N',
 `Trigger_priv`   char(2) DEFAULT 'N',
 `Create_tablespace_priv`   char(2) DEFAULT 'N',
 PRIMARY KEY (`Host`,`User`)
);

insert into `mysql`.`user` values ('%','test_user', 'Y', 'Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y');

 

编写注意点

单元测试的编写与待测试方法的代码结构密切相关,待测试方法实现的方式直接影响到单元测试用例的编写难度。如果发现单元测试很难编写,就要考虑一下待测试方法的实现是不是可以优化。

建议:

1、一个方法中实现的逻辑尽量不要太复杂;

2、与外部模块有交互的部分单独写一个函数实现;

3、不是与待测试方法主要逻辑相关的部分,可以采用mock方法模拟输出。

0条评论
0 / 1000
章****锐
2文章数
0粉丝数
章****锐
2 文章 | 0 粉丝
章****锐
2文章数
0粉丝数
章****锐
2 文章 | 0 粉丝
原创

单元测试实践

2023-08-03 07:30:15
4
0

单元测试实践(junit+mockito)

 

单元测试是保证代码质量和正确性的第一道防线,在开发过程中编写必要的单元测试用例既能保证代码的质量,也有利于项目整体的开发效率。然而在实际开发过程中,存在测试数据难构造,一个方法实现的逻辑较为复杂,有些逻辑对其他模块有依赖等问题,使得编写单测比较困难,有些情况编写单测耗费的精力甚至超过实际的业务逻辑开发。从而让开发人员望而却步,最终放弃单测。

本文介绍基于junit+mockito的单测框架使用,帮助开发人员提升编写单测的效率。

  • Junit4:单元测试框架

  • Mockito:生成模拟对象(造测试数据)

 

依赖配置

<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.12</version>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-core</artifactId>
  <version>3.2.0</version>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <version>1.4.199</version>
  <scope>test</scope>
</dependency>

<!-- mockito static method -->
<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-inline</artifactId>
  <version>4.3.1</version>
  <scope>test</scope>
</dependency>

 

Junit注解说明

junit中提供了一些注解来辅助单测的编写,通过这些注解可以方便的实现数据模拟,逻辑测试等功能。

方法相关注解

注解 描述
@Test 将方法标识为测试方法
@Before 在每次测试之前执行。用于准备测试环境(例如,读取输入数据,初始化类)
@After 每次测试之后执行。用于清理测试环境(例如,删除临时数据,恢复默认值)
@BeforeClass 用于 public static void方法,在所有测试开始之前执行一次。它用于执行耗时的活动,例如:连接数据库
@AfterClass 用于 public static void方法,在完成所有测试之后,执行一次。它用于执行清理活动,例如:与数据库断开连接
@Ignore 指定要忽略的测试
@Test(expected = Exception.class) 如果该方法未引发命名异常,则失败
@Test(timeout=100) 如果该方法花费的时间超过100毫秒,则失败
@RunWith(xxx) 修饰测试类,用于指定运行器,如果不指定则会采用默认的运行器。

 

Mockito相关注解

要使mockito的注解生效,需指定运行器为MockitoJUnitRunner.class

@RunWith(MockitoJUnitRunner.class)

或在初始化方法中使用MockitoAnnotations.initMocks(this)

@BeforeClass
public static void init() {
MockitoAnnotations.initMocks(this);
}
注解 描述
@Mock 被修饰的对象在对方法进行打桩后,返回打桩后指定的结果。需要先打桩再调用。
@Spy 被修饰的对象默认执行原来的逻辑,在对方法进行打桩后,返回打桩后指定的结果。利用该注解可实现自定义真实对象的部分行为。适用于需要模拟一个对象中的少部分方法,对象的大部分方法需执行实际逻辑的情况。
@InjectMocks 被修饰的对象自动注入被@Spy和@Mock标注的对象。

@Spy和@Mock都能用于模拟对象,区别在于:

@Spy:只对打桩过的方法,调用时返回指定的值,对于该对象中其他方法的调用仍将执行实际的函数体。此外,对于打过桩的方法,是否执行实际函数体与打桩方式也有关系。

  • when(...) thenReturn(...)做了真实调用。只是返回了指定的结果

  • doReturn(...) when(...)不做真实调用

(推荐使用下面的方式,可调用doReturn()|doThrow()| doAnswer()|doNothing()|doCallRealMethod() 系列方法。上一种方法因为执行了真实的方法调用,其中可能会抛出异常 )

@Mock:被修饰的对象中所有函数的调用都不会执行实际的函数体。对未打桩的方法直接调用将返回null。

 

junit常用断言

断言用于验证测试结果与期望结果的对比,junit提供以下声明方式。

声明 描述
fail([message]) 使方法失败。在执行测试代码之前,可用于检查未到达代码的特定部分或测试失败
assertTrue([message,]布尔条件) 检查布尔条件是否为真
assertFalse([message,]布尔条件) 检查布尔条件是否为假
assertEquals([message,]预期,实际) 测试两个值是否相同。注意:对于数组,会检查引用而不是数组的内容
assertNull([message,]对象) 检查对象是否为空
assertNotNull([message,]对象) 检查对象是否不为空
assertSame([message,]预期,实际) 检查两个变量是否引用同一对象
assertNotSame([message,]预期,实际) 检查两个变量是否引用了不同的对象

 

mock应用示例

在编写单测用例时,往往最大的痛点在于如何造出与测试逻辑无关的中间数据。下面介绍几种模拟函数返回值的数据mock方法。

有返回值函数的mock

Class Person {
  private Integer id = 1;
  private String name = "jack";
  private List<String> hobby;
  public String getName() {
      return this.name;
  }
  public String getId() {
      return this.Id;
  }
  public void setHobby(List<String> hobby) {
  this.hobby = hobby;
  }
}

@Spy
Person person;
when(person.getName()).thenReturn("xiaoming");
doReturn("xiaoming").when(person).getName();
String name = person.getName() //name值将为"xiaoming"
Integer id = person.getId() //此时会调用实际的执行逻辑,id值为1

 

viod函数的mock

对于void函数,无法采用上述打桩方式来模拟函数返回值。可采取以下机制:

  1. doNothing() :完全忽略对void方法的调用,这是默认行为

  2. doAnswer() :在调用void方法时执行一些运行时或复杂的操作

  3. doThrow() : 调用模拟的 void方法时引发异常

  4. doCallRealMethod() :不要模拟并调用真实方法

若需对方法输入参数做出模拟修改,可采用doAnswer()模式:

doAnswer(invocation -> {
  Object[] args = invocation.getArguments();
  List<String> strs = (List<String>) args[0];
  strs.add("run");
  return null;}).when(person).setHobby(anyList());

经过以上打桩后,在测试中调用person.change(list)方法后,list列表中将多出“run”元素。

 

静态函数的mock

从3.4.0版本开始,mokito支持mock静态方法。

 

Mockito提供了mockStatic静态方法mock包含静态方法的类,返回一个MockedStatic对象。MockedStatic使用完后需及时释放,mock逻辑只在MockedStatic对象释放之前有效,一旦被释放,就会返回到它的原始调用逻辑。

class StaticClass {
  public static int add(int... args) {
  return IntStream.of(args).sum();
  }
}
 
  @Test
  public void testAdd() {
      //实际的方法调用
      assertEquals(5, StaticClass.add(2, 3));
      try (MockedStatic mockStatic = Mockito.mockStatic(ClassWithStaticMethod.class)) {
          mockStatic.when(() -> ClassWithStaticMethod.add(anyInt(), anyInt())).thenReturn(10);
          // 返回mock的结果10
          assertEquals(10, ClassWithStaticMethod.add(2, 3));
          mockStatic.verify(() -> ClassWithStaticMethod.add(2, 3));
      }
      //实际的方法调用
      assertEquals(5, ClassWithStaticMethod.add(2, 3));
  }

 

 

模拟数据库交互

采用h2内存数据库

private static JdbcTemplate jdbcTemplate;
//xxx.sql为创建数据库的脚本,存放在resource目录下

@BeforeClass
public static void start() {
  DataSource dataSource = new EmbeddedDatabaseBuilder()
          .setType(EmbeddedDatabaseType.H2)
          .addScript("classpath:/H2_TYPE.sql")
          .addScript("classpath:/INIT_DATA_ADD_USER.sql")
          .build();
  jdbcTemplate = new JdbcTemplate(dataSource);
}

H2_TYPE.sql(指定数据库类型)

SET MODE MySQL;

INIT_DATA_ADD_USER.sql(创建库表,初始化数据)

create SCHEMA `mysql`;

CREATE TABLE `mysql`.`user` (
 `Host` char(60) DEFAULT '',
 `User` char(32) DEFAULT '',
 `Select_priv`   char(2) DEFAULT 'N',
 `Insert_priv`   char(2) DEFAULT 'N',
 `Update_priv`   char(2) DEFAULT 'N',
 `Delete_priv`   char(2) DEFAULT 'N',
 `Create_priv`   char(2) DEFAULT 'N',
 `Drop_priv`   char(2) DEFAULT 'N',
 `Reload_priv`   char(2) DEFAULT 'N',
 `Shutdown_priv`   char(2) DEFAULT 'N',
 `Process_priv`   char(2) DEFAULT 'N',
 `File_priv`   char(2) DEFAULT 'N',
 `Grant_priv`   char(2) DEFAULT 'N',
 `References_priv`   char(2) DEFAULT 'N',
 `Index_priv`   char(2) DEFAULT 'N',
 `Alter_priv`   char(2) DEFAULT 'N',
 `Show_db_priv`   char(2) DEFAULT 'N',
 `Super_priv`   char(2) DEFAULT 'N',
 `Create_tmp_table_priv`   char(2) DEFAULT 'N',
 `Lock_tables_priv`   char(2) DEFAULT 'N',
 `Execute_priv`   char(2) DEFAULT 'N',
 `Repl_slave_priv`   char(2) DEFAULT 'N',
 `Repl_client_priv`   char(2) DEFAULT 'N',
 `Create_view_priv`   char(2) DEFAULT 'N',
 `Show_view_priv`   char(2) DEFAULT 'N',
 `Create_routine_priv`   char(2) DEFAULT 'N',
 `Alter_routine_priv`   char(2) DEFAULT 'N',
 `Create_user_priv`   char(2) DEFAULT 'N',
 `Event_priv`   char(2) DEFAULT 'N',
 `Trigger_priv`   char(2) DEFAULT 'N',
 `Create_tablespace_priv`   char(2) DEFAULT 'N',
 PRIMARY KEY (`Host`,`User`)
);

insert into `mysql`.`user` values ('%','test_user', 'Y', 'Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y');

 

编写注意点

单元测试的编写与待测试方法的代码结构密切相关,待测试方法实现的方式直接影响到单元测试用例的编写难度。如果发现单元测试很难编写,就要考虑一下待测试方法的实现是不是可以优化。

建议:

1、一个方法中实现的逻辑尽量不要太复杂;

2、与外部模块有交互的部分单独写一个函数实现;

3、不是与待测试方法主要逻辑相关的部分,可以采用mock方法模拟输出。

文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
0
0