03.JUnit入门

136 阅读10分钟

1.JUnit历史

有些开发人员认为自动测试是开发过程中非常重要的一部分,只有通过一系列全面的测试,才能证明组件是有效的。曾有两位开发人员认为这种类型的单元测试非常重要,甚至认为值得为其编写一个框架。1997年,Erich Gamma和Kent Beck针对Java开发了一个简单、有效的单元测试框架,将其命名为JUnit。在一次长途旅行中,他们有了做这件趣事的机会。Erich想让Kent学习Java,而他自己对Kent之前为Smalltalk编写的SUnit测试框架产生了浓厚兴趣,这次旅行给了他们做这两件事的机会

JUnit很快成了Java应用程序单元测试事实上的标准框架。如今,JUnit作为一个开源软件托管在GitHub上,拥有Eclipse公共许可证。底层测试模型xUnit正在成为所有语言的标准框架。xUnit框架可用于ASP、C++、C#、Eiffel、Delphi、Perl、PHP、Python、REBOL、Smalltalk和Visual Basic等,此处不一一列举

2.JUnit5版本的变化

介绍

与之前的JUnit版本不同,JUnit 5由来自三个不同子项目的多个不同模块组成

  • JUnit Platform: Junit Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入
  • JUnit Jupiter: JUnit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部包含了一个测试引擎,用于在Junit Platform上运行
  • JUnit Vintage: 由于JUint已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,Junit3.x的测试引擎

模块说明

JUnit Platform

JUnit Platform是JUnit 5的基础模块,它负责在JVM上启动测试框架。Platform模块定义了测试引擎的API,使得测试运行在不同的环境中保持一致性。在命令行中,你可以直接运行Platform来启动测试。Platform还提供了与其他测试运行器的集成,如Gradle和Maven

JUnit Jupiter

JUnit Jupiter JUnit 5的扩展模块,它为编写测试用例和扩展测试框架提供了新的编程模型。在JUnit 4中,你需要使用注解来定义测试方法和测试类,而在JUnit 5中,你可以使用更简洁的声明方式来定义测试用例。此外,JUnit Jupiter还提供了更多的断言方法,如assertArrayEquals和assertThrows

JUnit Vintage

JUnit Vintage是JUnit 5的另一个模块,它允许兼容JUnit 3和JUnit 4的测试用例。这意味着,如果你有一些使用旧版JUnit(3 或 4)编写的测试用例,你可以在JUnit 5的平台上运行它们。这对于那些需要逐步迁移到新版本的测试代码来说非常有用。Vintage还提供了对旧版JUnit的断言方法的支持,如assertEquals和assertTrue

版本支持

JUnit 5在运行时要求使用Java 8(或更高版本)

public与private问题

测试类名通常以Test结尾。若使用JUnit3,则需要扩展TestCase类,但JUnit4去掉了这个限制条件。另外,在JUnit4之前,测试类必须是公有的。从JUnit 5开始,顶级测试类可以是公有的,也可以是包私有(package-private)的,并且可以任意命名

命名要求

现在使用@Test注解将一个方法标记为单元测试方法。过去,通常按照testXYZ格式来命名测试方法,这是JUnit3所要求的,现在就不需要这样了。有些开发人员删除了前缀test并用描述性短语作为方法名。我们可以随意命名方法,只要使用@Test注解,JUnit就会予以运行。JUnit 5的@Test注解属于org.junit.jupiter.api这个新包,而JUnit 4的@Test注解属于org.junit包

5版本已废弃的注解

以下的注解都是在5之前的版本使用的,现在已经被废弃:

3.快速体验Junit

依赖

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.6.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.6.0</version>
            <scope>test</scope>
        </dependency>

示例代码

被测试的方法

/**
 * @author sgy
 * @description
 * @date 2024/10/8
 */
public class Calculator {
    public double add(double num1, double num2){
        return num1+num2;
    }
}

单元测试的代码

/**
 * @author sgy
 * @description
 * @date 2024/10/8
 */
public class CalculatorTest {
    @Test
    public void testAdd(){
        Calculator calculator = new Calculator();
        double result = calculator.add(10, 50);
        Assertions.assertEquals(60,result,0);
    }

}

执行正确的结果

执行错误的结果

如果我们将代码稍加修改,期望正确结果是60,但是实际上是61

    @Test
    public void testAdd(){
        Calculator calculator = new Calculator();
        double result = calculator.add(10, 50)+1;
        Assertions.assertEquals(60,result,0);
    }

就会发生以下输出

0008134337.png 18134417.png

关于assertEquals方法的最后一个参数

在大多数情况下,delta参数的值可以是0,也可以忽略。该参数用于非精确计算,包括许多浮点计算。delta用于提供一个误差范围:如果实际值在expected − delta和expected + delta之间,测试就算通过。当运行带有舍入或截断误差的数学计算时,或者在断言文件修改日期的条件时,你就会发现这一点很有用,因为日期的精度取决于操作系统

4.Junit5核心概念

asdasdw1.png

5.Junit5常用注解

常识

JUnit在调用每个@Test标注的方法之前创建测试类的一个新实例,以确保测试方法的独立性,并防止测试代码中出现意想不到的副作用。另外,测试得到的结果必须与运行顺序无关,这是一个被普遍认可的事实。因为每个测试方法都在测试类的一个新实例上运行,所以不能跨测试方法重用实例变量值。为要运行的每个测试方法创建测试类的一个实例,这是JUnit 5和之前所有版本的默认行为

如果用@TestInstance(Lifecycle.PER_CLASS)标注测试类,JUnit 5将在同一个测试类实例上运行所有测试方法。使用该注解,我们可为每个测试类创建一个新的测试实例

总览

asdascxc.png

基本示例代码

import org.junit.jupiter.api.*;

import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {

    private Calculator calculator;

    @BeforeEach
    void setUp() {
        calculator = new Calculator();
    }

    @Test
    void testAdd() {
        assertEquals(5, calculator.add(2, 3), "2 + 3 should equal 5");
    }

    @Test
    void testSubtract() {
        assertEquals(1, calculator.subtract(3, 2), "3 - 2 should equal 1");
    }

    @AfterEach
    void tearDown() {
        // 清理工作
    }
}

@DisplayName注解

@DisplayName注解可用于类和测试方法。该注解可以让为一个测试类或测试方法指定显示名称。通常,该注解用于IDE和构建工具的测试报告中。@DisplayName注解的字符串参数可以包含空格、特殊字符,甚至是表 情符号


/**
 * @author sgy
 * @description
 * @date 2024/10/8
 */
@DisplayName("calculator测试")
public class JunitStudy {
    @DisplayName("测试add方法")
    @Test
    public void testAdd(){
        Calculator calculator = new Calculator();
        double result = calculator.add(10, 50);
        Assertions.assertEquals(60,result,0);
    }

}

运行结果如下:

xxx0241008141909.png

@Disabled注解

@Disabled注解可用于测试类和方法,表示禁用测试类或测试方法不予以运行。开发人员用这个注解给出禁用一个测试的理由,以便团队的其他成员确切地知道为什么要这么做。如果该注解用在一个类上,将禁用该测试类的所有方法。此外,当开发人员在IDE中运行测试时,被禁用的测试及禁用原因在不同的控制台上显示的内容也有所不同

/**
 * @author sgy
 * @description
 * @date 2024/10/8
 */
@DisplayName("calculator测试")
public class JunitStudy {
    @DisplayName("测试add方法")
    @Test
    @Disabled("暂时不运行")
    public void testAdd(){
        Calculator calculator = new Calculator();
        double result = calculator.add(10, 50);
        Assertions.assertEquals(60,result,0);
    }

}

运行结果如下:

ddd008142114.png

@Nested

JUnit 5可以通过Java中的内部类和@Nested注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和@AfterEach注解,而且嵌套的层次没有限制

@DisplayName("A stack")
class TestingAStackDemo {

    Stack<Object> 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());
            }
        }
    }
}

@Timeout

指定测试方法的超时时间

   /**
     * 调用一个耗时1秒的方法,用Timeout设置超时时间是500毫秒,
     * 因此该用例会测试失败
     */
    @Test
    @Timeout(unit = TimeUnit.MILLISECONDS, value = 500)
    @Disabled
    void remoteRequest() {
        assertThat(helloService.remoteRequest()).isEqualTo(true);
    }

6.@Tag

介绍

JUnit5 @Tag 可用于从测试计划中过滤测试用例。 它可以帮助针对不同的环境,不同的用例或任何特定要求创建多个不同的测试计划。 您可以通过仅在测试计划中包括那些标记的测试或通过从测试计划中排除其他测试来执行测试集

使用步骤

  • 编写测试类和测试方法
  • 过滤执行

测试类和测试方法

@Slf4j
@Tag("first")
public class FirstTest {

    @Test
    @Tag("easy")
    @Tag("important")
    @DisplayName("first-1")
    void first1Test() {
        log.info("first1Test");
        assertEquals(2, Math.addExact(1, 1));
    }

    @Test
    @Tag("easy")
    @DisplayName("first-2")
    void first2Test() {
        log.info("first2Test");
        assertEquals(2, Math.addExact(1, 1));
    }

    @Test
    @Tag("hard")
    @DisplayName("first-3")
    void first3Test() {
        log.info("first3Test");
        assertEquals(2, Math.addExact(1, 1));
    }
}
@Slf4j
@Tag("second")
public class SecondTest {

    @Test
    @Tag("easy")
    @DisplayName("second-1")
    void second1Test() {
        log.info("second1Test");
        assertEquals(2, Math.addExact(1, 1));
    }

    @Test
    @Tag("easy")
    @DisplayName("second-2")
    void second2Test() {
        log.info("second2Test");
        assertEquals(2, Math.addExact(1, 1));
    }

    @Test
    @Tag("hard")
    @Tag("important")
    @DisplayName("second-3")
    void second3Test() {
        log.info("second3Test");
        assertEquals(2, Math.addExact(1, 1));
    }
}

以上就是打好了标签的测试类和测试方法了,接下来看看如何通过这些标签对测试方法进行过滤,执行单元测试有三种常用方式,咱们挨个尝试每种方式如何用标签过滤

过滤执行

方式一:在IDEA中做标签过滤

cdb36c6788724e950b8b54b66bb50adb.jpeg dc65ab65f68888a716e4e0abaa88b424.jpeg b2b557e0cd1d686e7804277f395baabb.jpeg

执行结果如下,所有打了important标签的测试方法被执行:

c8993a73d5e67936271f40e689311ecf.jpeg

方式二:aven命令时做标签过滤

mvn clean test -Dgroups="important"

执行完毕后结果如下:

3e0aa2c8cbba81b5770a0da0aa61ff7e.jpeg

翻看日志,可见只有打了important标签的测试方法被执行了,如下图红框所示:

方式三:用surefire插件时做标签过滤

surefire是个测试引擎(TestEngine)以maven插件的方式来使用,打开tag子工程的pom.xml文件,将build节点配置成以下形式,可见groups就是标签过滤节点,另外excludedGroups节点制定的hard标签的测试方法不会执行

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
                <configuration>
                    <!--要执行的标签-->
                    <groups>important</groups>
                    <!--不要执行的标签-->
                    <excludedGroups>hard</excludedGroups>
                </configuration>
            </plugin>
        </plugins>
    </build>
                    <!--如果不是SpringBoot环境可以这么引入-->
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
                <configuration>
                    <groups>aaa</groups>
                    <excludedGroups>bbb</excludedGroups>
                </configuration>
            </plugin>
        </plugins>
    </build>

在tag子工程的pom.xml所在目录,执行命令mvn clean test即可开始单元测试,结果如下,可见打了important标签的first1Test被执行,而second3Test方法尽管有important标签,但是由于其hard标签已经被设置为不执行,因此second3Test没有被执行

标签表达式

前面咱们用三种方法执行了单元测试,每次都是用important标签过滤,其实除了指定标签,JUnit还支持更复杂的标签过滤,即标签表达式

所谓标签表达式,就是用"非"、“与”、"或"这三种操作符将更多的标签连接起来,实现更复杂的过滤逻辑

上述三种操作符的定义和用法如下表:

xxx8150049.png

试试标签表达式的效果,如下图红框,修改前面创建好的IDEA配置,从之前的important改为important | hard

e32d164a8fa813fb0ab7a2a928959c9b.jpeg

再次执行这个配置,结果如下图红框所示,只有这三个方法被执行:first1Test、first3Test、second3Test,可见标签表达式生效了

2e5e55f2481e3412bc366808def632d3.jpeg

在maven命令和surefire插件中使用标签表达式的操作就不在文中执行了,请您自行验证

自定义注解

可以使用一个自定义注解包裹@Test和@Tag,这也是可以生效的,当然您单独包裹@Tag也是可以的

命名规范

标签名左右两侧的空格是无效的,执行测试的时候会做trim处理,例如下面这个标签会被当作hard来过滤

c1f2a72de4aa2b2eab805624fb0d58bb.jpeg

标签名不能有这六个符号, ( ) & | !