概述
- 编写的测试代码就是 Java 代码
- 运行要遵守 Java 的语法和 JVM 的规定
测试类型
- 手工测试
- 单元测试 => 快速检查一个类的功能
- 集成测试 => 检查整个系统的功能
- 回归测试 => 检查新的代码有没有破坏旧的功能
- 冒烟测试 => 快速检查代码有没有大问题
- 黑盒测试 => 将整个系统看成一个黑盒进行测试
- 白盒测试 => 根据系统的内部细节设计专用的测试
- 压力测试 => 对系统施加一定的压力,确保系统的稳定性
- 用户接受测试 => 用户/甲方是否接受
测试形态
- TestNG
- JUnit
- JUnit 4
- JUnit 5
测试的基本形态
- 在依赖中引入测试框架
- Maven => 放在src/test下的都是测试代码,在 test 阶段会被自动执行
- Gradle => 当使用 java 插件的时候,自动创建名为 test 的 Test 任务
surefire plugin
- 指定哪些测试会被执行
- 忽略失败、自动重新运行
- 控制 JVM 的 fork
- 控制 JVM 的参数、System Properties
- 测试报告生成 => target/surefire-reports
- 问题调试
jacoco
- maven jacoco doc
-
maven jacoco config example +
<phase>test</phase>
- Java agent => maven test fork 的 JVM 中添加一个
-javaagent:<xxx>
参数,可以为 JVM 提供非常强大的增强的功能<plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.1</version> <configuration> <!-- jacoco 会添加 -javaagent:<xxx> 参数,此时 -Dfile.encoding=UTF-8 覆盖了 jacoco 的参数 --> <!-- <argLine>-Dfile.encoding=UTF-8 ${argLine}</argLine> --> <argLine>-Dfile.encoding=UTF-8</argLine> </configuration> </plugin> <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.5</version> <executions> <execution> <id>default-prepare-agent</id> <goals> <!-- 准备 java agent --> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>default-report</id> <goals> <!-- 生成测试报告 --> <goal>report</goal> </goals> <phase>test</phase> </execution> <execution> <id>default-check</id> <goals> <!-- 当测试率不满足预先设定的阈值时失败 --> <goal>check</goal> </goals> <configuration> <rules> <rule> <element>BUNDLE</element> <limits> <limit> <counter>COMPLEXITY</counter> <value>COVEREDRATIO</value> <minimum>0.60</minimum> </limit> </limits> </rule> </rules> </configuration> </execution> </executions> </plugin>
- Java instrumentation => 字节码截取。可以监控字节码每个方法的进入和退出,每个分支的进入和退出中,从而得知每个代码运行覆盖的情况,从而生成测试覆盖率报告
- jacoco 会拦截所有方法和分支的调用和执行生成 jacoco execution data file
测试的生命周期
对于每一个测试用例都会创建全新的测试类实例
- JUnit4 => @Test/@Before/@BeforeClass/@Ignore
- JUnit5 => @Test/@BeforeEach/@BeforeAll/@Disable
测试类的生命周期
- 调用 @BeforeClass/@BeforeAll
@BeforeAll // @BeforeAll 必须放置在静态方法上 public static void setUpAll() {}
- 创建测试类对象实例
- @Before/@BeforeEach
@BeforeEach public void setUp() {}
- @Test
- @After/@AfterEach
@AfterEach public void cleanUp() {} @AfterEach public void tearUp() {} @AfterEach public void tearDown() {}
- 调用 @AfterClass/@AfterAll
断言 & 假设
- Assertions => 断言
- Assume/Assumptions => 假设 => 根据动态的条件关闭测试 => 原理:抛出 TestAbortedException 异常,测试框架捕获到这个异常之后将测试状态标记为忽略
@Test public void test() { // System.setProperties("os.name", "Window"); // 如果是 Mac 会正常运行 // 如果不是会跳过 try { Assumptions.assumeTrue(System.getProperties("os.name").containes("Mac")); } catch (e) { throw e; } }
单元测试
- 从一个类的角度,编写方法测试其功能
- Mock 所有他依赖的对象 => Mockito
Mockito
- mock() => Creates mock object of given class or interface. => Mock Xxx 类:
Mockito.mock(Xxx.class)
- spy() =>
- when().thenReturn()/thenAnswer()/thenThrow()
- verify() => Verifies certain behavior Happened once.
- ArgumentCaptor => Use it to capture argument values for further assertions.
-
Mockito Extension
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-junit-jupiter</artifactId> <version>2.17.0</version> <scope>test</scope> </dependency> // test @ExtendWith(MockitoExtension.class) class XxxTest { // @Mock + @InjectMocks + @Captor }
Mock 实现
- 动态代理 => 使用动态代理的方式创建了 Mock 类的一个子类,当 when(mockClass.doSomething()).thenReturn() 时并不会调用真正的类
- 字节码增强
集成测试
- 最难的部分在于启动集成测试环境 => 数据库/Redis...其他服务
- Maven Failsafe Plugin
- Maven Lifecycle => pre-integration-test/integration-test/post-integration-test
Mock 数据库
- 数据库的初始化 => 使用 Flyway 或者数据库 dump 作为数据库表结构来源
- configuration
H2 数据库
内存数据库
public class IntegrationTest {
@BeforeEach
public void setUp() {
// 初始化内存数据库,以备测试
ClassicConfiguration configuration = new ClassicConfiguration();
configuration.setDataSource(
"jdbc:h2:mem:test",
"test",
"test"
);
Flyway flyway = new Flyway(configuration);
flyway.clean();
flyway.migrate();
// 注册一个测试用户,以备测试
registerUser("test", "test");
}
}
Docker 启动数据库
- exec maven plugin
- 在 pre-integration-test 阶段使用 exec 插件启动容器
- 在 pre-integration-test 阶段使用 Flyway 插件进行数据库迁移以及初始化
- 在 post-integration-test 阶段使用 exec 插件销毁容器
Mock Redis
- 使用 Redis 的 Mock 第三方库
- 使用 Redis Docker 容器
Mock Bean
Mock 第三方服务 => @MockBean => 将某些 Bean Mock
黑盒测试
- 在测试 JVM 中启动 Spring 容器
- 向容器发送 HTTP 请求进行测试
白盒测试
- 在测试 JVM 中启动 Spring 容器
- 其中的部分 Bean 进行 Mock 替换
- 向集成环境(待测容器)中注入 Mock 的 Bean 实例
- 写 Java 代码进行测试
JUnit 4
JUnit4 扩展性太差
- 每个类只能有一个 Runner
- 每个类只能有一个 Rule => 多个 Rule 使用 RuleChain,将多个 Rule 组合在一起
@RunWith 与自定义 Runner
- 自定义整个测试框架的行为
- JUnit4 默认 Runner => @RunWith(BlockJUnit4ClassRunner.class) => JUnit4 只能有一个 Runner
-
自带 Runner
- Suite => 将多个测试类捆绑在一起
- Parameterized => 参数化测试
- Categories => 分类测试
-
JUnit4 Custom Runner Third Party Runners
- SpringRunner => 能够启动 Spring 容器
- MockitoJUnitRunner => 能够识别 @Mock 的 Runner
@Rule
- 拦截测试的调用,完成自定义的行为
- ExternalResource => ExternalResource is a base class for Rules (like TemporaryFolder) that set up an external resource before a test (a file, socket, server, database connection, etc.), and guarantee to tear it down afterward
- TemporaryFolder => The TemporaryFolder Rule allows creation of files and folders that are deleted when the test method finishes (whether it passes or fails)
// 不推荐这么操作,每个测试太重了
public class JUnit4Test {
@Rule
public PrepareTestDockerMySqlRule prepareTestDockerMySqlRule = new PrepareTestDockerMySqlRule();
@Test
public void test() {
}
@Test
public void test2() {
}
}
class PrepareTestDockerMySqlRule extends ExternalResource {
protected void before() throws Throwable {
System.out.println("Before!");
new ProcessBuilder("docker", "run", "-d", "--name", "test-mysql", "mysql:5.7.27")
.inheritIO().start().waitFor();
// 1. 可以使用定时器监控 docker 容器状态,docker 容器启动之后再进行下一个任务
// 2. 可以使用 docker inspect 命令检查 docker 容器状态是否可用
Thread.sleep(20 * 1000);
}
protected void after() {
try {
new ProcessBuilder("docker", "rm", "-f", "test-mysql")
.inheritIO()
.start()
.waitFor();
} catch (InterruptedException | IOException e) {
throw new RuntimeException(e);
}
System.out.println("After!");
}
}
JUnit 5
JUnit5- Parameterized Tests => Parameterized tests make it possible to run a test multiple times with different arguments.
- TestFactory => Denotes that a method is a test factory for dynamic tests. Such methods are inherited unless they are overridden.
- Display Name => Test classes and test methods can declare custom display names via @DisplayName
- 元注解 => 方便地生成自定义注解
- 有条件的测试执行
JUnit5 扩展机制
可以同时应用多个 Extension => JUnit5 Third party Extensions
- ExecutionCondition => ExecutionCondition defines the Extension API for programmatic, conditional test execution.
- TestInstanceFactory => TestInstanceFactory defines the API for Extensions that wish to create test class instances.
- TestInstancePostProcessor => TestInstancePostProcessor defines the API for Extensions that wish to post process test instances.
- ParameterResolver => ParameterResolver defines the Extension API for dynamically resolving parameters at runtime.
- TestWatcher => TestWatcher defines the API for extensions that wish to process the results of test method executions. Specifically, a TestWatcher will be invoked with contextual information for the following events.
- Test Lifecycle Callbacks
public class PrepareMemoryDatabaseExtension implements BeforeEachCallback, AfterEachCallback {
@Override
public void afterEach(ExtensionContext extensionContext) throws Exception {
System.out.println("After!");
}
@Override
public void beforeEach(ExtensionContext extensionContext) throws Exception {
// 初始化内存数据库,以备测试
System.out.println("Before!");
ClassicConfiguration configuration = new ClassicConfiguration();
configuration.setDataSource(
"jdbc:h2:mem:test",
"test",
"test"
);
Flyway flyway = new Flyway(configuration);
flyway.clean();
flyway.migrate();
}
}
@ExtendWith(PrepareMemoryDatabaseExtension.class)
class XxxIntegrationTest {
@Test
public void test() {
}
}
知识点
-
mvn clean test -Dtest=XxxTest
=> 仅仅执行 XxxTest 这个测试 - 当运行测试时 => 一个新的 JVM 会被创建 => 隔离测试的环境,避免互相影响
- ** => ant path pattern
- flaky test
- 将 suspend 设为
y
Remote JVM Debug
网友评论