Java如何优雅地实现单元测试与集成测试

Posted 无敌码农

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java如何优雅地实现单元测试与集成测试相关的知识,希望对你有一定的参考价值。





在日常的开发过程中,为了保证代码质量,有追求的程序员一般都会对自己编写的代码进行充分的测试,这种测试不仅仅是体现在对正常功能的简单接口调用,而是要根据代码中的各种逻辑分支,进行尽可能多的覆盖性单元测试以及主要逻辑的集成测试。


上面说到的测试对于程序员来说,绝不仅仅只是依赖于Postman之类的网络工具,而要以编写独立的单元/集成测试代码的方式来实现,具体来说在Java中就是要基于JUnit、Mocktio之类的测试框架编写相应的UT及IT代码,并在这个过程中提前发现软件Bug、重新审视所写代码并进行优化。


实话说编写测试代码对提高软件质量,及自身编程水平来说都是一种非常有用的手段。但在工作中,并不是所有人都能正确地掌握单元测试和集成测试代码的写法和组织形式。以Maven工程代码为例,很多人会把单元测试和集成测试代码弄混,这样导致的后果就是大部分Maven工程代码:"mvn test"几乎很难跑通。


而本文想要表达的内容就是如何在Maven工程中有效的区分和组织单元测试、集成测试代码使得它们互不干扰,并具体演示它们的写法。



Maven测试代码结构的组织

Java如何优雅地实现单元测试与集成测试


我们知道在Maven工程结构中“src/test”目录是专门用于存放测试代码的,但令人痛苦的是Maven的标准目录结构只定义了这样一个测试目录,也就是说它本身是无法单独区分单元测试代码和集成测试代码的,这也是为什么很多人会把UT和IT代码同时写到"src/test"目录而导致“mvn test”难以跑过的原因。


那么有什么办法可以友好地解决这个问题呢?在接下来的内容中我们以Maven构建Spring Boot项目为例来具体演示下在Maven中如何友好地分离UT及IT,具体步骤如下:


1)、首先我们创建一个基于Maven构建的Spring Boot项目,代码结构如下图所示:


Java如何优雅地实现单元测试与集成测试


如上图所示,在规划的目录结构中我们将IT的代码目录及资源文件目录单独分离在“src/integration-test”目录下,默认的“src/test”目录还是作为存放UT代码的目录,而Maven在构建的过程中默认只运行UT代码。这样即便IT代码由于网络、环境等原因无法正常执行,但也不至于影响到UT代码的运行。


2)、创建区分UT、IT代码的Maven Profiles文件


默认情况下Maven是无法主动识别“src/test”目录之外的测试代码的,所以当我们将IT代码抽象到"src/integration-test"目录之后,需要通过编写Maven Profiles文件来进行区分,具体示意图如下:


Java如何优雅地实现单元测试与集成测试


如上图所示,我们可以在与“src”目录平行创建一个“profiles”的目录,其中分别用“dev”“integration-test”目录中的config.properties文件来进行区分,其中dev目录下的config.properties文件的内容为:

profile=dev

而integration-test目录中的config.properties文件则为:

profile=integration-test


3)、通过pom.xml文件配置上述profiles文件生效规则


为了使得这些profiles文件生效,我们还需要在pom.xml文件中进行相应的配置。具体如下:

<!--定义关于区分集成测试及单元测试代码的profiles-->
<profiles>
    <!-- The Configuration of the development profile -->
    <profile>
        <id>dev</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
        <properties>
            <build.profile.id>dev</build.profile.id>
            <!--Only unit tests are run when the development profile is active-->
            <skip.integration.tests>true</skip.integration.tests>
            <skip.unit.tests>false</skip.unit.tests>
        </properties>
    </profile>
    <!-- The Configuration of the integration-test profile -->
    <profile>
        <id>integration-test</id>
        <properties>
            <build.profile.id>integration-test</build.profile.id>
            <!--Only integration tests are run when the integration-test profile is active-->
            <skip.integration.tests>false</skip.integration.tests>
            <skip.unit.tests>true</skip.unit.tests>
        </properties>
    </profile>
</profiles>


上述内容先定义了区分dev及integration-test环境的的profile信息,接下来在build标签中定义资源信息及相关plugin,具体如下:

<build>
    <finalName>${project.artifactId}</finalName>
    <!--步骤1:单元测试代码、集成测试代码分离-->
    <filters>
        <filter>profiles/${build.profile.id}/config.properties</filter>
    </filters>
    <resources>
        <resource>
            <filtering>false</filtering>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.xml</include>
                <include>**/*.tld</include>
                <include>**/*.yml</include>
            </includes>
        </resource>
        <!--步骤2:通过Profile区分Maven集成测试代码、单元测试代码目录-->
        <resource>
            <filtering>true</filtering>
            <directory>src/main/resources</directory>
            <includes>
                <include>**/*.properties</include>
                <include>**/*.xml</include>
                <include>**/*.tld</include>
                <include>**/*.yml</include>
                <include>**/*.sh</include>
            </includes>
        </resource>
    </resources>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
        <!-- 步骤三:将源目录和资源目录添加到构建中 -->
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>build-helper-maven-plugin</artifactId>
            <version>3.1.0</version>
            <executions>
                <!-- Add a new source directory to our build -->
                <execution>
                    <id>add-integration-test-sources</id>
                    <phase>generate-test-sources</phase>
                    <goals>
                        <goal>add-test-source</goal>
                    </goals>
                    <configuration>
                        <!-- Configures the source directory of our integration tests -->
                        <sources>
                            <source>src/integration-test/java</source>
                        </sources>
                    </configuration>
                </execution>
                <!-- Add a new resource directory to our build -->
                <execution>
                    <id>add-integration-test-resources</id>
                    <phase>generate-test-resources</phase>
                    <goals>
                        <goal>add-test-resource</goal>
                    </goals>
                    <configuration>
                        <!-- Configures the resource directory of our integration tests -->
                        <resources>
                            <resource>
                                <filtering>true</filtering>
                                <directory>src/integration-test/resources</directory>
                                <includes>
                                    <include>**/*.properties</include>
                                </includes>
                            </resource>
                        </resources>
                    </configuration>
                </execution>
            </executions>
        </plugin>
        <!--步骤四:Runs unit tests -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.18</version>
            <configuration>
                <!-- Skips unit tests if the value of skip.unit.tests property is true -->
                <skipTests>${skip.unit.tests}</skipTests>
                <!-- Excludes integration tests when unit tests are run -->
                <excludes>
                    <exclude>**/IT*.java</exclude>
                </excludes>
            </configuration>
        </plugin>
        <!--步骤五:Runs integration tests -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-failsafe-plugin</artifactId>
            <version>2.18</version>
            <executions>
                <execution>
                    <id>integration-tests</id>
                    <goals>
                        <goal>integration-test</goal>
                        <goal>verify</goal>
                    </goals>
                    <configuration>
                        <skipTests>${skip.integration.tests}</skipTests>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>


到这里我们就完成了基于Maven构建的Spring Boot项目的UT及IT代码目录的分离配置,此时对UT代码的执行还是通过默认“mvn test”命令,而集成测试代码的运行则可以通过如下命令:

mvn clean verify -P integration-test


单元测试代码示例

Java如何优雅地实现单元测试与集成测试


通过前面的配置操作就完成了单元测试、集成测试代码目录的分离设置。在后续的开发过程中只需要将相应的测试代码写在对应的测试目录即可。接下来我们模拟一段业务逻辑并演示如何编写其对应的UT代码。具体如下:


Java如何优雅地实现单元测试与集成测试


如上图所示,参考MVC三层规范,我们编写了一个接口逻辑,该接口Controller层接收Http请求后调用Service层进行处理,而Service层处理逻辑时会调用Dao层操作数据库,并将具体信息插入数据库。


那么我们编写单元测试(UT)代码时,针对的是单独的某个逻辑单元的测试,而不是从头到位的整个逻辑,它的运行不应该依赖于任何网络环境或其他组件,所有依赖的组件或网络都应该先进行Mock。以单元测试TestServceImpl中的“saveTest”方法为例,其UT代码编写如下:


@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestServiceImpl.class)
@ActiveProfiles("test")
public class TestServiceImplTest {

    @Autowired
    TestServiceImpl testServiceImpl;

    @MockBean
    TestDao testDao;

    @Test
    public void saveTest() {
        //调用测试方法
        testServiceImpl.saveTest("无敌码农微信公众号");
        //验证执行测试的逻辑中是否调用过addUser方法
        verify(testDao).addUser(any());
    }
}


如上所示UT代码,我们UT测试的主要对象为TestServiceImpl类,所以可以在@SpringBootTest注解中进行范围指定。而@ActiveProfiles("test")则表示代码中所依赖的系统参数,可以从测试资源目录resouces/application-test.yml文件中获得。


单元测试的主要目的是验证单元代码内的逻辑,对于所依赖的数据库Dao组件并不是测试的范围,但是没有该Dao组件对象,UT代码在执行的过程中也会报错,所以一般会通过@MockBean注解进行组件Mock,以此解决UT测试过程中的代码依赖问题。此时运行“mvn test”命令:


Java如何优雅地实现单元测试与集成测试


单元测试代码得以正常执行!


集成测试代码示例

Java如何优雅地实现单元测试与集成测试


在Spring Boot中UT代码的编写方式与IT代码类似,但是其执行范围是包括了整个上下文环境。我们以模拟从Controller层发起Http接口请求为例,来完整的测试整个接口的逻辑,并最终将数据存入数据库。具体测试代码如下:


@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfiles("test")
public class ITTestControllerTest {

    @Autowired
    TestController testController;

    @Test
    public void saveTest() {
        testController.saveTest("无敌码农微信公众号");
    }
}


可以看到对于集成测试代码在@SpringBootTest中并没有指定具体的类,它的默认执行范围为整个应用的上下文环境。而代码中的依赖组件由于整个应用上下文都会被启动,所以依赖上并不会报错,可以理解为是一个正常启动的Spring Boot应用。


需要注意的是由于IT代码的目录有独立的资源配置,所以相关的依赖配置,如数据库等需要在“src/integration-test/resouces/application-test.yml”文件中单独配置,例如:


spring:
  application:
    name: springboot-test-demo
  #数据库逻辑
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/test
    username: root
    password: 123456
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    separator: //

server:
  port: 8080


此时运行集成测试命令“mvn clean verify -P integration-test”:

Java如何优雅地实现单元测试与集成测试

可以看到执行IT测试代码得以正常执行!



后记


本文着重介绍了在Java项目中如何编写单元测试(UT)和集成测试(IT)代码的工程实践。在日常编写代码的过程中,良好的测试代码编写是一种非常好的习惯,一般来说对于UT或IT代码执行错误的工程,要求严格的团队会让其构建的过程中无法通过,以此来严格要求团队成员。




—————END—————


参考链接:

https://www.petrikainulainen.net/programming/maven/integration-testing-with-maven/

以上是关于Java如何优雅地实现单元测试与集成测试的主要内容,如果未能解决你的问题,请参考以下文章

Springboot集成JUnit5优雅进行单元测试

DAO 测试的优雅方式

Spring Boot 集成 JUnit5,更优雅单元测试!

如何优雅的写单元测试?

如何优雅的写单元测试?

如何使用Spring + EasyMock做Java单元测试