JUnit5 + JMockit 知识整理

参考资料:
https://sjyuan.cc/junit5/user-guide-cn/
http://jmockit.cn/index.htm

1 JUnit5

1.1 基本概念

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

依赖关系图:在这里插入图片描述

TaskEngine:
maven-surefire-plugin:

  • JUnit Platform
    • junit-platform-commons
    • junit-platform-console
    • junit-platform-console-standalone
    • junit-platform-engine
    • junit-platform-launcher
  • JUnit Jupiter:
    • junit-jupiter-api
    • junit-jupiter-engine:默认的测试引擎
    • junit-jupiter-params:支持JUnit Jupiter中的 参数化测试。
  • Junit Vintage
    • junit-vintage-engine

简而言之,JUnit Platform 提供了 JUnit5 框架相关的组件,包括框架启动,还有 TaskEngine 服务接口,谁会用到它呢?引擎开发人员以及构建工具和IDE提供商的开发人员!比如 JMockit 为了集成 JUnit5 开发了 JMockitTestEngine,Maven 测试相关插件 maven-surefire-plugin, 2.22.0 及以上版本原生支持 JUnit 5。

JUnit Jupiter 是面向测试开发人员和扩展开发人员,。

JUnit Vintage 提供了一个TestEngine,用于运行基于JUnit 3和JUnit 4的测试。

  • Test Method: 添加 @Test, @RepeatedTest, @ParameterizedTest, @TestFactory, or @TestTemplate 注解的方法;
  • Test Class:包含 Test Method 的 Class;
  • @DisplayName: 定义 Test method 或 Test Class 的显示名称,运行或报告中使用;

Record-Replay-Verification
Arrange-Action-Assert

1.2 Annotations

@Test, @ParameterizedTest, @RepeatedTest, @TestFactory, @TestInstance, @TestTemplate, @DisplayName, @BeforeEach, @AfterEach, @BeforeAll, @AfterAll, @Nested, @Tag, @Disabled, @ExtendWith

@DisplayName("A special test case")
class StandardTests {

    @BeforeAll
    static void initAll() {
    }

    @BeforeEach
    void init() {
    }

    @Test
    @Tag("model")
    @DisplayName("first case")
    @EnabledOnJre(JAVA_8)
    @EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
    void succeedingTest() {
    }

    @Test
    void failingTest() {
        fail("a failing test");
    }

    @Test
    @Disabled("for demonstration purposes")
    void skippedTest() {
        // not executed
    }

    @RepeatedTest(value = 1, name = "{displayName} {currentRepetition}/{totalRepetitions}")
    @DisplayName("Repeat!")
    void customDisplayName(TestInfo testInfo) {
        assertEquals(testInfo.getDisplayName(), "Repeat! 1/1");
    }

    @ParameterizedTest
    @ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
    void palindromes(String candidate) {
        assertTrue(isPalindrome(candidate));
    }

    @TestFactory
    Iterable<DynamicTest> dynamicTestsFromIterable() {
        return Arrays.asList(
            dynamicTest("3rd dynamic test", () -> assertTrue(true)),
            dynamicTest("4th dynamic test", () -> assertEquals(4, 2 * 2))
        );
    }

    @Nested
    class NestedTest{
        @Test
        void isEmpty() {
            assertTrue(true);
        }
    }

    @AfterEach
    void tearDown() {
    }

    @AfterAll
    static void tearDownAll() {
    }
}

  • @BeforeAll, @AfterAll 作用在静态方法上;
  • @Disabled: 禁止执行;
  • @EnabledOnJre, @EnabledOnOs, @EnabledIfSystemProperty, @EnabledIfEnvironmentVariable, @EnabledIf: 配置条件执行属性;
  • @Tag: 测试类和测试方法可以被@Tag注解标记。那些标记可以在后面被用来过滤测试发现和执行;
  • @Nested: 嵌套测试让测试编写者能够表示出几组测试用例之间的关系;
  • TestInfo, RepetitionInfo, TestReporter: 允许给测试类的构造函数和方法传入参数;
  • @RepeatedTest: 注解并指定重复运行一个测试方法的次数;
  • @ParameterizedTest: 参数化测试可以用不同的参数多次运行试; 使用 @ValueSource, @CsvSource, @CsvFileSource, @EnumSource, @MethodSource, @ArgumentsSource 来指定数据源;
  • @TestFactory: 方法本身不是测试用例,而是测试用例的工厂; DynamicTest是运行时生成的测试用例。它由一个显示名称 和Executable组成;

1.3 Maven

<build>
    <plugins>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.22.0</version>
            <configuration>
                <includes>
                    <include>Sample.java</include>
                    <include>%regex[.*(Cat|Dog).*Test.*]</include>
                </includes>
                <excludes>
                    <exclude>**/TestCircle.java</exclude>
                    <exclude>**/TestSquare.java</exclude>
                </excludes>
                <groups>acceptance | !feature-a</groups>
                <excludedGroups>integration, regression</excludedGroups>
                <properties>
                    <configurationParameters>
                        junit.jupiter.conditions.deactivate = *
                        junit.jupiter.extensions.autodetection.enabled = true
                        junit.jupiter.testinstance.lifecycle.default = per_class
                    </configurationParameters>
                </properties>
            </configuration>
        </plugin>
    </plugins>
</build>


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

https://maven.apache.org/surefire/maven-surefire-plugin/examples/inclusion-exclusion.html

  • 按Tag过滤
  • 按测试类名过滤
  • 配置参数

2 JMockit

JMockit 中文网 http://jmockit.cn/index.htm

JMockit是一款Java类/接口/对象的Mock工具,目前广泛应用于Java应用程序的单元测试中。

2.1 基本概念

//JMockit的程序结构
public class ProgramConstructureTest {
 
    // 这是一个测试属性
    @Mocked
    HelloJMockit helloJMockit;
 
    @Test
    public void test1() {
        // 录制(Record)
        new Expectations() {
            {
                helloJMockit.sayHello();
                // 期待上述调用的返回是"hello,david",而不是返回"hello,JMockit"
                result = "hello,david";
            }
        };
        // 重放(Replay)
        String msg = helloJMockit.sayHello();
        Assert.assertTrue(msg.equals("hello,david"));
        // 验证(Verification)
        new Verifications() {
            {
                helloJMockit.sayHello();
 
                times = 1;
            }
        };
    }

    @Test
    public void testInjectable(@Injectable Locale locale) {
        new Expectations() {
            // 这是一个Expectations匿名内部类
            {
                // 这是这个内部类的初始化代码块,我们在这里写录制脚本,脚本的格式要遵循下面的约定
                //方法调用(可是类的静态方法调用,也可以是对象的非静态方法调用)
                //result赋值要紧跟在方法调用后面
                //...其它准备录制脚本的代码
                //方法调用
                //result赋值
            }
        };
        // 静态方法不mock
        Assert.assertTrue(Locale.getDefault() != null);
        // 非静态方法(返回类型为String)也不起作用了,返回了null,但仅仅限于locale这个对象
        Assert.assertTrue(locale.getCountry() == null);
        // 自已new一个,并不受影响
        Locale chinaLocale = new Locale("zh", "CN");
        Assert.assertTrue(chinaLocale.getCountry().equals("CN"));
    }

     @Test
    public void testCaputring(@Capturing IPrivilege privilegeManager) {
        // 加上了JMockit的API @Capturing,
        // JMockit会帮我们实例化这个对象,它除了具有@Mocked的特点,还能影响它的子类/实现类
        new Expectations() {
            {
                // 对IPrivilege的所有实现类录制,假设测试用户有权限
                privilegeManager.isAllow(testUserId);
                result = true;
            }
        };
        // 不管权限校验的实现类是哪个,这个测试用户都有权限
        Assert.assertTrue(privilegeManager1.isAllow(testUserId));
        Assert.assertTrue(privilegeManager2.isAllow(testUserId));
    }

    @Test
    public void testMockUp() {
        // 对Java自带类Calendar的get方法进行定制
        // 只需要把Calendar类传入MockUp类的构造函数即可
        new MockUp<Calendar>(Calendar.class) {
            // 想Mock哪个方法,就给哪个方法加上@Mock, 没有@Mock的方法,不受影响
            @Mock
            public int get(int unit) {
                if (unit == Calendar.YEAR) {
                    return 2017;
                }
                if (unit == Calendar.MONDAY) {
                    return 12;
                }
                if (unit == Calendar.DAY_OF_MONTH) {
                    return 25;
                }
                if (unit == Calendar.HOUR_OF_DAY) {
                    return 7;
                }
                return 0;
            }
        };
        // 从此Calendar的get方法,就沿用你定制过的逻辑,而不是它原先的逻辑。
        Calendar cal = Calendar.getInstance(Locale.FRANCE);
        Assert.assertTrue(cal.get(Calendar.YEAR) == 2017);
        Assert.assertTrue(cal.get(Calendar.MONDAY) == 12);
        Assert.assertTrue(cal.get(Calendar.DAY_OF_MONTH) == 25);
        Assert.assertTrue(cal.get(Calendar.HOUR_OF_DAY) == 7);
        // Calendar的其它方法,不受影响
        Assert.assertTrue((cal.getFirstDayOfWeek() == Calendar.MONDAY));
 
    }

    @Test
    public void testClassMockingByExpectation() {
        AnOrdinaryClass instanceToRecord = new AnOrdinaryClass();
        new Expectations(AnOrdinaryClass.class) {
            {
                // mock静态方法
                AnOrdinaryClass.staticMethod();
                result = 10;
                // mock普通方法
                instanceToRecord.ordinaryMethod();
                result = 20;
                // mock final方法
                instanceToRecord.finalMethod();
                result = 30;
                // native, private方法无法用Expectations来Mock
            }
        };
        AnOrdinaryClass instance = new AnOrdinaryClass();
        Assert.assertTrue(AnOrdinaryClass.staticMethod() == 10);
        Assert.assertTrue(instance.ordinaryMethod() == 20);
        Assert.assertTrue(instance.finalMethod() == 30);
        // 用Expectations无法mock native方法
        Assert.assertTrue(instance.navtiveMethod() == 4);
        // 用Expectations无法mock private方法
        Assert.assertTrue(instance.callPrivateMethod() == 5);
    }
}

JMockit的程序结构:

  • 测试属性&测试参数:测试属性即测试类的一个属性。它作用于测试类的所有测试方法;测试参数即测试方法的参数。二者皆可以用 @Mocked, @Tested, @Injectable,@Capturing 来标注。测试参数与测试属性的不同,主要是作用域的不同。
  • Record-Replay-Verification: 与JUnit程序的AAA(Arrange-Action-Assert)结构是一样的。Record对应Arrange,先准备一些测试数据,测试依赖。Replay对应Action,即执行测试逻辑。Verification对应Assert,即做测试验证。
  • @Mocked: @Mocked修饰的类/接口,是让 JMockit 生成一个 Mocked 对象,这个对象方法(包含静态方法)返回默认值。如果返回类型是其它引用类型,则返回这个引用类型的Mocked对象。
  • @Tested, @Injectable: @Tested修饰的类,表示是测试对象; @Injectable 也表示一个Mocked对象,相比@Mocked,只不过只影响类的一个实例。
  • @Capturing: 主要用于子类/实现类的Mock;
  • Expectations: 主要是用于录制, 即录制类/对象的调用,返回值是什么。
  • @Mock: 直接 mock 对象的方法;
  • 用Expectations来Mock类与用Expectations来Mock实例的唯一不同就在于,前者影响类的所有实例,而后者只影响某一个实例。

2.2 JMockit 架构

架构图: 在这里插入图片描述

通过上面的架构图,我们可以看到JMockit有如下核心组件

  1. JVM Attach

JMockit使用了JDK6动态添加代理功能。目的是为了运行JMockit启动程序做准备。 JMockit提供了不同OS的hotSpot JVM的Attach支持: BsdVirtualMachine, LinuxVirtualMachine,SolarisVirtualMachine,WindowsVirtualMachine。

JMockit启动程序:主要功能是集成测试框架(JUnit/TestNG),完成对JMockit类转换器织入。

  1. 测试框架集成

提供了JUnit4/5, TestNG的支持。

a) 对JUnit4的集成方法:改写JUnit4的核心类org.junit.runner.Runner,org.junit.runners.model.FrameworkMethod, org.junit.runners.model.TestRunnerDecorator,org.junit.runners.model.RunNotifier。改写的目的是为了让测试程序在运行测试方法前,完成Mock 注解API(@Mocked,@Injectable,@Capturing)修饰的测试属性&测试参数的类做相关字节码的织入。
详见可以见JMockit源代码中Runner类,FakeFrameworkMethod类,JUnit4TestRunnerDecorator类,RunNotifierDecorator类。

b) 对JUnit5/TestNG的集成方法: 由于JUnit5/TestNG支持ServiceLoader的扩展体系,JMockit通过配置/META-INF/services/org.junit.platform.engine.TestEngine,/META-INF/services/org.testng.ITestNGListener完成对JUnit5/TestNG的集成。集成的目的同样是为了让测试程序在运行测试方法前,完成Mock 注解API(@Mocked,@Injectable,@Capturing)修饰的测试属性&测试参数的类做相关字节码的织入。

  1. 字节码处理

通过ASM,在类的某个方法中加入某段逻辑以达到Mock的目的;生成某个类的子类以支持抽象类的Mock;生成某个接口的实例类以支持接口的Mock。通过ASM, 这些都变得不那么复杂了。

  1. 类转换器

类转换器是JMockit的核心。Mock的核心就是JMockit不同的类转换器在起作用。

a)录制(ExpectationsTransformer):用于对new Expectations(){{}},new Verifications(){{}},匿名类进行重定义。用于支持测试程序中的录制,重放,校验。
b)伪类(ClassLoadingBridgeFields): 伪类,即new MockUp {}的匿名类或 extends MockUp的子类。用于伪类的@Mock方法提供支持。 通过识别伪类@Mock方法,在对应的方法体中织入一段分支,用于走伪类的@Mock方法逻辑。
c)覆盖率(CodeCoverage):用于支持JMockit Coverage功能。 通过在类的方法体行加埋点。即可以完成行覆盖率,路径覆盖率的计算。
d)类缓存(CachedClassfiles): 这个没有什么好说的,对类进行了重定义,当然要求一个测试方法结束后,能复原类的原有字节码,于是需要一个Cache了。
e)对象捕捉(CaptureTransformer): 用于支持JMockit的withCapture()功能,即捕捉某次测试中,某个类的某个方法的入参是什么,并记录下来。通常用于在验证代码块中,某个方法的入参是否符合期望。

  1. Mock API

@Mocked, @Tested ,@Injectable, @Capturing, MockUp, @Mock ,Expectations, Verifications这些API,通过前面基础知识,常见用法等的学习,这些API已经耳熟能详了吧。 基本能满足大部分的Mock场景了。

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值