单元测试指南

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

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

 作者:Blinkfox    来源:个人博客

  一、必要性
  在我们公司中要做单元测试,确实比较难,因为公司缺少这种氛围,有也只是局部的,大多数工程师没有这方面的习惯和素养,很多人都是有一定的抵触的心理,经过我私下的了解大概有以下几种原因吧。
  写单元测试太耗费时间了,项目要赶进度,编写单元测试会导致不能按时完成开发任务,导致项目延期;
  做传统xx管理系统的项目,业务逻辑比较简单,主要就是对业务数据做增删改查,单元测试意义和价值不高;
  公司有专门的测试人员,很多问题在集成测试时一定能发现。
  以前项目上从没写过单元测试,没有经验,不知道怎么编写单元测试;
  这其中对单元测试就有些误解了,单元测试有几个比较常见的典型场景:
  开发前写单元测试,通过测试描述需求,即测试驱动开发。
  在开发过程中及时得到反馈,提前规避隐患和发现问题。
  应用于自动化构建或持续集成流程,对每次代码修改做回归测试。
  作为重构的基础,验证重构是否可靠。
  还有最重要的一点:编写单元测试的难易程度能够直接反应出代码的设计水平,能写出单元测试和写不出单元测试之间体现了编程能力上的巨大的鸿沟。无论是什么样的程序员,坚持编写一段时间的单元测试之后,都会明显感受到代码设计能力的巨大提升。
  公司开发人员的代码质量往往不是很高,尤其是对代码的拆分和逻辑的抽象还处于懵懂阶段。要对这类代码写单测,即使是工作了3,4年的高级码农也是一个挑战,对新人来说几乎是不可能完成的任务。这也让很多开发人员有了写单元测试很难的感觉。所以,写单元测试的难易程度跟代码的质量关系最大,并且是决定性的。项目里无论用了哪个测试框架都不能解决代码本身难以测试的问题。
  诚然,写单元测试在开发期间的确是会耗费更多时间的,尤其是要追求很高(超过80%,甚至100%)的代码覆盖率,更是需要耗费大量心血才能达到的。对于一些只需一次交付,很少维护的项目来说,意义和价值确实不是很大。但这本质上是属于为了赚快钱,不负责任的行为了,毕竟谁都无法保障自己写的程序,真的没有丝毫问题。这个问题的出现并不是个人的问题,而是反映了公司项目管理中的问题。当然,个人的原因也存在,就是如何在有限的时间里,提高效率。
  目前公司的大多数项目其实都有着至少两年的维护时间的,很多开发人员都不愿意把自己的时间耗在一个代码很烂、没有单元测试保障且经常变更需求的项目里面。总之,包括我本人在内,都是有项目维护恐惧症的,更愿意投入到新项目的开发中。但是新项目里面还是没有单元测试的保障,代码质量逐渐低劣,如此就又形成了一个不断的循环之中。无法挣脱这个循环的人员就只能选择离职了,也许不慎又到了新的漩涡里面。
一个 bug 被隐藏的时间越长,修复这个 bug 的代价就越大。
  单元测试能帮助我们在早期就规避、发现和修复很多不易察觉的 bug 和漏洞,而且更能保障后期的需求变动和代码重构时所带来的隐患,减少测试成本和维护成本。所以,在新项目中逐步推广和编写单元测试是有必要的,这将大大提高项目中代码的质量和可靠性,有些老项目中就算了吧,往往维护人员的负面情绪可能会更多,一些新的功能特性倒是可以试试。虽然写好单元测试很难,但写单元测试的难度其实是小于决定写单元测试的勇气的。
  二、基本概念
  单元测试:单元测试又称模块测试,属于白盒测试,是最小单位的测试。模块分为程序模块和功能模块。功能模块指实现了一个完整功能的模块(单元),一个完整的程序单元具备输入、加工和输出三个环节。而且每个程序单元都应该有正规的规格说明,使之对其输入、加工和输出的关系做出名明确的描述。
  驱动测试:驱动被测试模块正常运行起来的实体。通俗的说法就是你负责测试模块/方法是中间的,没有main()方法入口,怎么编译,怎么启动呢?就需要写一个带main()的方法来调用你的模块/方法,这个就是驱动测试。
  测试桩:代替被测模块调用的子模块的实体,该实体一般为桩函数(stub)。通俗的说法就是你负责测试的模块/方法所调用的模块/方法,所以你需要模仿他们做一个返回值(假的,但符合设计)。
  测试覆盖:评测测试过程中已经执行的代码的多少。
  测试覆盖率:代码的覆盖程度,一种度量方式。针对代码的测试覆盖率有很多种度量方式,常见的有以下几种:
  语句覆盖
  判定覆盖
  路径覆盖
  测试覆盖率数据到底有多大意义。主要有以下几个观点:
  路径覆盖率 > 判定覆盖 > 语句覆盖
  覆盖率数据只能代表你测试过哪些代码,不能代表你是否测试好这些代码。
  不要过于相信覆盖率数据,100%的测试覆盖率并不能保证bug的不出现。
  代码覆盖率只是一个最基本的前提,一定要保证,但不是意味着达到指标就代表测试的完成
  测试人员不能盲目追求代码覆盖率,而应该想办法设计更多更好的案例,哪怕多设计出来的案例对覆盖率一点影响也没有。
  三、单元测试工具
  在Java中有非常多的单元测试的工具或框架可供选择,我这里只选择一些常用的、主流的单元测试框架或者工具来作介绍和使用。
  JUnit:Java中最有名、使用最广泛的单元测试框架
  Mockito:模拟框架,可以让你用干净而简单的API编写测试
  Spring Test: 使用 Spring Test 来对Spring相关的项目做单元测试,其中会结合或者集成其他测试框架和工具
  spring-boot-starter-test: SpringBoot项目中的单元测试
  JaCoCo: 使用离线和运行时字节码工具来收集代码覆盖率指标的框架。
  1. JUnit4
  JUnit 是使用 Java 语言编写的用于编写和运行可重复的自动化测试的开源测试框架。除了 Junit 之外,TestNg也是Java中非常受欢迎的单元测试框架。两种框架在功能上看起来非常相似,这里有一篇关于JUnit 4 与 TestNG 的对比,总体来说,TestNG 比 Junit4 功能更强大一些,但是相比 Junit5 而言,TestNG 又落后了一代。开源的轮子滚滚向前,都是一代新的轮子超越一代老的轮子。所以,我们这里就只选择 Junit 来作单元测试框架的介绍了吧。
  JUnit4 和 TestNG 的功能比较
  目前最新版本是 JUnit5.2.0,相比 JUnit4 而言有很大的改变,这里主要讲解 JUnit4 的使用(目前的新老项目中应该使用的更多),并对 JUnit5 做简要介绍。学习了 Junit4 的主要使用方式之后,大家再去看JUnit5 用户指南在将来逐渐使用起来更好些。
  (1). 简单示例
  import static org.junit.Assert.*;
  import org.junit.Test;
  public class CalculateTest {
      @Test
      public void testSum() {
          Calculate calculation = new Calculate();
          int sum = calculation.sum(2, 5);
          int testSum = 7;
          System.out.println("@Test sum(): " + sum + " = " + testSum);
          assertEquals(sum, testSum);
      }
  }
  (2). 注解
  @Test: 测试方法,在这里还可以测试期望异常和超时时间。
  @Before: 每个测试方法执行之前执行的方法。
  @BeforeClass: 一个测试类中所有测试方法执行之前执行的方法,只执行一次,且方法必须为static的。
  @After: 每个测试方法执行之后执行的方法。
  @AfterClass: 一个测试类中所有测试方法执行之后执行的方法,只执行一次,且方法必须为static的。
  @Ignore: 忽略的测试方法。
  @RunWith: 指定测试类使用某个运行器。
  @Parameters: 参数化测试,指定测试类的测试数据集合。
  @FixMethodOrder: 注解在测试类上指定测试方法按一定顺序规则来执行,有三种。
  一个测试类单元测试的执行顺序为:
  @BeforeClass –> @Before –> @Test –> @After –> @AfterClass
  每一个测试方法的执行顺序为:
  @Before –> @Test –> @After
  综合示例:
  import static org.junit.Assert.*;  
  import java.util.*;  
  import org.junit.*;
  public class AnnotationsTest {
      private ArrayList testList;
      @BeforeClass
      public static void onceExecutedBeforeAll() {
          System.out.println("@BeforeClass: onceExecutedBeforeAll");
      }
      @Before
      public void executedBeforeEach() {
          testList = new ArrayList();
          System.out.println("@Before: executedBeforeEach");
      }
      @AfterClass
      public static void onceExecutedAfterAll() {
          System.out.println("@AfterClass: onceExecutedAfterAll");
      }
      @After
      public void executedAfterEach() {
          testList.clear();
          System.out.println("@After: executedAfterEach");
      }
      @Test
      public void EmptyCollection() {
          assertTrue(testList.isEmpty());
          System.out.println("@Test: EmptyArrayList");
      }
      @Test
      public void OneItemCollection() {
          testList.add("oneItem");
          assertEquals(1, testList.size());
          System.out.println("@Test: OneItemArrayList");
      }
      @Ignore
      public void executionIgnored() {
          System.out.println("@Ignore: This execution is ignored");
      }
  }
  如果我们运行上面的测试,控制台输出将是以下几点:
  @BeforeClass: onceExecutedBeforeAll
  @Before: executedBeforeEach
  @Test: EmptyArrayList
  @After: executedAfterEach
  @Before: executedBeforeEach
  @Test: OneItemArrayList
  @After: executedAfterEach
  @AfterClass: onceExecutedAfterAll
  (3). 断言
  断言是编写测试用例的核心实现方式,即期望值是多少,测试的结果是多少,以此来判断测试是否通过。JUnit4.x中的断言核心方法如下:
  assertArrayEquals(expecteds, actuals): 查看两个数组是否相等。
  assertEquals(expected, actual): 查看两个对象是否相等。类似于字符串比较使用的equals()方法。
  assertNotEquals(first, second): 查看两个对象是否不相等。
  assertNull(object): 查看对象是否为空。
  assertNotNull(object): 查看对象是否不为空。
  assertSame(expected, actual): 查看两个对象的引用是否相等。类似于使用“==”比较两个对象。
  assertNotSame(unexpected, actual): 查看两个对象的引用是否不相等。类似于使用“!=”比较两个对象。
  assertTrue(condition): 查看运行结果是否为true。
  assertFalse(condition): 查看运行结果是否为false。
  assertThat(actual, matcher): 查看实际值是否满足指定的条件。
  fail(): 让测试失败。
  (4). 套件测试
  测试套件意味着捆绑几个单元测试用例并且一起执行他们。在 JUnit 中,@RunWith和@Suite注释用来运行套件测试。简单示例如下:
  public class TestJunit1 {
     @Test
     public void testPrint1() {
        System.out.println("Test Junit 1...");
     }
  }

  public class TestJunit2 {
     @Test
     public void testPrint2() {
        System.out.println("Test Junit 2...");
     }
  }

  @RunWith(Suite.class)
  @Suite.SuiteClasses({
     TestJunit1.class,
     TestJunit2.class
  })
  public class JunitTestSuite {
  }
  (5). 参数化测试
  一个测试类也可以被看作是一个参数化测试类。但它要满足下列所有要求:
  该类被注解为@RunWith(Parameterized.class)。
  这个类有一个构造函数,存储测试数据。
  这个类有一个静态方法生成并返回测试数据,并注明@Parameters注解。
  这个类有一个测试,它需要注解@Test到方法。
  简单示例如下:
请  import static org.junit.Assert.assertEquals;
  import java.util.Arrays;  
  import java.util.Collection;
  import org.junit.Test;  
  import org.junit.runner.RunWith;  
  import org.junit.runners.Parameterized;  
  import org.junit.runners.Parameterized.Parameters;
  @RunWith(Parameterized.class)
  public class CalculateTest {
      private int expected;
      private int first;
      private int second;
      public CalculateTest(int expectedResult, int firstNumber, int secondNumber) {
          this.expected = expectedResult;
          this.first = firstNumber;
          this.second = secondNumber;
      }
      @Parameters
      public static Collection addedNumbers() {
          return Arrays.asList(new Integer[][] { { 3, 1, 2 }, { 5, 2, 3 },
                  { 7, 3, 4 }, { 9, 4, 5 }, });
      }
      @Test
      public void sum() {
          Calculate add = new Calculate();
          System.out.println("Addition with parameters : " + first + " and " + second);
          assertEquals(expected, add.sum(first, second));
      }
  }
  运行CalculateTest测试用例,控制台输出如下:
  Addition with parameters : 1 and 2  
  Adding values: 1 + 2  
  Addition with parameters : 2 and 3  
  Adding values: 2 + 3  
  Addition with parameters : 3 and 4  
  Adding values: 3 + 4  
  Addition with parameters : 4 and 5  
  Adding values: 4 + 5
  
  (6). 忽略测试
  有时可能会发生我们的代码还没有准备好的情况,这时测试用例去测试这个方法或代码的时候会造成失败。@Ignore注释会在这种情况时帮助我们。
  一个含有@Ignore注释的测试方法将不会被执行。
  如果一个测试类有@Ignore注释,则它的测试方法将不会执行
  public class JunitTest3 {
      @Test
      @Ignore("该测试方法还没准备好运行.")
      public void testHello() {
          System.out.println("Hello World!");
      }
  }
  在上面的示例中,JUnit将不会执行testHello()方法。
  (7). 异常测试
  它用于测试由方法抛出的异常。
  import org.junit.*;
  public class JunitTest4 {
      @Test(expected = ArithmeticException.class)
      public void testWithException() {
        int i = 1 / 0;
      }
  }
  在上面的示例中,testWithException()方法将抛出ArithmeticException异常,因为这是一个预期的异常,因此单元测试会通过。
  (8). 超时测试
  超时测试是指,一个单元测试运行时间是否超过指定的毫秒数,测试将终止并标记为失败。
  import org.junit.*;
  public class JunitTest5 {
      @Test(timeout = 1000)
      public void testTimeout() {
          while (true) {
              // do nothing.
          }
      }
  }
  在上面的示例中,testTimeout()方法将不会返回,因此JUnit引擎会将其标记为失败,并抛出一个异常。java.lang.Exception:test timed out after 1000 milliseconds。
  (9). Hamcrest
  在实际开发中,一些基本的断言,如eqaul, null, true它们的可读性并不是很好。而且很多时候我们要比较对象、集合、Map等数据结构。这样我们要么进行大段的字段获取再断言。或者干脆自己编写表达式并断言其结果。JUnit4.4 引入了 Hamcrest 框架,Hamcest 提供了一套匹配符 Matcher,这些匹配符更接近自然语言,可读性高,更加灵活。
  Hamcrest提供了大量被称为“匹配器”的方法。其中每个匹配器都设计用于执行特定的比较操作。Hamcrest 的可扩展性很好,让你能够创建自定义的匹配器。最重要的是,JUnit 也包含了 Hamcrest 的核心,提供了对 Hamcrest 的原生支持,可以直接使用 Hamcrest。当然要使用功能齐备的Hamcrest,还是要引入对它的依赖。
  看个对比例子,前者使用Junit的 断言,后者使用 Hamcrest 的断言。
  @Test
  public void test_with_junit_assert() {  
      int expected = 51;
      int actual = 51;
      assertEquals("failure - They are not same!", expected, actual);
  }
  @Test
  public void test_with_hamcrest_assertThat() {  
      int expected = 51;
      int actual = 51;
      assertThat("failure - They are not same!", actual, equalTo(expected));
  }

  // 联合匹配符not和equalTo表示“不等于”
  assertThat( something, not( equalTo( "developer" ) ) );  
  // 联合匹配符not和containsString表示“不包含子字符串”
  assertThat( something, not( containsString( "Works" ) ) );  
  // 联合匹配符anyOf和containsString表示“包含任何一个子字符串”
  assertThat(something, anyOf(containsString("developer"), containsString("Works")));
  使用 assertThat 的优点:
  1.Hamcrest 一条 assertThat 即可以替代其他所有的 assertion 语句,这样可以在所有的单元测试中只使用一个断言方法,使得编写测试用例变得简单,代码风格变得统一,测试代码也更容易维护。
  2.assertThat 使用了 Hamcrest 的 Matcher 匹配符,用户可以使用匹配符规定的匹配准则精确的指定一些想设定满足的条件,具有很强的易读性,而且使用起来更加灵活
  3.assertThat 不再像 assertEquals 那样,使用比较难懂的“谓宾主”语法模式(如:assertEquals(3, x);),相反,assertThat 使用了类似于“主谓宾”的易读语法模式(如:assertThat(x,is(3));),使得代码更加直观、易读。
  4.可以将这些 Matcher 匹配符联合起来灵活使用,达到更多目的。
  JUnit 4.4 自带了一些 Hamcrest 的匹配符 Matcher,但是只有有限的几个,在类org.hamcrest.CoreMatchers中定义,要想使用他们,必须导入包 org.hamcrest.CoreMatchers.*。
  Hamcrest 提供了很强大的一些api 供我们进行测试断言。
  核心:
      anything - 总是匹配,如果你不关心测试下的对象是什么是有用的
      describedAs - 添加一个定制的失败表述装饰器
      is - 改进可读性装饰器 - 见下 “Sugar”
  逻辑:
      allOf - 如果所有匹配器都匹配才匹配,像Java里的&&
      anyOf - 如果任何匹配器匹配就匹配,像Java里的||
      not - 如果包装的匹配器不匹配器时匹配,反之亦然
  对象:
      equalTo - 测试对象相等使用Object.equals方法
      hasToString - 测试Object.toString方法
      instanceOf, isCompatibleType - 测试类型
      notNullValue, nullValue - 测试null
      sameInstance - 测试对象实例
  Beans:  
      hasProperty - 测试JavaBeans属性
  集合:
      array - 测试一个数组元素test an array’s elements against an array of matchers
      hasEntry, hasKey, hasValue - 测试一个Map包含一个实体,键或者值
      hasItem, hasItems - 测试一个集合包含一个元素
      hasItemInArray - 测试一个数组包含一个元素
  数字:
      closeTo - 测试浮点值接近给定的值
      greaterThan, greaterThanOrEqualTo, lessThan, lessThanOrEqualTo - 测试次序
  文本:
      equalToIgnoringCase - 测试字符串相等忽略大小写
      equalToIgnoringWhiteSpace - 测试字符串忽略空白
      containsString, endsWith, startsWith - 测试字符串匹配
  以下示例代码列举了大部分 assertThat 的使用例子,供大家学习使用时参考:
  //---------------- 字符相关匹配符 ----------------
  /**equalTo匹配符断言被测的testedValue等于expectedValue,
  * equalTo可以断言数值之间,字符串之间和对象之间是否相等,相当于Object的equals方法
  */
  assertThat(testedValue, equalTo(expectedValue));
  /**equalToIgnoringCase匹配符断言被测的字符串testedString
  *在忽略大小写的情况下等于expectedString
  */
  assertThat(testedString, equalToIgnoringCase(expectedString));
  /**equalToIgnoringWhiteSpace匹配符断言被测的字符串testedString
  *在忽略头尾的任意个空格的情况下等于expectedString,
  *注意:字符串中的空格不能被忽略
  */
  assertThat(testedString, equalToIgnoringWhiteSpace(expectedString);
  /**containsString匹配符断言被测的字符串testedString包含子字符串subString**/
  assertThat(testedString, containsString(subString));
  /**endsWith匹配符断言被测的字符串testedString以子字符串suffix结尾*/
  assertThat(testedString, endsWith(suffix));
  /**startsWith匹配符断言被测的字符串testedString以子字符串prefix开始*/
  assertThat(testedString, startsWith(prefix));
  // ---------------- 一般匹配符 ----------------
  /**nullValue()匹配符断言被测object的值为null*/
  assertThat(object,nullValue());
  /**notNullValue()匹配符断言被测object的值不为null*/
  assertThat(object,notNullValue());
  /**is匹配符断言被测的object等于后面给出匹配表达式*/
  assertThat(testedString, is(equalTo(expectedValue)));
  /**is匹配符简写应用之一,is(equalTo(x))的简写,断言testedValue等于expectedValue*/
  assertThat(testedValue, is(expectedValue));
  /**is匹配符简写应用之二,is(instanceOf(SomeClass.class))的简写,
  *断言testedObject为Cheddar的实例
  */
  assertThat(testedObject, is(Cheddar.class));
  /**not匹配符和is匹配符正好相反,断言被测的object不等于后面给出的object*/
  assertThat(testedString, not(expectedString));
  /**allOf匹配符断言符合所有条件,相当于“与”(&&)*/
  assertThat(testedNumber, allOf(greaterThan(8), lessThan(16)));
  /**anyOf匹配符断言符合条件之一,相当于“或”(||)*/
  assertThat(testedNumber, anyOf(greaterThan(16), lessThan(8)));
  // ---------------- 数值相关匹配符 ----------------
  /**closeTo匹配符断言被测的浮点型数testedDouble在20.0?à0.5范围之内*/
  assertThat(testedDouble, closeTo(20.0, 0.5));
  /**greaterThan匹配符断言被测的数值testedNumber大于16.0*/
  assertThat(testedNumber, greaterThan(16.0));
  /** lessThan匹配符断言被测的数值testedNumber小于16.0*/
  assertThat(testedNumber, lessThan (16.0));
  /** greaterThanOrEqualTo匹配符断言被测的数值testedNumber大于等于16.0*/
  assertThat(testedNumber, greaterThanOrEqualTo (16.0));
  /** lessThanOrEqualTo匹配符断言被测的testedNumber小于等于16.0*/
  assertThat(testedNumber, lessThanOrEqualTo (16.0));
  // ---------------- 集合相关匹配符 ----------------
  /**hasEntry匹配符断言被测的Map对象mapObject含有一个键值为"key"对应元素值为"value"的Entry项*/
  assertThat(mapObject, hasEntry("key", "value"));
  /**hasItem匹配符表明被测的迭代对象iterableObject含有元素element项则测试通过*/
  assertThat(iterableObject, hasItem (element));
  /** hasKey匹配符断言被测的Map对象mapObject含有键值“key”*/
  assertThat(mapObject, hasKey ("key"));
  /** hasValue匹配符断言被测的Map对象mapObject含有元素值value*/
  assertThat(mapObject, hasValue(value));  
  2. JUnit5
  (1). Junit5简介
  JUnit 5 跟以前的JUnit版本不一样,它由几大不同的模块组成,这些模块分别来自三个不同的子项目。
  JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
  JUnit Platform是在JVM上 启动测试框架 的基础平台。它还定义了TestEngine API,该API可用于开发在平台上运行的测试框架。此外,平台还提供了一个从命令行或者 Gradle 和 Maven 插件来启动的 控制台启动器 ,它就好比一个 基于 JUnit4 的 Runner 在平台上运行任何TestEngine。
  JUnit Jupiter是一个组合体,它是由在JUnit 5中编写测试和扩展的新 编程模型 和 扩展模型 组成。另外,Jupiter子项目还提供了一个TestEngine,用于在平台上运行基于Jupiter的测试。
  JUnit Vintage 提供了一个TestEngine,用于在平台上运行基于JUnit 3和JUnit 4的测试。
  JUnit 5需要Java 8(或更高)的运行时环境。不过,你仍然可以测试那些由老版本JDK编译的代码。
  (2). 简单示例
  import static org.junit.jupiter.api.Assertions.assertEquals;
  import org.junit.jupiter.api.Test;
  class FirstJUnit5Tests {
      @Test
      void myFirstTest() {
          assertEquals(2, 1 + 1);
      }
  }
  表面上来看,使用方式和 Junit4 差别不大,但是与 JUnit4 比较起来还是有些不同的。
  导入测试测试注解(@Test)和断言方法(assertEquals)的包路径不同。
  不需要手动把测试和测试方法声明为public了。
  (3). 注解
  JUnit Jupiter支持使用下面表格中的注解来配置测试和扩展框架。
  所有的核心注解都位于junit-jupiter-api模块的org.junit.jupiter.api`包中。
  @Test: 表示该方法是一个测试方法。与JUnit 4的@Test注解不同的是,它没有声明任何属性,因为JUnit Jupiter中的测试扩展是基于它们自己的专用注解来完成的。这样的方法会被继承,除非它们被覆盖。
  @ParameterizedTest: 表示该方法是一个参数化测试(可以用不同的参数多次运行试)。这样的方法会被继承,除非它们被覆盖。
  @RepeatedTest: 表示该方法是一个重复测试的测试模板(让某个测试方法运行多次)。这样的方法会被继承,除非它们被覆盖。
  @TestFactory: 表示该方法是一个动态测试的测试工厂。这样的方法会被继承,除非它们被覆盖。
  @TestInstance: 用于配置所标注的测试类的测试实例生命周期。这些注解会被继承。
  @TestTemplate: 表示该方法是一个测试模板,它会依据注册的提供者所返回的调用上下文的数量被多次调用。这样的方法会被继承,除非它们被覆盖。
  @DisplayName: 为测试类或测试方法声明一个自定义的显示名称(空格、特殊字符甚至是emojis表情)。该注解不能被继承。
  @BeforeEach: 表示使用了该注解的方法应该在当前类中每一个使用了@Test、@RepeatedTest、@ParameterizedTest或者@TestFactory注解的方法之前执行;类似于 JUnit4 的@Before。这样的方法会被继承,除非它们被覆盖。
  @AfterEach: 表示使用了该注解的方法应该在当前类中每一个使用了@Test、@RepeatedTest、@ParameterizedTest或者@TestFactory注解的方法之后执行;类似于 JUnit4 的@After。这样的方法会被继承,除非它们被覆盖。
  @BeforeAll: 表示使用了该注解的方法应该在当前类中所有使用了@Test、@RepeatedTest、@ParameterizedTest或者@TestFactory注解的方法之前执行;类似于 JUnit4 的@BeforeClass。这样的方法会被继承(除非它们被隐藏或覆盖),并且它必须是static方法(除非"per-class" 测试实例生命周期被使用)。
  @AfterAll: 表示使用了该注解的方法应该在当前类中所有使用了@Test、@RepeatedTest、@ParameterizedTest或者@TestFactory注解的方法之后执行;类似于 JUnit4 的@AfterClass。这样的方法会被继承(除非它们被隐藏 或覆盖),并且它必须是static方法(除非"per-class" 测试实例生命周期被使用)。
  @Nested: 表示使用了该注解的类是一个内嵌、非静态的测试类(让测试编写者能够表示出几组测试用例之间的关系)。@BeforeAll和@AfterAll方法不能直接在@Nested测试类中使用,(除非"per-class"测试实例生命周期被使用)。该注解不能被继承。
  @Tag: 用于声明过滤测试的tags,该注解可以用在方法或类上;类似于TesgNG的测试组或 JUnit4 的分类。该注解能被继承,但仅限于类级别,而非方法级别。
  @Disable: 用于禁用一个测试类或测试方法;类似于 JUnit4 的@Ignore。该注解不能被继承。
  @ExtendWith: 用于注册自定义扩展。该注解不能被继承。
  注:被@Test、@TestTemplate、@RepeatedTest、@BeforeAll、@AfterAll、@BeforeEach 或 @AfterEach 注解标注的方法不可以有返回值。
  在 JUnit5 中的一个测试类的基本生命周期示例如下:
  @BeforeClass: onceExecutedBeforeAll
  @Before: executedBeforeEach
  @Test: EmptyArrayList
  @After: executedAfterEach
  @Before: executedBeforeEach
  @Test: OneItemArrayList
  @After: executedAfterEach
  @AfterClass: onceExecutedAfterAll请在文本框输入文字
  由于 JUnit5 中的新特性很多,限于篇幅就简单介绍到这里了,如想详细了解 Junit5 的更多特性,请前往Junit5官网和JUnit5用户指南中文版去查看。
  3. Mockito
  在软件开发中提及Mock,通常理解为模拟对象。为什么需要模拟? 在我们一开始学编程时,我们所写的对象通常都是独立的,并不依赖其他的类,也不会操作别的类。但实际上,软件中是充满依赖关系的,比如我们会基于 service 业务操作类,而 service 类又是基于数据访问类(DAO)的,依次下去,形成复杂的依赖关系。
  单元测试的思路就是我们想在不涉及依赖关系的情况下测试代码。这种测试可以让你无视代码的依赖关系去测试代码的有效性。核心思想就是如果代码按设计正常工作,并且依赖关系也正常,那么他们应该会同时工作正常。
  有些时候,我们代码所需要的依赖可能尚未开发完成,甚至还不存在,那如何让我们的开发进行下去呢?使用mock可以让开发进行下去,mock技术的目的和作用就是模拟一些在应用中不容易构造或者比较复杂的对象,从而把测试与测试边界以外的对象隔离开。
  我们可以自己编写自定义的 Mock 对象实现 Mock 技术,但是编写自定义的 Mock 对象需要额外的编码工作,同时也可能引入错误。现在实现 Mock 技术的优秀开源框架有很多,Mockito就是一个优秀的用于单元测试的 Mock 框架。
  除了Mockito以外,还有一些类似的框架,比如:
  EasyMock:早期比较流行的 MocK 测试框架。它提供对接口的模拟,能够通过录制、回放、检查三步来完成大体的测试过程,可以验证方法的调用种类、次数、顺序,可以令 Mock 对象返回指定的值或抛出指定异常。
  PowerMock:这个工具是在 EasyMock 和 Mockito 上扩展出来的,目的是为了解决 EasyMock 和 Mockito 不能解决的问题(比如对static, final, private方法均不能 Mock)。其实测试架构设计良好的代码,一般并不需要这些功能,但如果是在已有项目上增加单元测试,老代码有问题且不能改时,就不得不使用这些功能了。
  JMockit:JMockit 是一个轻量级的mock框架是用以帮助开发人员编写测试程序的一组工具和API,该项目完全基于Java 5 SE的 java.lang.instrument包开发,内部使用ASM库来修改Java的Bytecode`。
  WireMock: 模拟您的API以进行快速、可靠和全面的测试。WireMock是一个基于 HTTP 的 API 的模拟器。有些人可能认为它是一个服务虚拟化工具或模拟服务器。
  Mockito 已经被广泛应用,所以这里重点介绍 Mockito,其他的Mock框架也各自有自己的特点,大家下来自己学习或者分享,参考的Mockito中文文档在这里。
  下面的例子大多都会模拟一个 List,因为大多数人都熟悉它(比如add(),get(),clear()等方法)。实际上,请不要模拟List类,改用真实的实例。
  (1). 验证行为
  一旦创建,mock会记录所有交互,你可以验证所有你想要验证的东西。
  // 静态导入会使代码更简洁
  import static org.mockito.Mockito.*;
  // 创建mock对象
  List mockedList = mock(List.class);
  // 使用mock对象
  mockedList.add("one");  
  mockedList.clear();
  // 验证行为
  verify(mockedList).add("one");  
  verify(mockedList).clear();  
  Mock一旦创建,模拟对象将记住你的所有的交互。然后,您可以选择性地验证您感兴趣的任何行为。
  (2). 如何做一些测试打桩(stubbing)
  // 你可以mock具体的类型,不仅只是接口
  LinkedList mockedList = mock(LinkedList.class);
  // 测试桩
  when(mockedList.get(0)).thenReturn("first");  
  when(mockedList.get(1)).thenThrow(new RuntimeException());
  // 输出“first”
  System.out.println(mockedList.get(0));
  // 抛出异常
  System.out.println(mockedList.get(1));
  // 因为get(999) 没有打桩,因此输出null
  System.out.println(mockedList.get(999));
  // 验证get(0)被调用的次数
  verify(mockedList).get(0);  
  默认情况下,所有的函数都有返回值。mock函数默认返回的是null,一个空的集合或者一个被对象类型包装的内置类型,例如0、false对应的对象类型为Integer、Boolean;
  测试桩函数可以被覆写: 例如常见的测试桩函数可以用于初始化夹具,但是测试函数能够覆写它。请注意,覆写测试桩函数是一种可能存在潜在问题的做法;
  一旦测试桩函数被调用,该函数将会一致返回固定的值;
  上一次调用测试桩函数有时候极为重要,当你调用一个函数很多次时,最后一次调用可能是你所感兴趣的。
  (3). 参数匹配器(matchers)
  Mockito以自然的java风格来验证参数值: 使用equals()函数。有时,当需要额外的灵活性时你可能需要使用参数匹配器,也就是argument matchers:
  // 使用内置的anyInt()参数匹配器
  when(mockedList.get(anyInt())).thenReturn("element");
  // 使用自定义的参数匹配器( 在isValid()函数中返回你自己的匹配器实现 )
  when(mockedList.contains(argThat(isValid()))).thenReturn("element");
  // 输出element
  System.out.println(mockedList.get(999));
  // 你也可以验证参数匹配器
  verify(mockedList).get(anyInt());  
  参数匹配器使验证和测试桩变得更灵活。点击这里可以查看更多内置的匹配器以及自定义参数匹配器或者hamcrest 匹配器的示例。
  (4). 验证函数的确切、最少、从未调用次数
  // 使用模拟对象
  mockedList.add("once");
  mockedList.add("twice");  
  mockedList.add("twice");
  mockedList.add("three times");  
  mockedList.add("three times");  
  mockedList.add("three times");
  // 下面的两个验证函数效果一样,因为verify默认验证的就是times(1)
  verify(mockedList).add("once");  
  verify(mockedList, times(1)).add("once");
  // 验证具体的执行次数
  verify(mockedList, times(2)).add("twice");  
  verify(mockedList, times(3)).add("three times");
  // 使用never()进行验证,never相当于times(0)
  verify(mockedList, never()).add("never happened");
  // 使用atLeast()/atMost()
  verify(mockedList, atLeastOnce()).add("three times");  
  verify(mockedList, atLeast(2)).add("five times");  
  verify(mockedList, atMost(5)).add("three times");  
  verify函数默认验证的是执行了times(1),也就是某个测试函数是否执行了1次.因此,times(1)通常被省略了。
  (5). 为返回值为void的函数通过Stub抛出异常
  doThrow(new RuntimeException()).when(mockedList).clear();
  // 调用这句代码会抛出异常
  mockedList.clear();  
  当你调用doThrow(), doAnswer(), doNothing(), doReturn() and doCallRealMethod() 这些函数时可以在适当的位置调用when()函数. 当你需要下面这些功能时这是必须的:
  测试void函数
  在受监控的对象上测试函数
  不知一次的测试为同一个函数,在测试过程中改变mock对象的行为。
  但是在调用when()函数时你可以选择是否调用这些上述这些函数。





上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。
21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

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

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

沪ICP备05003035号

沪公网安备 31010102002173号