自动化测试

概述

  • 编写的测试代码就是 Java 代码
  • 运行要遵守 Java 的语法和 JVM 的规定

测试类型

  • 手工测试
  • 单元测试 => 快速检查一个类的功能
  • 集成测试 => 检查整个系统的功能
  • 回归测试 => 检查新的代码有没有破坏旧的功能
  • 冒烟测试 => 快速检查代码有没有大问题
  • 黑盒测试 => 将整个系统看成一个黑盒进行测试
  • 白盒测试 => 根据系统的内部细节设计专用的测试
  • 压力测试 => 对系统施加一定的压力,确保系统的稳定性
  • 用户接受测试 => 用户/甲方是否接受

测试形态

  • TestNG
  • JUnit
    1. JUnit 4
    2. 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 + test
  • Java agent => maven test fork 的 JVM 中添加一个 -javaagent: 参数,可以为 JVM 提供非常强大的增强的功能
    
        maven-surefire-plugin
        2.22.1
        
            
            
            -Dfile.encoding=UTF-8
        
    
    
        org.jacoco
        jacoco-maven-plugin
        0.8.5
        
            
                default-prepare-agent
                
                    
                    prepare-agent
                
            
            
                default-report
                
                    
                    report
                
                test
            
            
                default-check
                
                    
                    check
                
                
                    
                        
                            BUNDLE
                            
                                
                                    COMPLEXITY
                                    COVEREDRATIO
                                    0.60
                                
                            
                        
                    
                  
              
          
      
    
  • Java instrumentation => 字节码截取。可以监控字节码每个方法的进入和退出,每个分支的进入和退出中,从而得知每个代码运行覆盖的情况,从而生成测试覆盖率报告
  • jacoco 会拦截所有方法和分支的调用和执行生成 jacoco execution data file

测试的生命周期

对于每一个测试用例都会创建全新的测试类实例

  • JUnit4 => @Test/@Before/@BeforeClass/@Ignore
  • JUnit5 => @Test/@BeforeEach/@BeforeAll/@Disable

测试类的生命周期

  1. 调用 @BeforeClass/@BeforeAll
    @BeforeAll // @BeforeAll 必须放置在静态方法上
    public static void setUpAll() {}
    
  2. 创建测试类对象实例
  3. @Before/@BeforeEach
    @BeforeEach
    public void setUp() {}
    
  4. @Test
  5. @After/@AfterEach
    @AfterEach
    public void cleanUp() {}
    
    @AfterEach
    public void tearUp() {}
    
    @AfterEach
    public void tearDown() {}
    
  6. 调用 @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
    
        org.mockito
        mockito-junit-jupiter
        2.17.0
        test
    
    
    // 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

  1. 使用 Redis 的 Mock 第三方库
  2. 使用 Redis Docker 容器

Mock Bean

Mock 第三方服务 => @MockBean => 将某些 Bean Mock

黑盒测试

  1. 在测试 JVM 中启动 Spring 容器
  2. 向容器发送 HTTP 请求进行测试

白盒测试

  1. 在测试 JVM 中启动 Spring 容器
  2. 其中的部分 Bean 进行 Mock 替换
  3. 向集成环境(待测容器)中注入 Mock 的 Bean 实例
  4. 写 Java 代码进行测试

JUnit 4

JUnit4 扩展性太差

  1. 每个类只能有一个 Runner
  2. 每个类只能有一个 Rule => 多个 Rule 使用 RuleChain,将多个 Rule 组合在一起

@RunWith 与自定义 Runner

  • 自定义整个测试框架的行为
  • JUnit4 默认 Runner => @RunWith(BlockJUnit4ClassRunner.class) => JUnit4 只能有一个 Runner
  • 自带 Runner
    1. Suite => 将多个测试类捆绑在一起
    2. Parameterized => 参数化测试
    3. Categories => 分类测试
  • JUnit4 Custom Runner Third Party Runners
    1. SpringRunner => 能够启动 Spring 容器
    2. 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

  1. ExecutionCondition => ExecutionCondition defines the Extension API for programmatic, conditional test execution.
  2. TestInstanceFactory => TestInstanceFactory defines the API for Extensions that wish to create test class instances.
  3. TestInstancePostProcessor => TestInstancePostProcessor defines the API for Extensions that wish to post process test instances.
  4. ParameterResolver => ParameterResolver defines the Extension API for dynamically resolving parameters at runtime.
  5. 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.
  6. 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() {
    }
}

知识点

  1. mvn clean test -Dtest=XxxTest => 仅仅执行 XxxTest 这个测试
  2. 当运行测试时 => 一个新的 JVM 会被创建 => 隔离测试的环境,避免互相影响
  3. ** => ant path pattern
  4. flaky test
  5. 将 suspend 设为 y
    Remote JVM Debug

你可能感兴趣的:(自动化测试)