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

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

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

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

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

Maven测试代码结构的组织

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

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

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

如上图所示,在规划的目录结构中我们将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文件来进行区分,具体示意图如下:

如上图所示,我们可以在与“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

单元测试代码示例

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

如上图所示,参考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”命令:

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

集成测试代码示例

在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”:

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

后记

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

希望本文的内容能对你的编码有所启发,如果觉得还不错,可以转发给更多的朋友!

写在最后

欢迎大家关注我的公众号【风平浪静如码】,海量Java相关文章,学习资料都会在里面更新,整理的资料也会放在里面。

觉得写的还不错的就点个赞,加个关注呗!点关注,不迷路,持续更新!!!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342