单元测试指南

发表于:2018-5-16 14:35

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:Blinkfox    来源:个人博客

分享:
  (6). 验证执行执行顺序
  // A. 验证mock一个对象的函数执行顺序
  // 创建Mock对象
  List singleMock = mock(List.class);
  // 使用mock对象
  singleMock.add("was added first");  
  singleMock.add("was added second");
  // 为该mock对象创建一个inOrder对象
  InOrder inOrder = inOrder(singleMock);
  // 确保add函数首先执行的是add("was added first"),然后才是add("was added second")
  inOrder.verify(singleMock).add("was added first");  
  inOrder.verify(singleMock).add("was added second");
  // B .验证多个mock对象的函数执行顺序
  List firstMock = mock(List.class);  
  List secondMock = mock(List.class);
  // 使用mock对象
  firstMock.add("was called first");  
  secondMock.add("was called second");
  // 为这两个Mock对象创建inOrder对象
  InOrder inOrder = inOrder(firstMock, secondMock);
  // 验证它们的执行顺序
  inOrder.verify(firstMock).add("was called first");  
  inOrder.verify(secondMock).add("was called second");  
  验证执行顺序是非常灵活的。你不需要一个一个的验证所有交互,只需要验证你感兴趣的对象即可。另外,你可以仅通过那些需要验证顺序的mock对象来创建InOrder对象。
  (7). 确保交互(interaction)操作不会执行在mock对象上
  // 使用Mock对象
  mockOne.add("one");
  // 普通验证
  verify(mockOne).add("one");
  // 验证某个交互是否从未被执行
  verify(mockOne, never()).add("two");
  // 验证mock对象没有交互过
  verifyZeroInteractions(mockTwo, mockThree);  
  (8). 查找冗余的调用
  // 使用mock对象
  mockedList.add("one");  
  mockedList.add("two");
  verify(mockedList).add("one");
  // 下面的验证将会失败
  verifyNoMoreInteractions(mockedList);  
  一些用户可能会在频繁地使用verifyNoMoreInteractions(),甚至在每个测试函数中都用。但是verifyNoMoreInteractions()并不建议在每个测试函数中都使用。verifyNoMoreInteractions()在交互测试套件中只是一个便利的验证,它的作用是当你需要验证是否存在冗余调用时。滥用它将导致测试代码的可维护性降低。你可以阅读这篇文档来了解更多相关信息。
  (9). 简化mock对象的创建
  最小化重复的创建代码;
  使测试类的代码可读性更高;
  使验证错误更易于阅读,因为字段名可用于标识mock对象;
  public class ArticleManagerTest {
     @Mock private ArticleCalculator calculator;
     @Mock private ArticleDatabase database;
     @Mock private UserProvider userProvider;
     private ArticleManager manager;
  注意!下面这句代码需要在运行测试函数之前被调用,一般放到测试类的基类或者test runner中:
  MockitoAnnotations.initMocks(testClass);  
  关于mock注解的更多信息可以阅读MockitoAnnotations文档。
  (10). 为连续的调用做测试打桩 (stub)
  有时我们需要为同一个函数调用的不同的返回值或异常做测试桩。
  when(mock.someMethod("some arg"))  
      .thenThrow(new RuntimeException())
      .thenReturn("foo");
  // 第一次调用 : 抛出运行时异常
  mock.someMethod("some arg");
  // 第二次调用 : 输出"foo"
  System.out.println(mock.someMethod("some arg"));
  // 后续调用 : 也是输出"foo"
  System.out.println(mock.someMethod("some arg"));  
  另外,连续调用的另一种更简短的版本 :
  // 第一次调用时返回"one",第二次返回"two",第三次返回"three"
  when(mock.someMethod("some arg"))  
      .thenReturn("one", "two", "three");
  (11). 为回调做测试桩
  when(mock.someMethod(anyString())).thenAnswer(new Answer() {  
       Object answer(InvocationOnMock invocation) {
           Object[] args = invocation.getArguments();
           Object mock = invocation.getMock();
           return "called with arguments: " + args;
       }
  });
  // 输出 : "called with arguments: foo"
  System.out.println(mock.someMethod("foo"));  
  (12). 监控真实对象
  你可以为真实对象创建一个监控(spy)对象。当你使用这个spy对象时真实的对象也会也调用,除非它的函数被stub了。尽量少使用spy对象,使用时也需要小心形式,例如spy对象可以用来处理遗留代码。
  List list = new LinkedList();  
  List spy = spy(list);
  // 你可以为某些函数打桩
  when(spy.size()).thenReturn(100);
  // 通过spy对象调用真实对象的函数
  spy.add("one");  
  spy.add("two");
  // 输出第一个元素
  System.out.println(spy.get(0));
  // 因为size()函数被打桩了,因此这里返回的是100
  System.out.println(spy.size());
  // 交互验证
  verify(spy).add("one");  
  verify(spy).add("two");  
  Mockito 并不会为真实对象代理函数调用,实际上它会拷贝真实对象。因此如果你保留了真实对象并且与之交互,不要期望从监控对象得到正确的结果。当你在监控对象上调用一个没有被stub的函数时并不会调用真实对象的对应函数,你不会在真实对象上看到任何效果。
  因此结论就是: 当你在监控一个真实对象时,你想在stub这个真实对象的函数,那么就是在自找麻烦。或者你根本不应该验证这些函数。
  (13). 重置mocks对象
  聪明的 Mockito 使用者很少会用到这个特性,因为他们知道这是出现糟糕测试单元的信号。通常情况下你不会需要重设你的测试单元,只需要为每一个测试方法重新创建一个测试单元就可以了。
  如果你真的想通过reset()方法满足某些需求的话,请考虑实现简单,小而且专注于测试方法而不是冗长,精确的测试。首先可能出现的代码异味就是测试方法中间那的reset()方法。这可能意味着你已经过度测试了。
  添加 reset() 方法的唯一原因就是让它能与容器注入的测试单元协作。
  List mock = mock(List.class);  
  when(mock.size()).thenReturn(10);  
  mock.add(1);
  reset(mock);  
  //at this point the mock forgot any interactions & stubbing
  (14). 更多的注解
  @Captor: 创建ArgumentCaptor。
  @Spy: 可以代替spy(Object)。
  @InjectMocks: 如果此注解声明的变量需要用到mock对象,mockito会自动注入mock或spy成员。
  //可以这样写
  @Spy
  BeerDrinker drinker = new BeerDrinker();
  //也可以这样写,mockito会自动实例化drinker.
  @Spy
  BeerDrinker drinker;
  //会自动实例化LocalPub
  @InjectMocks
  LocalPub pub;  
  (15). BDD 风格的验证(Since 1.10.0)
  开启Behavior Driven Development(BDD,即行为驱动开发)风格的验证可以通过BBD的关键词then开始验证。
  given(dog.bark()).willReturn(2);
  // when
  ...
  then(person).should(times(2)).ride(bike);  
  以上就是 Mockito 的主要使用方式,关于更详细的介绍可参考Mockito官方文档和Mockito中文文档。
  4. Spring Test
  目前几乎大多数 Java web 项目都是有基于 Spring 来开发的。通过 Spring 进行 bean 管理后,仅仅通过 JUnit 来做测试会有各种麻烦,比如:Spring容器初始化问题、使用硬编码方式手工获取Bean、不方便对数据操作的正确性做检查等。这时我们就可以通过 Spring 全家桶中的另一位成员spring-test来帮助我们在 Spring 工程中做单元测试了。以下通过简单的示例来演示其使用。
  (1). 加入依赖包
  通过Maven加入JUnit、spring-test的Jar包(最好其他Spring包版本一致)。
  <dependency>  
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
  </dependency>  
  <dependency>  
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>xxxx</version>
      <scope>test</scope>
  </dependency> 
 
  (2). 创建测试类
  @RunWith(SpringJUnit4ClassRunner.class)
  @ContextConfiguration("/application-context-test.xml")
  public class UserDaoTest {
      /** 自动注入baseDao,默认按名称. */
      @Resource
      private IBaseDao baseDao;
      @Test
      @Transactional
      @Rollback
      public void insert() {
          String sql = "INSERT INTO t_user(c_name, c_password) values(?, ?)";
          Object[] objs = new Object[]{"zhangsan", "123456"};
          baseDao.insert(sql , objs);
          String sql2 = "SELECT * FROM t_user WHERE c_name = ? and c_password = ?";
          List<Map<String,Object>> list = baseDao.queryForList(sql1, objs);
          assertTrue(list.size() > 0);
          System.out.println(list);
      }
  }
  使用Spring Test 可以使用@Autowired自动注入相关的bean信息,而不需要自己手动通过getBean去获取相应的bean信息。
  使用Spring Test 测试,可以@Transaction注解,表示该方法使用spring的事务,在单元测试中,执行完毕后默认会回滚。
  使用@Rollback注解,标明使用完此方法后事务回滚,可以@Rollback(false)这个注解来使对数据库操作的测试结果不回滚。
  (3). 对 Spring MVC 的测试
  为了测试 web 项目,需要一些 Servlet 相关的模拟对象,比如:MockMVC/MockHttpServletRequest/MockHttpServletResponse/MockHttpSession。使用示例如下:
  import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;  
  import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;  
  import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl;  
  import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;  
  import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;  
  import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
  import org.junit.Before;  
  import org.junit.Test;  
  import org.junit.runner.RunWith;  
  import org.springframework.beans.factory.annotation.Autowired;  
  import org.springframework.mock.web.MockHttpServletRequest;  
  import org.springframework.mock.web.MockHttpSession;  
  import org.springframework.test.context.ContextConfiguration;  
  import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;  
  import org.springframework.test.context.web.WebAppConfiguration;  
  import org.springframework.test.web.servlet.MockMvc;  
  import org.springframework.test.web.servlet.setup.MockMvcBuilders;  
  import org.springframework.web.context.WebApplicationContext;
  @RunWith(SpringJUnit4ClassRunner.class)
  @ContextConfiguration("/application-context-test.xml")
  @WebAppConfiguration("src/main/resources") // 此注解指定web资源的位置,默认为src/main/webapp
  public class TestControllerIntegrationTests {
      private MockMvc mockMvc; // 模拟MVC对象
      @Autowired
      private DemoService demoService;// 在测试用例注入spring的bean
      @Autowired
      WebApplicationContext wac; // 注入WebApplicationContext
      @Autowired
      MockHttpSession session; // 注入模拟的http session
      @Autowired
      MockHttpServletRequest request; // 模拟request
      @Before // 测试开始前的初始化工作
      public void setup() {
          this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); //2
      }
      @Test
      public void testNormalController() throws Exception{
          String exp_str = demoService.saySomething(); // expect str
          mockMvc.perform(get("/normal")) // 模拟GET /normal
              .andExpect(status().isOk())// 预期返回状态为200
              .andExpect(view().name("page"))// 预期view的名称
              .andExpect(forwardedUrl("/WEB-INF/classes/views/page.jsp"))// 预期页面转向的真正路径
              .andExpect(model().attribute("msg", exp_str));// 预期model里的值
      }
      @Test
      public void testRestController() throws Exception{
          mockMvc.perform(get("/testRest")) // HTTP GET 方法
              .andExpect(status().isOk())
              .andExpect(content().contentType("text/plain;charset=UTF-8"))//14
              .andExpect(content().string(demoService.saySomething()));//15
      }
  }
  注: demoService及相关方法的调用,也可以通过Mockito工具Mock出来,更符合单元测试对单元性的要求,否则这些测试又额外附带了一定集成测试的性质了。
  4. spring-boot-starter-test
  (1). 简单介绍
  现在越来越多的应用都采用SpringBoot的方式来构建,在SpringBoot应用中单元测试变得更加容易了,只需要加入spring-boot-starter-test的 Starter 即可,其中默认导入了 Spring Boot 测试模块以及JUnit,AssertJ,Hamcrest和其他一些有用的库。
  <dependency>  
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
  </dependency> 
 
  spring-boot-starter-test的 Starter (Scope为test),包括了以下提供的类库:
  JUnit:单元测试Java应用程序的事实标准。
  Spring Test 和 Spring Boot Test:Spring Boot应用程序的实用程序和集成测试支持。
  AssertJ:流畅的断言库。
  Hamcrest:匹配器对象库。
  Mockito:Java Mock 框架。
  JSONassert:JSON的断言库。
  JsonPath:JSON的XPath。
  我们通常在编写测试时发现这些通用库都是比较有用的。如果这些库还不适合您的需求,您还可以添加您自己的附加测试依赖库。
  Spring Boot 提供了一个@SpringBootTest注释,当您需要 Spring Boot 功能时,它可以用作标准 spring-test @ContextConfiguration注释的替代方法。注解的工作原理是通过SpringApplication创建用于测试的ApplicationContext。除了@SpringBootTest之外,还提供了许多其他注释来测试应用程序的更具体的切片。
  提示:不要忘记在测试中添加@RunWith(SpringRunner.class),否则注释将被忽略。
  (2). 一个简单示例
  @RunWith(SpringRunner.class)
  @SpringBootTest
  public class UserServiceTest {
      @Value("${msg}")
      private String msg;
      @Autowired
      private UserService userService;
      @Test
      public void getUser() {
          User user = userService.selectByKey(20180302325L);
          Assert.assertThat(user.getName(), is("Blinkfox"));
          System.out.println("获取的配置信息为:" + msg);
      }
  }
  上面就是最简单的单元测试写法,测试类上只需要@RunWith(SpringRunner.class)和@SpringBootTest两个注解即可测试任何类和方法。
  (3). web模块的单元测试
  要测试 Spring MVC 控制器是否按预期工作,请使用@WebMvcTest注释。@WebMvcTest自动配置Spring MVC基础结构,并将扫描的bean限制为@Controller,@ControllerAdvice,@JsonComponent,Converter,GenericConverter,Filter,WebMvcConfigurer和HandlerMethodArgumentResolver。 使用此注释时,不会扫描常规的@Component bean。
  您还可以使用@AutoConfigureMockMvc对其进行注释,从而在非@WebMvcTest(如@SpringBootTest)中自动配置MockMvc。 以下示例使用MockMvc:
  @RunWith(SpringRunner.class)
  @WebMvcTest(UserVehicleController.class)
  public class MyControllerTests {
      @Autowired
      private MockMvc mvc;
      @MockBean
      private UserVehicleService userVehicleService;
      @Test
      public void testExample() throws Exception {
          given(this.userVehicleService.getVehicleDetails("sboot"))
                  .willReturn(new VehicleDetails("Honda", "Civic"));
          this.mvc.perform(get("/sboot/vehicle").accept(MediaType.TEXT_PLAIN))
                  .andExpect(status().isOk()).andExpect(content().string("Honda Civic"));
      }
  }
  SpringBoot对各种单元测试的场景支持的比较全,更多的示例可直接在Spiring Boot Test 官方指南中去查看,这里就不再一一列举了。
  5. JaCoCo
  在做单元测试时,代码覆盖率常常被拿来作为衡量测试好坏的指标,甚至,用代码覆盖率来考核测试任务完成情况,比如,代码覆盖率必须达到80%或 90%。 目前Java常用覆盖率工具clover、Jacoco和Cobertura等。关于这些代码覆盖率工具的对比可参看这里。这里我们就选取 Jacoco 来作为代码覆盖率工具来做介绍。
  Jacoco 是一个开源的覆盖率工具。Jacoco 可以嵌入到Ant 、Maven中,并提供了 Eclipse、IDEA 插件,也可以使用Java Agent技术监控Java程序。很多第三方的工具提供了对 Jacoco 的集成,如sonar、Jenkins。
  Jacoco与Maven的集成很简单,只需要在plugins中添加如下插件即可。
  <plugin>  
      <groupId>org.jacoco</groupId>
      <artifactId>jacoco-maven-plugin</artifactId>
      <version>0.7.7.201606060606</version>
      <configuration>
          <destFile>target/coverage-reports/jacoco-unit.exec</destFile>
          <dataFile>target/coverage-reports/jacoco-unit.exec</dataFile>
      </configuration>
      <executions>
          <execution>
              <id>jacoco-initialize</id>
              <goals>
                  <goal>prepare-agent</goal>
              </goals>
          </execution>
          <execution>
              <id>jacoco-site</id>
              <phase>package</phase>
              <goals>
                  <goal>report</goal>
              </goals>
          </execution>
      </executions>
  </plugin>  
  做单元测试时,测试覆盖率是不是越高代表代码质量越好呢?Martin Fowler(重构那本书的作者)曾经写过一篇博客来讨论这个问题,他指出:把测试覆盖作为质量目标没有任何意义,而我们应该把它作为一种发现未被测试覆盖的代码的手段。
  所以,代码覆盖率统计是用来发现没有被测试覆盖的代码;代码覆盖率统计不能完全用来衡量代码质量。
22/2<12
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号