相关文章推荐
冷静的猴子  ·  Selenium + Chrome ...·  6 月前    · 
爱跑步的沙滩裤  ·  UnionWithOperation ...·  8 月前    · 
  • 一是在具体实现代码之前,这是测试驱动开发(TDD)所提倡的;
  • 二是与具体实现代码同步进行。先写少量功能代码,紧接着写单元测试(重复这两个过程,直到完成功能代码开发)。其实这种方案跟第一种已经很接近,基本上功能代码开发完,单元测试也差不多完成了。
  • 三是编写完功能代码再写单元测试。我的实践经验告诉我,事后编写的单元测试“粒度”都比较粗。对同样的功能代码,采取前两种方案的结果可能是用10个“小”的单测来覆盖,每个单测比较简单易懂,可读性可维护性都比较好(重构时单测的改动不大);而第三种方案写的单测,往往是用1个“大”的单测来覆盖,这个单测逻辑就比较复杂,因为它要测的东西很多,可读性可维护性就比较差。
  • 建议:我个人是比较推荐单元测试与具体实现代码同步进行这个方案的。只有对需求有一定的理解后才能知道什么是代码的正确性,才能写出有效的单元测试来验证正确性,而能写出一些功能代码则说明对需求有一定理解了。

    单元测试要写多细?

    单元测试不是越多越好,而是越有效越好!进一步解读就是哪些代码需要有单元测试覆盖:

  • 逻辑复杂的
  • 容易出错的
  • 不易理解的,即使是自己过段时间也会遗忘的,看不懂自己的代码,单元测试代码有助于理解代码的功能和需求
  • 公共代码。比如自定义的所有http请求都会经过的拦截器;工具类等。
  • 核心业务代码。一个产品里最核心最有业务价值的代码应该要有较高的单元测试覆盖率。
  • 被测系统(System under test, SUT)表示正在被测试的系统, 目的是测试系统能否正确操作. 根据测试类型的不同, SUT 指代的内容也不同, 例如 SUT 可以是一个类甚至是一整个系统.

    测试依赖组件(DOC)

    被测系统所依赖的组件, 例如进程 UserService 的单元测试时, UserService 会依赖 UserDao, 因此 UserDao 就是 DOC.

    测试替身(Test Double)

    一个实际的系统会依赖多个外部对象, 但是在进行单元测试时, 我们会用一些功能较为简单的并且其行为和实际对象类似的假对象来作为 SUT 的依赖对象, 以此来降低单元测试的复杂性和可实现性. 在这里, 这些假对象就被称为 测试替身(Test Double). 测试替身有如下 5 种类型:

    Test fixture

    所谓 test fixture, 就是运行测试程序所需要的先决条件(precondition). 即对被测对象进行测试时锁需要的一切东西(The test fixture is everything we need to have in place to exercise the SUT). 这个 东西 不单单指的是数据, 同时包括对被测对象的配置, 被测对象所需要的依赖对象等. JUnit4 之前是通过 setUp, TearDown 方法完成, 在 JUnit4这, 我们可以使用@Before 代替 setUp 方法, @After 代替 tearDown 方法.

    注意, @Before 在每个测试方法运行前都会被调用, @After 在每个测试方法运行后都会被调用.

    因为 @Before 和 @After 会在每个测试方法前后都会被调用, 而有时我们仅仅需要在测试前进行一次初始化, 这样的情况下, 可以使用@BeforeClass 和@AfterClass 注解.

    测试用例(Test case)

    在 JUnit 3中, 测试方法都必须以 test 为前缀, 且必须是 public void 的, JUnit 4之后, 就没有这个限制了, 只要在每个测试方法标注 @Test 注解, 方法签名可以是任意的.

    通过 TestSuit 对象将多个测试用例组装成一个测试套件, 测试套件批量运行.

    通过@RunWith 和@SuteClass 两个注解, 我们可以创建一个测试套件. 通过@RunWith 指定一个特殊的运行器, Suite.class 套件运行器, 并通过@SuiteClasses 注解, 将需要进行测试的类列表作作为参数传入

    单元测试 - JUnit4 详解

    什么是JUnit?

    JUint是Java编程语言的单元测试框架,用于编写和运行可重复的自动化测试。

    JUnit特点?

    JUnit 是一个开放的资源框架,用于编写和运行测试。

  • 提供注解来识别测试方法。
  • 提供断言来测试预期结果。
  • JUnit 测试允许你编写代码更快,并能提高质量。
  • JUnit 优雅简洁。没那么复杂,花费时间较少。
  • JUnit测试可以自动运行并且检查自身结果并提供即时反馈。所以也没有必要人工梳理测试结果的报告。
  • JUnit测试可以被组织为测试套件,包含测试用例,甚至其他的测试套件。
  • JUnit在一个条中显示进度。如果运行良好则是绿色;如果运行失败,则变成红色。
  • 最好的资料依然在Junit官方网站,以下我帮你总结下Junit相关的官方网址。@pdai

    junit.org/junit4/

  • 官方入门文档
  • github.com/junit-team/…

  • 官方github
  • github.com/junit-team

  • @Test
  • 在junit3中,是通过对测试类和测试方法的命名来确定是否是测试,且所有的测试类必须继承junit的测试基类。在junit4中,定义一个测试方法变得简单很多,只需要在方法前加上@Test就行了。

    注意:测试方法必须是public void,即公共、无返回数据。可以抛出异常。

  • @Ignore
  • 有时候我们想暂时不运行某些测试方法\测试类,可以在方法前加上这个注解。在运行结果中,junit会统计忽略的用例数,来提醒你。但是不建议经常这么做,因为这样的坏处时,容易忘记去更新这些测试方法,导致代码不够干净,用例遗漏。使用此标注的时候不能与其它标注一起使用,如:和@Test 标注一起使用,那就没用了

  • @BeforeClass
  • 当我们运行几个有关联的用例时,可能会在数据准备或其它前期准备中执行一些相同的命令,这个时候为了让代码更清晰,更少冗余,可以将公用的部分提取出来,放在一个方法里,并为这个方法注解@BeforeClass。意思是在测试类里所有用例运行之前,运行一次这个方法。例如创建数据库连接、读取文件等。

    注意:方法名可以任意,但必须是public static void,即公开、静态、无返回。这个方法只会运行一次。

  • @AfterClass
  • 跟@BeforeClass对应,在测试类里所有用例运行之后,运行一次。用于处理一些测试后续工作,例如清理数据,恢复现场。

    注意:同样必须是public static void,即公开、静态、无返回。这个方法只会运行一次。

  • @Before
  • 与@BeforeClass的区别在于,@Before不止运行一次,它会在每个用例运行之前都运行一次。主要用于一些独立于用例之间的准备工作。

    比如两个用例都需要读取数据库里的用户A信息,但第一个用例会删除这个用户A,而第二个用例需要修改用户A。那么可以用@BeforeClass创建数据库连接。用@Before来插入一条用户A信息。

    注意:必须是public void,不能为static。不止运行一次,根据用例数而定。

  • @After :与@Before对应。
  • @Runwith
  • @RunWith(Parameterized.class) 参数化运行器,配合@Parameters使用junit的参数化功能
  • @RunWith(Suite.class) @SuiteClasses({ATest.class,BTest.class,CTest.class})测试集运行器配合使用测试集功能
  • @RunWith(JUnit4.class) junit4的默认运行器
  • @RunWith(JUnit38ClassRunner.class) 用于兼容junit3.8的运行器
  • 一些其它运行器具备更多功能。例如@RunWith(SpringJUnit4ClassRunner.class)集成了spring的一些功能
  • Maven包引入

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <groupId>org.example</groupId>
      <artifactId>java-junit4</artifactId>
      <version>1.0-SNAPSHOT</version>
      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
              <source>8</source>
              <target>8</target>
            </configuration>
          </plugin>
        </plugins>
      </build>
      <dependencies>
        <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.12</version>
          <scope>test</scope>
        </dependency>
      </dependencies>
    </project>
    

    测试:Hello World

    package tech.pdai.junit4;
    import org.junit.Test;
    import static org.junit.Assert.assertEquals;
    * Hello world test.
    * @author pdai
    public class HelloWorldTest {
        @Test
        public void firstTest() {
            assertEquals(2, 1 + 1);
    

    @Test注解在方法上标记方法为测试方法,以便构建工具和 IDE 能够识别并执行它们。JUnit 4 需要测试方法为public,这和Junit 5 有差别。

    测试:生命周期

    测试:生命周期

  • @BeforeClass注解修饰的方法(该方法要用static修饰)会在所有方法运行前被执行,且只执行一次,通常用来为后面测试方法的准备工作,如加载配置、进行数据库的连接等。父类的@BeforeClass注解方法会在子类的@BeforeClass注解方法执行前执行。
  • @Before注解修饰的方法会在每个测试方法执行前执行一次,父类@Before修饰的方法会在子类@Before修饰的方法执行前 执行
  • @After注解修饰的方法会在每个测试方法执行后执行一次,父类@After修饰的方法会在子类@After修饰的方法执行后执行。
  • @AfterClass注解修饰的方法(该方法要用static修饰)会在所有方法执行结束后执行一次,且也只执行一次,通常用来对资源进行释放,比如数据库连接的关闭等,无论测试用例里的其他方法有没有抛出异常,该方法最终都会被执行。而且父类中的被@AfterClass注解方法修饰的方法会在子类的@AfterClass注解修饰的方法执行之后才会被执行。
  • package tech.pdai.junit4;
    import org.junit.*;
     * Standard Test.
    public class StandardTest {
        @BeforeClass
        public static void beforeClass() {
            System.out.println("in before class");
        @AfterClass
        public static void afterClass() {
            System.out.println("in after class");
        @Before
        public void before() {
            System.out.println("in before");
        @After
        public void after() {
            System.out.println("in after");
        @Test
        public void testCase1() {
            System.out.println("in test case 1");
        @Test
        public void testCase2() {
            System.out.println("in test case 2");
    

    测试:禁用测试

    @Ignore:暂不执行该方法;

    package tech.pdai.junit4;
    import org.junit.Ignore;
    import org.junit.Test;
     * Ignore Test.
    public class IgnoreTest {
         * ignore.
        @Ignore
        @Test
        public void ignoreTest(){
            System.out.println("ignore test");
    

    测试:断言测试

  • 断言测试注解有哪些
  • 断言描述
    void assertEquals([String message],expected value,actual value)断言两个值相等。值类型可能是int,short,long,byte,char,Object,第一个参数是一个可选字符串消息
    void assertTrue([String message],boolean condition)断言一个条件为真
    void assertFalse([String message],boolean condition)断言一个条件为假
    void assertNotNull([String message],java.lang.Object object)断言一个对象不为空(null)
    void assertNull([String message],java.lang.Object object)断言一个对象为空(null)
    void assertSame([String message],java.lang.Object expected,java.lang.Object actual)断言两个对象引用相同的对象
    void assertNotSame([String message],java.lang.Object unexpected,java.lang.Object actual)断言两个对象不是引用同一个对象
    void assertArrayEquals([String message],expectedArray,resultArray)断言预期数组和结果数组相等,数组类型可能是int,short,long,byte,char,Object
    package tech.pdai.junit4;
    import org.junit.Assert;
    import org.junit.Test;
     * Assertion Test.
    public class AssertionTest {
        @Test
        public void test() {
            String obj1 = "junit";
            String obj2 = "junit";
            String obj3 = "test";
            String obj4 = "test";
            String obj5 = null;
            int var1 = 1;
            int var2 = 2;
            int[] array1 = {1, 2, 3};
            int[] array2 = {1, 2, 3};
            Assert.assertEquals(obj1, obj2);
            Assert.assertSame(obj3, obj4);
            Assert.assertNotSame(obj2, obj4);
            Assert.assertNotNull(obj1);
            Assert.assertNull(obj5);
            Assert.assertTrue(var1 < var2);
            Assert.assertFalse(var1 > var2);
            Assert.assertArrayEquals(array1, array2);
    
  • 更多测试,来自官网github.com/junit-team/…
  • package tech.pdai.junit4;
    import org.hamcrest.core.CombinableMatcher;
    import org.junit.Test;
    import java.util.Arrays;
    import static org.hamcrest.CoreMatchers.*;
    import static org.junit.Assert.*;
     * More Assertion Test from Junit-Team.
    public class Assertion2Test {
        @Test
        public void testAssertArrayEquals() {
            byte[] expected = "trial".getBytes();
            byte[] actual = "trial".getBytes();
            assertArrayEquals("failure - byte arrays not same", expected, actual);
        @Test
        public void testAssertEquals() {
            assertEquals("failure - strings are not equal", "text", "text");
        @Test
        public void testAssertFalse() {
            assertFalse("failure - should be false", false);
        @Test
        public void testAssertNotNull() {
            assertNotNull("should not be null", new Object());
        @Test
        public void testAssertNotSame() {
            assertNotSame("should not be same Object", new Object(), new Object());
        @Test
        public void testAssertNull() {
            assertNull("should be null", null);
        @Test
        public void testAssertSame() {
            Integer aNumber = Integer.valueOf(768);
            assertSame("should be same", aNumber, aNumber);
        // JUnit Matchers assertThat
        @Test
        public void testAssertThatBothContainsString() {
            assertThat("albumen", both(containsString("a")).and(containsString("b")));
        @Test
        public void testAssertThatHasItems() {
            assertThat(Arrays.asList("one", "two", "three"), hasItems("one", "three"));
        @Test
        public void testAssertThatEveryItemContainsString() {
            assertThat(Arrays.asList(new String[]{"fun", "ban", "net"}), everyItem(containsString("n")));
        // Core Hamcrest Matchers with assertThat
        @Test
        public void testAssertThatHamcrestCoreMatchers() {
            assertThat("good", allOf(equalTo("good"), startsWith("good")));
            assertThat("good", not(allOf(equalTo("bad"), equalTo("good"))));
            assertThat("good", anyOf(equalTo("bad"), equalTo("good")));
            assertThat(7, not(CombinableMatcher.<Integer>either(equalTo(3)).or(equalTo(4))));
            assertThat(new Object(), not(sameInstance(new Object())));
        @Test
        public void testAssertTrue() {
            assertTrue("failure - should be true", true);
    

    测试:异常测试

    Junit 用代码处理提供了一个追踪异常的选项。你可以测试代码是否它抛出了想要得到的异常。expected 参数和 @Test 注释一起使用。现在让我们看看 @Test(expected):

    package tech.pdai.junit4;
    import org.junit.Test;
     * Exception Test.
    public class ExceptionTest {
        @Test(expected = ArithmeticException.class)
        public void exceptionTest() {
            System.out.println("in exception success test");
            int a = 0;
            int b = 1 / a;
        @Test(expected = NullPointerException.class)
        public void exceptionFailTest() {
            System.out.println("in exception fail test");
            int a = 0;
            int b = 1 / a;
    

    测试:时间测试

    JUnit提供了一个暂停的方便选项,如果一个测试用例比起指定的毫秒数花费了更多的时间,那么JUnit将自动将它标记为失败,timeout参数和@Test注解一起使用,例如@Test(timeout=1000)。

    package tech.pdai.junit4;
    import org.junit.Test;
    import java.util.concurrent.TimeUnit;
     * Timeout Test.
    public class TimeoutTest {
        @Test(timeout = 1000)
        public void testCase1() throws InterruptedException {
            TimeUnit.SECONDS.sleep(5000);
            System.out.println("in timeout exception");
    

    应用到测试类的所有测试用例

    package tech.pdai.junit4;
    import org.junit.Rule;
    import org.junit.Test;
    import org.junit.rules.Timeout;
    import java.util.concurrent.CountDownLatch;
    import java.util.concurrent.TimeUnit;
     * Timeout Rule.
    public class HasGlobalTimeoutTest {
        public static String log;
        private final CountDownLatch latch = new CountDownLatch(1);
        @Rule
        public Timeout globalTimeout = Timeout.seconds(10); // 10 seconds max per method tested
        @Test
        public void testSleepForTooLong() throws Exception {
            log += "ran1";
            TimeUnit.SECONDS.sleep(100); // sleep for 100 seconds
        @Test
        public void testBlockForever() throws Exception {
            log += "ran2";
            latch.await(); // will block
    

    测试:参数化测试

    Junit 4 引入了一个新的功能参数化测试。参数化测试允许开发人员使用不同的值反复运行同 一个测试。你将遵循 5 个步骤来创建参数化测试:

  • 为准备使用参数化测试的测试类指定特殊的运行器 org.junit.runners.Parameterized。
  • 为测试类声明几个变量,分别用于存放期望值和测试所用数据。
  • 为测试类声明一个带有参数的公共构造函数,并在其中为第二个环节中声明的几个变量赋值。
  • 为测试类声明一个使用注解 org.junit.runners.Parameterized.Parameters 修饰的,返回值为 java.util.Collection 的公共静态方法,并在此方法中初始化所有需要测试的参数对。
  • 编写测试方法,使用定义的变量作为参数进行测试
  • package tech.pdai.junit4;
     * PrimeNumberChecker.
    public class PrimeNumberChecker {
        public Boolean validate(final Integer parimeNumber) {
            for (int i = 2; i < (parimeNumber / 2); i++) {
                if (parimeNumber % i == 0) {
                    return false;
            return true;
    
    package tech.pdai.junit4;
    import org.junit.Assert;
    import org.junit.Before;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.junit.runners.Parameterized;
    import java.util.Arrays;
    import java.util.Collection;
     * Parameterized Test.
    @RunWith(Parameterized.class) // 步骤一: 指定定参数运行器
    public class PrimeNumberCheckerTest {
         * 步骤二:声明变量
        private Integer inputNumber;
        private Boolean expectedResult;
        private PrimeNumberChecker primeNumberChecker;
         * 步骤三:为测试类声明一个带有参数的公共构造函数,为变量赋值
        public PrimeNumberCheckerTest(Integer inputNumber,
                                      Boolean expectedResult) {
            this.inputNumber = inputNumber;
            this.expectedResult = expectedResult;
         * 步骤四:为测试类声明一个使用注解 org.junit.runners.Parameterized.Parameters 修饰的,返回值为
         * java.util.Collection 的公共静态方法,并在此方法中初始化所有需要测试的参数对
         *   1)该方法必须由Parameters注解修饰
         2)该方法必须为public static的
         3)该方法必须返回Collection类型
         4)该方法的名字不做要求
         5)该方法没有参数
        @Parameterized.Parameters
        public static Collection primeNumbers() {
            return Arrays.asList(new Object[][]{
                    {2, true},
                    {6, false},
                    {19, true},
                    {22, false},
                    {23, true}
        @Before
        public void initialize() {
            primeNumberChecker = new PrimeNumberChecker();
         * 步骤五:编写测试方法,使用自定义变量进行测试
        @Test
        public void testPrimeNumberChecker() {
            System.out.println("Parameterized Number is : " + inputNumber);
            Assert.assertEquals(expectedResult,
                    primeNumberChecker.validate(inputNumber));
    

    测试:套件测试

    “套件测试”是指捆绑了几个单元测试用例并运行起来。在JUnit中,@RunWith 和 @Suite 这两个注解是用来运行套件测试。先来创建几个测试类

    package tech.pdai.junit4.testsuite;
    import org.junit.Test;
    public class JunitTest1 {
        @Test
        public void printMessage(){
            System.out.println("in JunitTest1");
    
    package tech.pdai.junit4.testsuite;
    import org.junit.Test;
    public class JunitTest2 {
        @Test
        public void printMessage(){
            System.out.println("in JunitTest2");
    
    package tech.pdai.junit4.testsuite;
    import org.junit.runner.RunWith;
    import org.junit.runners.Suite;
     * Test suite.
    @RunWith(Suite.class)
    @Suite.SuiteClasses({
             * 此处类的配置顺序会影响执行顺序
            JunitTest1.class,
            JunitTest2.class
    public class JunitSuiteTest {
    

    测试:测试顺序

    自定义测试方法的顺序,比如按照方法的名字顺序:

    package tech.pdai.junit4;
    import org.junit.FixMethodOrder;
    import org.junit.Test;
    import org.junit.runners.MethodSorters;
     * Order.
    @FixMethodOrder(MethodSorters.NAME_ASCENDING)
    public class TestMethodOrder {
        @Test
        public void testA() {
            System.out.println("first");
        @Test
        public void testC() {
            System.out.println("third");
        @Test
        public void testB() {
            System.out.println("second");
    

    单元测试 - Junit5 详解

    JUnit 5是JUnit的下一代。目标是为JVM上的开发人员端测试创建一个最新的基础。这包括专注于Java 8及更高版本,以及启用许多不同风格的测试。

    最好的资料依然在Junit官方网站,以下我帮你总结下Junit相关的官方网址。@pdai

    junit.org/junit5/

  • 官方入门文档
  • junit.org/junit5/docs…

    github.com/junit-team/…

  • 官方github
  • github.com/junit-team

    Junit5的架构

    与以前版本的JUnit不同,JUnit 5由三个不同子项目中的几个不同模块组成。

    JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

  • JUnit Platform是基于JVM的运行测试的基础框架在,它定义了开发运行在这个测试框架上的TestEngine API。此外该平台提供了一个控制台启动器,可以从命令行启动平台,可以为Gradle和 Maven构建插件,同时提供基于JUnit 4的Runner。
  • JUnit Jupiter是在JUnit 5中编写测试和扩展的新编程模型和扩展模型的组合.Jupiter子项目提供了一个TestEngine在平台上运行基于Jupiter的测试。
  • JUnit Vintage提供了一个TestEngine在平台上运行基于JUnit 3和JUnit 4的测试。
  • 架构图如下:

    JUnit Jupiter API 的使用

    JUnit Jupiter是在JUnit 5中编写测试和扩展的新编程模型和扩展模型的组合; 所以我们使用Jupiter来学习Junit5。

    @Test 表示方法是一种测试方法。 与JUnit 4的@Test注解不同,此注释不会声明任何属性。

    @ParameterizedTest 表示方法是参数化测试

    @RepeatedTest 表示方法是重复测试模板

    @TestFactory 表示方法是动态测试的测试工程

    @DisplayName 为测试类或者测试方法自定义一个名称

    @BeforeEach 表示方法在每个测试方法运行前都会运行 , @AfterEach 表示方法在每个测试方法运行之后都会运行

    @BeforeAll 表示方法在所有测试方法之前运行 , @AfterAll 表示方法在所有测试方法之后运行

    @Nested 表示带注解的类是嵌套的非静态测试类, @BeforeAll@AfterAll方法不能直接在@Nested测试类中使用,除非修改测试实例生命周期。

    @Tag 用于在类或方法级别声明用于过滤测试的标记

    @Disabled 用于禁用测试类或测试方法

    @ExtendWith 用于注册自定义扩展,该注解可以继承

    @FixMethodOrder(MethodSorters.NAME_ASCENDING) ,控制测试类中方法执行的顺序,这种测试方式将按方法名称的进行排序,由于是按字符的字典顺序,所以以这种方式指定执行顺序会始终保持一致;不过这种方式需要对测试方法有一定的命名规则,如 测试方法均以testNNN开头(NNN表示测试方法序列号 001-999)

    编写单元测试

    接下来,我们开始学习JUnit5单元测试实例:

    Maven包引入

    最新的包引入,请参考这里:junit.org/junit5/docs…

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <groupId>pdai.tech</groupId>
      <artifactId>java-junit5</artifactId>
      <version>1.0-SNAPSHOT</version>
      <dependencies>
        <!-- Only needed to run tests in a version of IntelliJ IDEA that bundles older versions -->
        <dependency>
          <groupId>org.junit.platform</groupId>
          <artifactId>junit-platform-launcher</artifactId>
          <version>1.7.0</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>org.junit.jupiter</groupId>
          <artifactId>junit-jupiter-engine</artifactId>
          <version>5.7.0</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>org.junit.vintage</groupId>
          <artifactId>junit-vintage-engine</artifactId>
          <version>5.7.0</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>org.junit.jupiter</groupId>
          <artifactId>junit-jupiter-api</artifactId>
          <version>5.7.0</version>
        </dependency>
        <!-- lombok -->
        <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
          <version>1.18.16</version>
        </dependency>
      </dependencies>
    </project>
    

    测试:Hello World

    第一个测试:

    package tech.pdai.junit5;
    import org.junit.jupiter.api.Test;
    import static org.junit.jupiter.api.Assertions.assertEquals;
    * Hello world test.
    * @author pdai
    public class HelloWorldTest {
        @Test
        void firstTest() {
            assertEquals(2, 1 + 1);
    

    @Test注解在方法上标记方法为测试方法,以便构建工具和 IDE 能够识别并执行它们。JUnit 5不再需要手动将测试类与测试方法为public,包可见的访问级别就足够了。

    测试:生命周期

    首先,需要对比下Junit5和Junit4注解:

    Junit4Junit5注释
    @Test@Test表示该方法是一个测试方法
    @BeforeClass@BeforeAll表示使用了该注解的方法应该在当前类中所有测试方法之前执行(只执行一次),并且它必须是 static方法(除非@TestInstance指定生命周期为Lifecycle.PER_CLASS)
    @AfterClass@AfterAll表示使用了该注解的方法应该在当前类中所有测试方法之后执行(只执行一次),并且它必须是 static方法(除非@TestInstance指定生命周期为Lifecycle.PER_CLASS)
    @Before@BeforeEach表示使用了该注解的方法应该在当前类中每一个测试方法之前执行
    @After@AfterEach表示使用了该注解的方法应该在当前类中每一个测试方法之后执行
    @Ignore@Disabled用于禁用(或者说忽略)一个测试类或测试方法
    @Category@Tag用于声明过滤测试的tag标签,该注解可以用在方法或类上

    测试用例:

    package tech.pdai.junit5;
    import static org.junit.jupiter.api.Assertions.fail;
    import static org.junit.jupiter.api.Assumptions.assumeTrue;
    import lombok.extern.slf4j.Slf4j;
    import org.junit.jupiter.api.AfterAll;
    import org.junit.jupiter.api.AfterEach;
    import org.junit.jupiter.api.BeforeAll;
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.Disabled;
    import org.junit.jupiter.api.Test;
     * Standard Test.
     * @author pdai
    public class StandardTest {
        @BeforeAll
        static void initAll() {
            System.out.println("BeforeAll");
        @BeforeEach
        void init() {
            System.out.println("BeforeEach");
        @Test
        void succeedingTest() {
            System.out.println("succeedingTest");
        @Test
        void failingTest() {
            System.out.println("failingTest");
            fail("a failing test");
        @Test
        @Disabled("for demonstration purposes")
        void skippedTest() {
            // not executed
        @Test
        void abortedTest() {
            System.out.println("abortedTest");
            assumeTrue("abc".contains("Z"));
            fail("test should have been aborted");
        @AfterEach
        void tearDown() {
            System.out.println("AfterEach");
        @AfterAll
        static void tearDownAll() {
            System.out.println("AfterEach");
    

    测试:禁用测试

    这是一个禁用的测试案例:

    import org.junit.jupiter.api.Disabled;
    import org.junit.jupiter.api.Test;
    @Disabled
    class DisabledClassTest {
        @Test
        void testWillBeSkipped() {
    

    这是一个带有禁用测试方法的测试案例:

    import org.junit.jupiter.api.Disabled;
    import org.junit.jupiter.api.Test;
    class DisabledTest {
        @Disabled
        @Test
        void testWillBeSkipped() {
        @Test
        void testWillBeExecuted() {
    

    测试:断言测试

    准备好测试实例、执行了被测类的方法以后,断言能确保你得到了想要的结果。一般的断言,无非是检查一个实例的属性(比如,判空与判非空等),或者对两个实例进行比较(比如,检查两个实例对象是否相等)等。无论哪种检查,断言方法都可以接受一个字符串作为最后一个可选参数,它会在断言失败时提供必要的描述信息。如果提供出错信息的过程比较复杂,它也可以被包装在一个 lambda 表达式中,这样,只有到真正失败的时候,消息才会真正被构造出来。

  • 常用断言 Assertions
  • import tech.pdai.junit5.entity.Person; import static java.time.Duration.ofMillis; import static java.time.Duration.ofMinutes; import static org.junit.jupiter.api.Assertions.*; * Assertions Test. public class AssertionsTest { Person person = new Person("John", "Doe"); @Test void standardAssertions() { assertEquals(2, 2); assertEquals(4, 4, "The optional assertion message is now the last parameter."); assertTrue(2 == 2, () -> "Assertion messages can be lazily evaluated -- " + "to avoid constructing complex messages unnecessarily."); @Test void groupedAssertions() { // In a grouped assertion all assertions are executed, and any // failures will be reported together. assertAll("person", () -> assertEquals("John", person.getFirstName()), () -> assertEquals("Doe", person.getLastName()) @Test void dependentAssertions() { // Within a code block, if an assertion fails the // subsequent code in the same block will be skipped. assertAll("properties", () -> { String firstName = person.getFirstName(); assertNotNull(firstName); // Executed only if the previous assertion is valid. assertAll("first name", () -> assertTrue(firstName.startsWith("J")), () -> assertTrue(firstName.endsWith("n")) () -> { // Grouped assertion, so processed independently // of results of first name assertions. String lastName = person.getLastName(); assertNotNull(lastName); // Executed only if the previous assertion is valid. assertAll("last name", () -> assertTrue(lastName.startsWith("D")), () -> assertTrue(lastName.endsWith("e")) @Test void exceptionTesting() { Throwable exception = assertThrows(IllegalArgumentException.class, () -> { throw new IllegalArgumentException("a message"); assertEquals("a message", exception.getMessage()); @Test void timeoutNotExceeded() { // The following assertion succeeds. assertTimeout(ofMinutes(2), () -> { // Perform task that takes less than 2 minutes. @Test void timeoutNotExceededWithResult() { // The following assertion succeeds, and returns the supplied object. String actualResult = assertTimeout(ofMinutes(2), () -> { return "a result"; assertEquals("a result", actualResult); @Test void timeoutNotExceededWithMethod() { // The following assertion invokes a method reference and returns an object. String actualGreeting = assertTimeout(ofMinutes(2), AssertionsTest::greeting); assertEquals("hello world!", actualGreeting); @Test void timeoutExceeded() { // The following assertion fails with an error message similar to: // execution exceeded timeout of 10 ms by 91 ms assertTimeout(ofMillis(10), () -> { // Simulate task that takes more than 10 ms. Thread.sleep(100); @Test void timeoutExceededWithPreemptiveTermination() { // The following assertion fails with an error message similar to: // execution timed out after 10 ms assertTimeoutPreemptively(ofMillis(10), () -> { // Simulate task that takes more than 10 ms. Thread.sleep(100); private static String greeting() { return "hello world!";

    这里注意下:assertTimeoutPreemptively() 和 assertTimeout() 的区别为: 两者都是断言超时,前者在指定时间没有完成任务就会立即返回断言失败;后者会在任务执行完毕之后才返回。

    执行结果:

    测试:异常测试

    我们代码中对于带有异常的方法通常都是使用 try-catch 方式捕获处理,针对测试这样带有异常抛出的代码,而 JUnit 5 提供方法 Assertions#assertThrows(Class, Executable) 来进行测试,第一个参数为异常类型,第二个为函数式接口参数,跟 Runnable 接口相似,不需要参数,也没有返回,并且支持 Lambda表达式方式使用,具体使用方式可参考下方代码:

    package tech.pdai.junit5;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;
    import static org.junit.jupiter.api.Assertions.assertThrows;
     * Exception Test.
    public class ExceptionTest {
        // 标准的测试例子
        @Test
        @DisplayName("Exception Test Demo")
        void assertThrowsException() {
            String str = null;
            assertThrows(IllegalArgumentException.class, () -> {
                Integer.valueOf(str);
        // 注:异常失败例子,当Lambda表达式中代码出现的异常会跟首个参数的异常类型进行比较,如果不属于同一类异常,则失败
        @Test
        @DisplayName("Exception Test Demo2")
        void assertThrowsException2() {
            String str = null;
            assertThrows(NullPointerException.class, () -> {
                Integer.valueOf(str);
    

    测试:嵌套测试

    嵌套测试给测试编写者更多的能力,来表达几组测试之间的关系。这里有一个详细的例子。

    用于测试stack的嵌套测试套件:

    package tech.pdai.junit5;
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Nested;
    import org.junit.jupiter.api.Test;
    import java.util.EmptyStackException;
    import java.util.Stack;
    import static org.junit.jupiter.api.Assertions.*;
     * Stack test for Nest Demo.
    @DisplayName("A stack")
    public class NestedTest {
        Stack stack;
        @Test
        @DisplayName("is instantiated with new Stack()")
        void isInstantiatedWithNew() {
            new Stack<>();
        @Nested
        @DisplayName("when new")
        class WhenNew {
            @BeforeEach
            void createNewStack() {
                stack = new Stack<>();
            @Test
            @DisplayName("is empty")
            void isEmpty() {
                assertTrue(stack.isEmpty());
            @Test
            @DisplayName("throws EmptyStackException when popped")
            void throwsExceptionWhenPopped() {
                assertThrows(EmptyStackException.class, () -> stack.pop());
            @Test
            @DisplayName("throws EmptyStackException when peeked")
            void throwsExceptionWhenPeeked() {
                assertThrows(EmptyStackException.class, () -> stack.peek());
            @Nested
            @DisplayName("after pushing an element")
            class AfterPushing {
                String anElement = "an element";
                @BeforeEach
                void pushAnElement() {
                    stack.push(anElement);
                @Test
                @DisplayName("it is no longer empty")
                void isNotEmpty() {
                    assertFalse(stack.isEmpty());
                @Test
                @DisplayName("returns the element when popped and is empty")
                void returnElementWhenPopped() {
                    assertEquals(anElement, stack.pop());
                    assertTrue(stack.isEmpty());
                @Test
                @DisplayName("returns the element when peeked but remains not empty")
                void returnElementWhenPeeked() {
                    assertEquals(anElement, stack.peek());
                    assertFalse(stack.isEmpty());
    

    测试:重复测试

    JUnit Jupiter通过使用@RepeatedTest注解方法并指定所需的重复次数,提供了重复测试指定次数的功能。每次重复测试的调用都像执行常规的@Test方法一样,完全支持相同的生命周期回调和扩展。

    以下示例演示了如何声明名为repeatedTest()的测试,该测试将自动重复10次。

    @RepeatedTest(10)
    void repeatedTest() {
        // ...
    

    除了指定重复次数外,还可以通过@RepeatedTest注解的name属性为每次重复配置自定义显示名称。此外,显示名称可以是模式,由静态文本和动态占位符的组合而成。目前支持以下占位符:

  • {displayName}: @RepeatedTest方法的显示名称
  • {currentRepetition}: 当前重复次数
  • {totalRepetitions}: 重复的总次数
  • package tech.pdai.junit5;
    import org.junit.jupiter.api.*;
    import static org.junit.jupiter.api.Assertions.assertEquals;
     * Repeat Test.
    public class RepeatTest {
        @BeforeEach
        void beforeEach(TestInfo testInfo, RepetitionInfo repetitionInfo) {
            int currentRepetition = repetitionInfo.getCurrentRepetition();
            int totalRepetitions = repetitionInfo.getTotalRepetitions();
            String methodName = testInfo.getTestMethod().get().getName();
            System.out.println(String.format("About to execute repetition %d of %d for %s", //
                    currentRepetition, totalRepetitions, methodName));
        @RepeatedTest(3)
        void repeatedTest() {
            // ...
        @RepeatedTest(2)
        void repeatedTestWithRepetitionInfo(RepetitionInfo repetitionInfo) {
            assertEquals(2, repetitionInfo.getTotalRepetitions());
        @RepeatedTest(value = 1, name = "{displayName} {currentRepetition}/{totalRepetitions}")
        @DisplayName("Repeat!")
        void customDisplayName(TestInfo testInfo) {
            assertEquals(testInfo.getDisplayName(), "Repeat! 1/1");
        @RepeatedTest(value = 1, name = RepeatedTest.LONG_DISPLAY_NAME)
        @DisplayName("Details...")
        void customDisplayNameWithLongPattern(TestInfo testInfo) {
            assertEquals(testInfo.getDisplayName(), "Details... :: repetition 1 of 1");
        @RepeatedTest(value = 2, name = "Wiederholung {currentRepetition} von {totalRepetitions}")
        void repeatedTestInGerman() {
            // ...
    

    测试:参数化测试

    JUnit Jupiter开箱即用,提供了不少source注解。下面的每个小节都为他们提供了简要的概述和示例。请参阅org.junit.jupiter.params.provider包中的JavaDoc以获取更多信息。

  • @ValueSource
  • @ValueSource是最简单的source之一。它可以让你指定一个原生类型(String,int,long或double)的数组,并且只能为每次调用提供一个参数。

    @ParameterizedTest
    @ValueSource(ints = { 1, 2, 3 })
    void testWithValueSource(int argument) {
        assertNotNull(argument);
    
  • @EnumSource
  • @EnumSource提供了一个使用Enum常量的简便方法。该注释提供了一个可选的name参数,可以指定使用哪些常量。如果省略,所有的常量将被用在下面的例子中。

    @ParameterizedTest
    @EnumSource(TimeUnit.class)
    void testWithEnumSource(TimeUnit timeUnit) {
        assertNotNull(timeUnit);
    @ParameterizedTest
    @EnumSource(value = TimeUnit.class, names = { "DAYS", "HOURS" })
    void testWithEnumSourceInclude(TimeUnit timeUnit) {
        assertTrue(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
    

    @EnumSource注解还提供了一个可选的mode参数,可以对将哪些常量传递给测试方法进行细化控制。例如,您可以从枚举常量池中排除名称或指定正则表达式,如下例所示。

    @ParameterizedTest
    @EnumSource(value = TimeUnit.class, mode = EXCLUDE, names = { "DAYS", "HOURS" })
    void testWithEnumSourceExclude(TimeUnit timeUnit) {
        assertFalse(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
        assertTrue(timeUnit.name().length() > 5);
    @ParameterizedTest
    @EnumSource(value = TimeUnit.class, mode = MATCH_ALL, names = "^(M|N).+SECONDS$")
    void testWithEnumSourceRegex(TimeUnit timeUnit) {
        String name = timeUnit.name();
        assertTrue(name.startsWith("M") || name.startsWith("N"));
        assertTrue(name.endsWith("SECONDS"));
    
  • @MethodSource
  • @MethodSource允许你引用一个或多个测试类的工厂方法。这样的方法必须返回一个Stream,Iterable,Iterator或者参数数组。另外,这种方法不能接受任何参数。默认情况下,除非测试类用@TestInstance(Lifecycle.PER_CLASS)注解,否则这些方法必须是静态的。

    如果只需要一个参数,则可以返回参数类型的实例Stream,如以下示例所示。

    @ParameterizedTest
    @MethodSource("stringProvider")
    void testWithSimpleMethodSource(String argument) {
        assertNotNull(argument);
    static Stream<String> stringProvider() {
        return Stream.of("foo", "bar");
    

    支持原始类型(DoubleStream,IntStream和LongStream)的流,示例如下:

    @ParameterizedTest
    @MethodSource("range")
    void testWithRangeMethodSource(int argument) {
        assertNotEquals(9, argument);
    static IntStream range() {
        return IntStream.range(0, 20).skip(10);
    

    如果测试方法声明多个参数,则需要返回一个集合或Arguments实例流,如下所示。请注意,Arguments.of(Object…)是Arguments接口中定义的静态工厂方法。

    @ParameterizedTest
    @MethodSource("stringIntAndListProvider")
    void testWithMultiArgMethodSource(String str, int num, List<String> list) {
        assertEquals(3, str.length());
        assertTrue(num >=1 && num <=2);
        assertEquals(2, list.size());
    static Stream<Arguments> stringIntAndListProvider() {
        return Stream.of(
            Arguments.of("foo", 1, Arrays.asList("a", "b")),
            Arguments.of("bar", 2, Arrays.asList("x", "y"))
    
  • @CsvSource
  • @CsvSource允许您将参数列表表示为以逗号分隔的值(例如,字符串文字)。

    @ParameterizedTest
    @CsvSource({ "foo, 1", "bar, 2", "'baz, qux', 3" })
    void testWithCsvSource(String first, int second) {
        assertNotNull(first);
        assertNotEquals(0, second);
    

    @CsvSource使用'作为转义字符。 请参阅上述示例和下表中的’baz, qux’值。 一个空的引用值''会导致一个空的String; 而一个完全空的值被解释为一个null引用。如果null引用的目标类型是基本类型,则引发ArgumentConversionException。

    示例输入结果字符列表
    @CsvSource({ “foo, bar” })"foo", "bar"
    @CsvSource({ “foo, ‘baz, qux’” })"foo", "baz, qux"
    @CsvSource({ “foo, ‘’” })"foo", ""
    @CsvSource({ “foo, “ })"foo", null
  • @CsvFileSource
  • @CsvFileSource让你使用classpath中的CSV文件。CSV文件中的每一行都会导致参数化测试的一次调用。

    @ParameterizedTest
    @CsvFileSource(resources = "/two-column.csv")
    void testWithCsvFileSource(String first, int second) {
        assertNotNull(first);
        assertNotEquals(0, second);
    

    two-column.csv

    foo, 1
    bar, 2
    "baz, qux", 3
    

    与@CsvSource中使用的语法相反,@CsvFileSource使用双引号"作为转义字符,请参阅上面例子中的"baz, qux"值,一个空的转义值""会产生一个空字符串, 一个完全为空的值被解释为null引用,如果null引用的目标类型是基本类型,则引发ArgumentConversionException。

  • @ArgumentsSource
  • 可以使用@ArgumentsSource指定一个自定义的,可重用的ArgumentsProvider。

    @ParameterizedTest
    @ArgumentsSource(MyArgumentsProvider.class)
    void testWithArgumentsSource(String argument) {
        assertNotNull(argument);
    static class MyArgumentsProvider implements ArgumentsProvider {
        @Override
        public Stream< ? extends Arguments > provideArguments(ExtensionContext context) {
            return Stream.of("foo", "bar").map(Arguments::of);
    

    测试:动态测试

    除了这些标准测试外,JUnit Jupiter还引入了一种全新的测试编程模型。这种新的测试是动态测试,它是由 @TestFactory 注解的工厂方法在运行时生成的。

    与@Test方法相比,@TestFactory方法本身不是测试用例,而是测试用例的工厂。因此,动态测试是工厂的产物。从技术上讲,@TestFactory方法必须返回DynamicNode实例的Stream,Collection,Iterable或Iterator。 DynamicNode的可实例化的子类是DynamicContainer和DynamicTest。 DynamicContainer实例由一个显示名称和一个动态子节点列表组成,可以创建任意嵌套的动态节点层次结构。然后,DynamicTest实例将被延迟执行,从而实现测试用例的动态甚至非确定性生成。

    任何由@TestFactory返回的Stream都要通过调用stream.close()来正确关闭,使得使用诸如Files.lines()之类的资源变得安全。

    与@Test方法一样,@TestFactory方法不能是private或static,并且可以选择声明参数,以便通过ParameterResolvers解析。

    DynamicTest是运行时生成的测试用例。它由显示名称和Executable组成。 Executable是@FunctionalInterface,这意味着动态测试的实现可以作为lambda表达式或方法引用来提供。

    package tech.pdai.junit5;
    import org.junit.jupiter.api.DynamicNode;
    import org.junit.jupiter.api.DynamicTest;
    import org.junit.jupiter.api.TestFactory;
    import org.junit.jupiter.api.function.ThrowingConsumer;
    import java.util.*;
    import java.util.function.Function;
    import java.util.stream.IntStream;
    import java.util.stream.Stream;
    import static org.junit.jupiter.api.Assertions.*;
    import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
    import static org.junit.jupiter.api.DynamicTest.dynamicTest;
     * Dynamic Test.
    public class DynamicsTest {
        // This will result in a JUnitException!
        @TestFactory
        List<String> dynamicTestsWithInvalidReturnType() {
            return Arrays.asList("Hello");
        @TestFactory
        Collection<DynamicTest> dynamicTestsFromCollection() {
            return Arrays.asList(
                    dynamicTest("1st dynamic test", () -> assertTrue(true)),
                    dynamicTest("2nd dynamic test", () -> assertEquals(4, 2 * 2))
        @TestFactory
        Iterable<DynamicTest> dynamicTestsFromIterable() {
            return Arrays.asList(
                    dynamicTest("3rd dynamic test", () -> assertTrue(true)),
                    dynamicTest("4th dynamic test", () -> assertEquals(4, 2 * 2))
        @TestFactory
        Iterator<DynamicTest> dynamicTestsFromIterator() {
            return Arrays.asList(
                    dynamicTest("5th dynamic test", () -> assertTrue(true)),
                    dynamicTest("6th dynamic test", () -> assertEquals(4, 2 * 2))
            ).iterator();
        @TestFactory
        Stream<DynamicTest> dynamicTestsFromStream() {
            return Stream.of("A", "B", "C")
                    .map(str -> dynamicTest("test" + str, () -> { /* ... */ }));
        @TestFactory
        Stream<DynamicTest> dynamicTestsFromIntStream() {
            // Generates tests for the first 10 even integers.
            return IntStream.iterate(0, n -> n + 2).limit(10)
                    .mapToObj(n -> dynamicTest("test" + n, () -> assertTrue(n % 2 == 0)));
        @TestFactory
        Stream<DynamicTest> generateRandomNumberOfTests() {
            // Generates random positive integers between 0 and 100 until
            // a number evenly divisible by 7 is encountered.
            Iterator<Integer> inputGenerator = new Iterator<Integer>() {
                Random random = new Random();
                int current;
                @Override
                public boolean hasNext() {
                    current = random.nextInt(100);
                    return current % 7 != 0;
                @Override
                public Integer next() {
                    return current;
            // Generates display names like: input:5, input:37, input:85, etc.
            Function<Integer, String> displayNameGenerator = (input) -> "input:" + input;
            // Executes tests based on the current input value.
            ThrowingConsumer<Integer> testExecutor = (input) -> assertTrue(input % 7 != 0);
            // Returns a stream of dynamic tests.
            return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor);
        @TestFactory
        Stream<DynamicNode> dynamicTestsWithContainers() {
            return Stream.of("A", "B", "C")
                    .map(input -> dynamicContainer("Container " + input, Stream.of(
                            dynamicTest("not null", () -> assertNotNull(input)),
                            dynamicContainer("properties", Stream.of(
                                    dynamicTest("length > 0", () -> assertTrue(input.length() > 0)),
                                    dynamicTest("not empty", () -> assertFalse(input.isEmpty()))
    

    单元测试 - Mockito 详解

    Mock通常是指,在测试一个对象A时,我们构造一些假的对象来模拟与A之间的交互,而这些Mock对象的行为是我们事先设定且符合预期。通过这些Mock对象来测试A在正常逻辑,异常逻辑或压力情况下工作是否正常。而Mockito是最流行的Java mock框架之一。

    什么是 Mock 测试

    Mock 测试就是在测试过程中,对于某些不容易构造(如 HttpServletRequest 必须在Servlet 容器中才能构造出来)或者不容易获取比较复杂的对象(如 JDBC 中的ResultSet 对象),用一个虚拟的对象(Mock 对象)来创建以便测试的测试方法。Mock 最大的功能是帮你把单元测试的耦合分解开,如果你的代码对另一个类或者接口有依赖,它能够帮你模拟这些依赖,并帮你验证所调用的依赖的行为。

    先来看看下面这个示例:

    从上图可以看出如果我们要对A进行测试,那么就要先把整个依赖树构建出来,也就是BCDE的实例。

    一种替代方案就是使用mocks

    从图中可以清晰的看出, mock对象就是在调试期间用来作为真实对象的替代品。

    mock测试就是在测试过程中,对那些不容易构建的对象用一个虚拟对象来代替测试的方法就叫mock测试。

    Mock 适用在什么场景

    在使用Mock的过程中,发现Mock是有一些通用性的,对于一些应用场景,是非常适合使用Mock的:

  • 真实对象具有不可确定的行为(产生不可预测的结果,如股票的行情)
  • 真实对象很难被创建(比如具体的web容器)
  • 真实对象的某些行为很难触发(比如网络错误)
  • 真实情况令程序的运行速度很慢
  • 真实对象有用户界面
  • 测试需要询问真实对象它是如何被调用的(比如测试可能需要验证某个回调函数是否被调用了)
  • 真实对象实际上并不存在(当需要和其他开发小组,或者新的硬件系统打交道的时候,这是一个普遍的问题)
  • 当然,也有一些不得不Mock的场景:

  • 一些比较难构造的Object:这类Object通常有很多依赖,在单元测试中构造出这样类通常花费的成本太大。
  • 执行操作的时间较长Object:有一些Object的操作费时,而被测对象依赖于这一个操作的执行结果,例如大文件写操作,数据的更新等等,出于测试的需求,通常将这类操作进行Mock。
  • 异常逻辑:一些异常的逻辑往往在正常测试中是很难触发的,通过Mock可以人为的控制触发异常逻辑。
  • 在一些压力测试的场景下,也不得不使用Mock,例如在分布式系统测试中,通常需要测试一些单点(如namenode,jobtracker)在压力场景下的工作是否正常。而通常测试集群在正常逻辑下无法提供足够的压力(主要原因是受限于机器数量),这时候就需要应用Mock去满足。

    Mockito

    Mockito是最流行的Java mock框架之一.

  • Mockito 官方网站
  • site.mockito.org/

  • PowerMockito Github
  • github.com/powermock/p…

    Maven包引入

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <groupId>pdai.tech</groupId>
      <artifactId>java-mockito</artifactId>
      <version>1.0-SNAPSHOT</version>
      <dependencies>
        <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.12</version>
          <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
        <dependency>
          <groupId>org.mockito</groupId>
          <artifactId>mockito-core</artifactId>
          <version>3.7.7</version>
          <scope>test</scope>
        </dependency>
      </dependencies>
    </project>
    

    测试:Hello World

    本例子主要用来测试DemoService类,但是DemoService又依赖于DemoDao,这时候我们便可以mock出DemoDao的返回预期值,从而测试DemoService类。

    待测试类DemoService

    package tech.pdai.mockito.service;
    import tech.pdai.mockito.dao.DemoDao;
    public class DemoService {
        private DemoDao demoDao;
        public DemoService(DemoDao demoDao) {
            this.demoDao = demoDao;
        public int getDemoStatus(){
            return demoDao.getDemoStatus();
    

    依赖DemoDao

    package tech.pdai.mockito.dao;
    import java.util.Random;
    public class DemoDao {
        public int getDemoStatus(){
            return new Random().nextInt();
    
    package tech.pdai.mockito;
    import org.junit.Assert;
    import org.junit.Test;
    import org.mockito.Mockito;
    import tech.pdai.mockito.dao.DemoDao;
    import tech.pdai.mockito.service.DemoService;
     * Hello World Test.
    public class HelloWorldTest {
        @Test
        public void helloWorldTest() {
            // mock DemoDao instance
            DemoDao mockDemoDao = Mockito.mock(DemoDao.class);
            // 使用 mockito 对 getDemoStatus 方法打桩
            Mockito.when(mockDemoDao.getDemoStatus()).thenReturn(1);
            // 调用 mock 对象的 getDemoStatus 方法,结果永远是 1
            Assert.assertEquals(1, mockDemoDao.getDemoStatus());
            // mock DemoService
            DemoService mockDemoService = new DemoService(mockDemoDao);
            Assert.assertEquals(1, mockDemoService.getDemoStatus() );
    

    测试:适用@Mock注解

    @Mock 注解可以理解为对 mock 方法的一个替代。

    使用该注解时,要使用MockitoAnnotations.initMocks 方法,让注解生效, 比如放在@Before方法中初始化。

    比较优雅优雅的写法是用MockitoJUnitRunner,它可以自动执行MockitoAnnotations.initMocks 方法。

    package tech.pdai.mockito;
    import org.junit.Assert;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.mockito.Mock;
    import org.mockito.junit.MockitoJUnitRunner;
    import java.util.Random;
    import static org.mockito.Mockito.when;
     * Mock Annotation
    @RunWith(MockitoJUnitRunner.class)
    public class MockAnnotationTest {
        @Mock
        private Random random;
        @Test
        public void test() {
            when(random.nextInt()).thenReturn(100);
            Assert.assertEquals(100, random.nextInt());
    

    测试:参数匹配

    如果参数匹配既申明了精确匹配,也声明了模糊匹配;又或者同一个值的精确匹配出现了两次,使用时会匹配符合匹配条件的最新声明的匹配。

    package tech.pdai.mockito;
    import org.junit.Assert;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.mockito.Mock;
    import org.mockito.junit.MockitoJUnitRunner;
    import java.util.List;
    import static org.mockito.ArgumentMatchers.anyInt;
    import static org.mockito.Mockito.when;
     * Mock Parameter Test.
    @RunWith(MockitoJUnitRunner.class)
    public class ParameterTest {
        @Mock
        private List<String> testList;
        @Test
        public void test01() {
            // 精确匹配 0
            when(testList.get(0)).thenReturn("a");
            Assert.assertEquals("a", testList.get(0));
            // 精确匹配 0
            when(testList.get(0)).thenReturn("b");
            Assert.assertEquals("b", testList.get(0));
            // 模糊匹配
            when(testList.get(anyInt())).thenReturn("c");
            Assert.assertEquals("c", testList.get(0));
            Assert.assertEquals("c", testList.get(1));
    

    anyInt 只是用来匹配参数的工具之一,目前 mockito 有多种匹配函数,部分如下:

    函数名匹配类型
    any()所有对象类型
    anyInt()基本类型 int、非 null 的 Integer 类型
    anyChar()基本类型 char、非 null 的 Character 类型
    anyShort()基本类型 short、非 null 的 Short 类型
    anyBoolean()基本类型 boolean、非 null 的 Boolean 类型
    anyDouble()基本类型 double、非 null 的 Double 类型
    anyFloat()基本类型 float、非 null 的 Float 类型
    anyLong()基本类型 long、非 null 的 Long 类型
    anyByte()基本类型 byte、非 null 的 Byte 类型
    anyString()String 类型(不能是 null)
    anyList()List 类型(不能是 null)
    anyMap()Map<K, V>类型(不能是 null)
    anyCollection()Collection类型(不能是 null)
    anySet()Set类型(不能是 null)
    any(Class type)type类型的对象(不能是 null)
    isNull()null
    notNull()非 null
    isNotNull()非 null

    测试:Mock异常

    Mockito 使用 thenThrow 让方法抛出异常

    如下代码中,包含两个例子:一个是单个异常,一个是多个异常。

    package tech.pdai.mockito;
    import org.junit.Assert;
    import org.junit.Test;
    import java.util.Random;
    import static org.mockito.Mockito.mock;
    import static org.mockito.Mockito.when;
     * Exception Test.
    public class ThrowTest {
         * 例子1: thenThrow 用来让函数调用抛出异常.
        @Test
        public void throwTest1() {
            Random mockRandom = mock(Random.class);
            when(mockRandom.nextInt()).thenThrow(new RuntimeException("异常"));
            try {
                mockRandom.nextInt();
                Assert.fail();  // 上面会抛出异常,所以不会走到这里
            } catch (Exception ex) {
                Assert.assertTrue(ex instanceof RuntimeException);
                Assert.assertEquals("异常", ex.getMessage());
         * thenThrow 中可以指定多个异常。在调用时异常依次出现。若调用次数超过异常的数量,再次调用时抛出最后一个异常。
        @Test
        public void throwTest2() {
            Random mockRandom = mock(Random.class);
            when(mockRandom.nextInt()).thenThrow(new RuntimeException("异常1"), new RuntimeException("异常2"));
            try {
                mockRandom.nextInt();
                Assert.fail();
            } catch (Exception ex) {
                Assert.assertTrue(ex instanceof RuntimeException);
                Assert.assertEquals("异常1", ex.getMessage());
            try {
                mockRandom.nextInt();
                Assert.fail();
            } catch (Exception ex) {
                Assert.assertTrue(ex instanceof RuntimeException);
                Assert.assertEquals("异常2", ex.getMessage());
    

    对应返回类型是 void 的函数,thenThrow 是无效的,要使用 doThrow。

    package tech.pdai.mockito;
    import org.junit.Assert;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.mockito.Mock;
    import org.mockito.MockitoAnnotations;
    import org.mockito.junit.MockitoJUnitRunner;
    import static org.mockito.Mockito.doThrow;
     * Do Throw for void return.
    @RunWith(MockitoJUnitRunner.class)
    public class DoThrowTest {
        static class ExampleService {
            public void hello() {
                System.out.println("Hello");
        @Mock
        private ExampleService exampleService;
        @Test
        public void test() {
            // 这种写法可以达到效果
            doThrow(new RuntimeException("异常")).when(exampleService).hello();
            try {
                exampleService.hello();
                Assert.fail();
            } catch (RuntimeException ex) {
                Assert.assertEquals("异常", ex.getMessage());
    

    此外还有,可以查看官方文档

  • doAnswer(Answer)
  • doNothing()
  • doCallRealMethod()
  • 测试:spy 和 @Spy 注解

    spy 和 mock不同,不同点是:

  • spy 的参数是对象示例,mock 的参数是 class。
  • 被 spy 的对象,调用其方法时默认会走真实方法。mock 对象不会。
  • 下面是一个对比:

    import org.junit.Assert;
    import org.junit.Test;
    import static org.mockito.Mockito.*;
    class ExampleService {
        int add(int a, int b) {
            return a+b;
    public class MockitoDemo {
        // 测试 spy
        @Test
        public void test_spy() {
            ExampleService spyExampleService = spy(new ExampleService());
            // 默认会走真实方法
            Assert.assertEquals(3, spyExampleService.add(1, 2));
            // 打桩后,不会走了
            when(spyExampleService.add(1, 2)).thenReturn(10);
            Assert.assertEquals(10, spyExampleService.add(1, 2));
            // 但是参数比匹配的调用,依然走真实方法
            Assert.assertEquals(3, spyExampleService.add(2, 1));
        // 测试 mock
        @Test
        public void test_mock() {
            ExampleService mockExampleService = mock(ExampleService.class);
            // 默认返回结果是返回类型int的默认值
            Assert.assertEquals(0, mockExampleService.add(1, 2));
    

    spy 对应注解 @Spy,和 @Mock 是一样用的。

    import org.junit.Assert;
    import org.junit.Test;
    import org.mockito.MockitoAnnotations;
    import org.mockito.Spy;
    import static org.mockito.Mockito.*;
    class ExampleService {
        int add(int a, int b) {
            return a+b;
    public class MockitoDemo {
        private ExampleService spyExampleService;
        @Test
        public void test_spy() {
            MockitoAnnotations.initMocks(this);
            Assert.assertEquals(3, spyExampleService.add(1, 2));
            when(spyExampleService.add(1, 2)).thenReturn(10);
            Assert.assertEquals(10, spyExampleService.add(1, 2));
    

    对于@Spy,如果发现修饰的变量是 null,会自动调用类的无参构造函数来初始化。

    所以下面两种写法是等价的:

    // 写法1
    private ExampleService spyExampleService;
    // 写法2
    private ExampleService spyExampleService = new ExampleService();
    

    如果没有无参构造函数,必须使用写法2。例子:

    import org.junit.Assert;
    import org.junit.Test;
    import org.mockito.MockitoAnnotations;
    import org.mockito.Spy;
    class ExampleService {
        private int a;
        public ExampleService(int a) {
            this.a = a;
        int add(int b) {
            return a+b;
    public class MockitoDemo {
        private ExampleService spyExampleService = new ExampleService(1);
        @Test
        public void test_spy() {
            MockitoAnnotations.initMocks(this);
            Assert.assertEquals(3, spyExampleService.add(2));
    

    测试:测试隔离

    根据 JUnit 单测隔离 ,当 Mockito 和 JUnit 配合使用时,也会将非static变量或者非单例隔离开。

    比如使用 @Mock 修饰的 mock 对象在不同的单测中会被隔离开。

    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.mockito.Mock;
    import org.mockito.junit.MockitoJUnitRunner;
    import static org.mockito.Mockito.*;
    @RunWith(MockitoJUnitRunner.class)
    public class MockitoDemo {
        static class ExampleService {
            public int add(int a, int b) {
                return a+b;
        @Mock
        private ExampleService exampleService;
        @Test
        public void test01() {
            System.out.println("---call test01---");
            System.out.println("打桩前: " + exampleService.add(1, 2));
            when(exampleService.add(1, 2)).thenReturn(100);
            System.out.println("打桩后: " + exampleService.add(1, 2));
        @Test
        public void test02() {
            System.out.println("---call test02---");
            System.out.println("打桩前: " + exampleService.add(1, 2));
            when(exampleService.add(1, 2)).thenReturn(100);
            System.out.println("打桩后: " + exampleService.add(1, 2));
    

    将两个单测一起运行,运行结果是:

    ---call test01---
    打桩前: 0
    打桩后: 100
    ---call test02---
    打桩前: 0
    打桩后: 100
    

    test01 先被执行,打桩前调用add(1, 2)的结果是0,打桩后是 100。

    然后 test02 被执行,打桩前调用add(1, 2)的结果是0,而非 100,这证明了我们上面的说法。

    测试:结合PowerMock支持静态方法

    PowerMock 是一个增强库,用来增加 Mockito 、EasyMock 等测试库的功能。

    Mockito为什么不能mock静态方法?

    因为Mockito使用继承的方式实现mock的,用CGLIB生成mock对象代替真实的对象进行执行,为了mock实例的方法,你可以在subclass中覆盖它,而static方法是不能被子类覆盖的,所以Mockito不能mock静态方法。

    但PowerMock可以mock静态方法,因为它直接在bytecode上工作。

  • Mockito 默认是不支持静态方法
  • 比如我们在 ExampleService 类中定义静态方法 add:

    public class ExampleService {
        public static int add(int a, int b) {
            return a+b;
    

    尝试给静态方法打桩,会报错:

    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.mockito.Mock;
    import org.mockito.junit.MockitoJUnitRunner;
    import static org.mockito.Mockito.*;
    @RunWith(MockitoJUnitRunner.class)
    public class MockitoDemo {
        @Test
        public void test() {
            // 会报错
            when(ExampleService.add(1, 2)).thenReturn(100);
    
  • 可以用 Powermock 弥补 Mockito 缺失的静态方法 mock 功能
  • 在 pom.xml 中配置以下依赖:(版本的匹配问题可以参考:github.com/powermock/p…)

    <properties>
      <powermock.version>2.0.2</powermock.version>
    </properties>
    <dependencies>
      <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-module-junit4</artifactId>
        <version>${powermock.version}</version>
        <scope>test</scope>
      </dependency>
      <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-api-mockito2</artifactId>
        <version>${powermock.version}</version>
        <scope>test</scope>
      </dependency>
    </dependencies>
    
    import org.junit.Assert;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.powermock.api.mockito.PowerMockito;
    import org.powermock.core.classloader.annotations.PrepareForTest;
    import org.powermock.modules.junit4.PowerMockRunner;
    import static org.mockito.Mockito.*;
    @RunWith(PowerMockRunner.class)     // 这是必须的
    @PrepareForTest(ExampleService.class)  // 声明要处理 ExampleService
    public class MockitoDemo {
        @Test
        public void test() {
            PowerMockito.mockStatic(ExampleService.class);  // 这也是必须的
            when(ExampleService.add(1, 2)).thenReturn(100);
            Assert.assertEquals(100, ExampleService.add(1, 2));
            Assert.assertEquals(0, ExampleService.add(2, 2));
    
  • PowerMockRunner 支持 Mockito 的 @Mock 等注解
  • 上面我们用了 PowerMockRunner ,MockitoJUnitRunner 就不能用了。但不要担心, @Mock 等注解还能用。

    import org.junit.Assert;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.mockito.Mock;
    import org.powermock.modules.junit4.PowerMockRunner;
    import java.util.Random;
    import static org.mockito.Mockito.*;
    @RunWith(PowerMockRunner.class)
    public class MockitoDemo {
        @Mock
        private Random random;
        @Test
        public void test() {
            when(random.nextInt()).thenReturn(1);
            Assert.assertEquals(1,  random.nextInt());
    

    单元测试 - IDEA下单元测试详解

    工欲善其事必先利其器,我们在写单元测试一定要使用工具,这将能大幅度提升编码的效率。本文以IDEA为例,看看如何利用插件提升效率。

    准备一个待测试的类, 其中还包含着错误。

    package tech.pdai.junit4.module;
    public class Calculator {
        public int result = 0;
         * add.
         * @param operand1 first param
         * @param operand2 second param
         * @return sum
        public int add(int operand1, int operand2) {
            result = operand1 + operand2;
            return result;
        public int subtract(int operand1, int operand2) {
            result = operand1 - operand2;
            return result;
        public int multiple(int operand1, int operand2) {
            result = operand1 * operand2;
            for (; ; ) {                    //死循环
        public int divide(int operand1, int operand2) {
            result = operand1 / 0;
            return result;
        public int getResult() {
            return this.result;
    

    自动生成单元测试

    第一个插件,首推的是JunitGeneratorV2.0

    在大量的单元测试时,如何提升测试的效率呢?肯定是并行,所以你可以用如下的插件

    JUnit4 Parallel Runner

    代码覆盖率

    如何快速看本地代码测试覆盖率呢?

    代码覆盖率

    单元测试 - SpringBoot2+H2+Mockito实战

    在真实的开发中,我们通常是使用SpringBoot的,目前SpringBoot是v2.4.x的版本(SpringBoot 2.2.2.RELEASE之前默认是使用 JUnit4,之后版本默认使用Junit5);所以我们写个基于SpringBoot2.4+H2的内存库的简单例子,同时加点必要的单元测试。

    为何H2会被用来做单元测试

    一个 Junit单元测试的流程包括

  • 初始化数据
  • 在真实的测试代码开发中,有几类问题会造成困扰:

  • 数据库环境的搭建
  • 搭建一套完整的数据库往往比较耗时,然而一旦将数据库配置加入测试范围,就必须长期维护其稳定性;
  • 这同时也会带来代码库同步的困扰。
  • 保证数据库的"干净"
  • 大多数情况下,每个测试用例在启动前(初始化数据)都期望数据库是"干净"的状态;然而使用真实的数据库却很难保证这点,原因是:

  • 多个应用可能会共享一个物理数据库;
  • 测试用例在销毁数据时很难保证完全清除,可能一次意外的调试也会产生垃圾数据;
  • H2内存数据库很好的解决了上述问题,本身作为嵌入式数据库并不需要额外的看护成本;在程序退出时,所有数据都能保证完全清除

    SpringBoot对单测试的差异

    SpringBoot 2.2.2.RELEASE之前默认是使用 JUnit4,之后版本默认使用Junit5。

    Springboot+junit4:

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class SpringBootQuickStartApplicationTests {
        private MockMvc mvc;
        @Before
        public void setUp() throws Exception {
            mvc = MockMvcBuilders.standaloneSetup(new UserController()).build();
        @Test
        public void contextLoads() throws Exception {
            RequestBuilder request = null;
            request = MockMvcRequestBuilders.get("/")
                    .contentType(MediaType.APPLICATION_JSON);
            mvc.perform(request)
                    .andExpect(MockMvcResultMatchers.status().isOk())
                    .andDo(MockMvcResultHandlers.print())
                    .andReturn();
    

    Springboot+junit5:

    @SpringBootTest
    // 使用spring的测试框架
    @ExtendWith(SpringExtension.class)
    class SpringbootQuickStartApplicationTests {
        private MockMvc mockMvc;
        @BeforeEach // 类似于junit4的@Before
        public void setUp() throws Exception {
            mockMvc = MockMvcBuilders.standaloneSetup(new UserController()).build();
        @Test
        void contextLoads() throws Exception {
            RequestBuilder request = null;
            request = MockMvcRequestBuilders.get("/")
                    .contentType(MediaType.APPLICATION_JSON);
            mockMvc.perform(request)
                    .andExpect(MockMvcResultMatchers.status().isOk())
                    .andDo(MockMvcResultHandlers.print())
                    .andReturn();
    

    Spring Boot 2.4.2 + H2 + Lombok + Spring Boot Test (默认包含了 Junit5 + Mockito)。

    Demo程序准备

  • pom.xml
  • <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
      <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.2</version>
        <relativePath/> <!-- lookup parent from repository -->
      </parent>
      <groupId>tech.pdai</groupId>
      <artifactId>java-springboot-unit5</artifactId>
      <version>0.0.1-SNAPSHOT</version>
      <name>java-springboot-unit5</name>
      <description>java-springboot-unit5</description>
      <properties>
        <java.version>1.8</java.version>
      </properties>
      <dependencies>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
          <groupId>com.h2database</groupId>
          <artifactId>h2</artifactId>
          <scope>runtime</scope>
        </dependency>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-configuration-processor</artifactId>
          <optional>true</optional>
        </dependency>
        <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
          <optional>true</optional>
        </dependency>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-test</artifactId>
          <scope>test</scope>
        </dependency>
      </dependencies>
      <build>
        <plugins>
          <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
              <excludes>
                <exclude>
                  <groupId>org.projectlombok</groupId>
                  <artifactId>lombok</artifactId>
                </exclude>
              </excludes>
            </configuration>
          </plugin>
        </plugins>
      </build>
    </project>
    
  • application.yml
  • spring:
      datasource:
        platform: h2
        driverClassName: org.h2.Driver
        url: jdbc:h2:mem:testdb;MODE=MYSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false
        username: pdai
        password: pdai
        schema: classpath:db/schema/user-schema.sql
        data: classpath:db/data/user-data.sql
        initialization-mode: always
        console:
          settings:
            trace: true
            web-allow-others: true
          enabled: true
          path: /h2-console
        show-sql: true
        hibernate:
          ddl-auto: update
        generate-ddl: false
        open-in-view: false
    
  • 数据库文件准备
  • schema

    DROP TABLE IF EXISTS user;
    CREATE TABLE user (
        id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
        name varchar(35),
        phone varchar(35)
    
    insert into user(id,name,phone) values(1,'pdai','123456');
    insert into user(id,name,phone) values(2,'zhangsan','123456');
    
  • entity
  • package tech.pdai.springboot2unit5.entity;
    import lombok.AllArgsConstructor;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    import lombok.Setter;
    import javax.persistence.*;
    * User.
    @Entity
    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    @Table(name = "user")
    public class User {
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Integer id;
        @Column
        private String name;
        @Column
        private String phone;
    
    package tech.pdai.springboot2unit5.dao;
    import org.springframework.data.jpa.repository.JpaRepository;
    import org.springframework.stereotype.Repository;
    import tech.pdai.springboot2unit5.entity.User;
     * user dao.
    @Repository
    public interface UserRepository extends JpaRepository<User, Integer> {
    
  • service
  • package tech.pdai.springboot2unit5.service;
    import tech.pdai.springboot2unit5.entity.User;
    import java.util.List;
     * user service.
    public interface IUserService {
         * find all user.
         * @return list
        List<User> findAll();
    
    package tech.pdai.springboot2unit5.service.impl;
    import org.springframework.stereotype.Service;
    import tech.pdai.springboot2unit5.dao.UserRepository;
    import tech.pdai.springboot2unit5.entity.User;
    import tech.pdai.springboot2unit5.service.IUserService;
    import java.util.List;
     * User service impl.
    @Service
    public class UserServiceImpl implements IUserService {
         * user dao.
        private final UserRepository userRepository;
         * init.
         * @param userRepository2 user dao
        public UserServiceImpl(final UserRepository userRepository2) {
            this.userRepository = userRepository2;
         * find all user.
         * @return list
        @Override
        public List<User> findAll() {
            return userRepository.findAll();
    
  • Controller
  • package tech.pdai.springboot2unit5.controller;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    import tech.pdai.springboot2unit5.entity.User;
    import tech.pdai.springboot2unit5.service.IUserService;
    import java.util.List;
     * User controller.
    @RestController
    public class UserController {
         * user service.
        private final IUserService userService;
         * init.
         * @param userService2 user service
        public UserController(final IUserService userService2) {
            this.userService = userService2;
         * find user list.
         * @return list
        @GetMapping("user/list")
        public ResponseEntity<List<User>> list() {
            return ResponseEntity.ok(userService.findAll());
    

    在实际的项目中可以使用profile来区分测试ut,使用test profile(包含H2内存库),真实环境使用MySQL或其它。

    package tech.pdai.springboot2unit5;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.api.extension.ExtendWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.context.annotation.Profile;
    import org.springframework.test.context.junit.jupiter.SpringExtension;
    import tech.pdai.springboot2unit5.service.IUserService;
    import static org.junit.jupiter.api.Assertions.assertEquals;
    import static org.junit.jupiter.api.Assertions.assertFalse;
     * A way to test from H2.
     * Just a demo, and change profile to 'test' for H2, and 'product' for MySQL.
    @Profile("default")
    @ExtendWith(SpringExtension.class)
    @SpringBootTest
    class JavaSpringbootUnit5ApplicationTests {
        @Autowired
        IUserService userService;
        @Test
        @DisplayName("Integration test")
        void contextLoads() {
            assertFalse(userService.findAll().isEmpty());
            assertEquals("pdai", userService.findAll().get(0).getName());
    
  • controller
  • mockMvc

    package tech.pdai.springboot2unit5.controller;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.api.extension.ExtendWith;
    import org.mockito.Mockito;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
    import org.springframework.boot.test.mock.mockito.MockBean;
    import org.springframework.http.MediaType;
    import org.springframework.test.context.junit.jupiter.SpringExtension;
    import org.springframework.test.web.servlet.MockMvc;
    import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
    import tech.pdai.springboot2unit5.entity.User;
    import tech.pdai.springboot2unit5.service.IUserService;
    import java.util.Collections;
    import static org.mockito.Mockito.times;
    import static org.mockito.Mockito.verify;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
     * user controller test - use mockito.
    @ExtendWith(SpringExtension.class)
    @WebMvcTest(value = UserController.class)
    class UserControllerTest {
        @Autowired
        private MockMvc mockMvc;
        @MockBean
        private IUserService userService;
         * test find all user.
         * @throws Exception exception
        @Test
        @DisplayName("Test findAll()")
        public void list() throws Exception {
            Mockito.when(userService.findAll()).thenReturn(
                    Collections.singletonList(new User(1, "pdai.tech", "1221111")));
            mockMvc.perform(MockMvcRequestBuilders.get("/user/list")
                    .accept(MediaType.APPLICATION_JSON))
                    .andExpect(status().isOk());
            verify(userService, times(1)).findAll();
    
  • service
  • package tech.pdai.springboot2unit5.service.impl;
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.api.extension.ExtendWith;
    import org.mockito.Mock;
    import org.mockito.Mockito;
    import org.springframework.test.context.junit.jupiter.SpringExtension;
    import tech.pdai.springboot2unit5.entity.User;
    import java.util.Collections;
    import java.util.List;
    import static org.junit.jupiter.api.Assertions.assertFalse;
    import static org.mockito.Mockito.times;
    import static org.mockito.Mockito.verify;
    @ExtendWith(SpringExtension.class)
    class UserServiceImplTest {
        @Mock
        private UserServiceImpl userService;
        @Test
        public void findAll() {
            //Given
            Mockito.when(userService.findAll()).thenReturn(
                    Collections.singletonList(new User(1, "pdai.tech", "1221111")));
            //When
            List<User> userDtoList = userService.findAll();
            //Then
            assertFalse(userDtoList.isEmpty());
            verify(userService, times(1)).findAll();
    

    groovy+spock

    爱上单元测试:基于Groovy的Spock框架_tom3mao的博客-CSDN博客_groovy spock

    分类:
    后端
    标签: