参考资料:
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有如下核心组件
- JVM Attach
JMockit使用了JDK6动态添加代理功能。目的是为了运行JMockit启动程序做准备。 JMockit提供了不同OS的hotSpot JVM的Attach支持: BsdVirtualMachine, LinuxVirtualMachine,SolarisVirtualMachine,WindowsVirtualMachine。
JMockit启动程序:主要功能是集成测试框架(JUnit/TestNG),完成对JMockit类转换器织入。
- 测试框架集成
提供了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)修饰的测试属性&测试参数的类做相关字节码的织入。
- 字节码处理
通过ASM,在类的某个方法中加入某段逻辑以达到Mock的目的;生成某个类的子类以支持抽象类的Mock;生成某个接口的实例类以支持接口的Mock。通过ASM, 这些都变得不那么复杂了。
- 类转换器
类转换器是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()功能,即捕捉某次测试中,某个类的某个方法的入参是什么,并记录下来。通常用于在验证代码块中,某个方法的入参是否符合期望。
- Mock API
@Mocked, @Tested ,@Injectable, @Capturing, MockUp, @Mock ,Expectations, Verifications这些API,通过前面基础知识,常见用法等的学习,这些API已经耳熟能详了吧。 基本能满足大部分的Mock场景了。