组件测试-复杂业务系统的单测解决方案

简介: 本文的目的长久以来,码农们有一种默契的共识,只有写的好的代码才能写单测,代码的可测性也因此成为了评判好代码的一个标准。但是,同学有没有想过,我们手里有多少是写的好的代码,很多情况下,我们接手的代码都是又长又臭的“面条代码”,难道我们就不能对这些应用写单测了吗?难道这些应用不是更需要单测的保障吗?在本文中,我将使用一种全新的单测解决方案,通过这个方案,再复杂的应用也能简单的实现单测覆盖,保障我们的业

本文的目的

长久以来,码农们有一种默契的共识,只有写的好的代码才能写单测,代码的可测性也因此成为了评判好代码的一个标准。但是,同学有没有想过,我们手里有多少是写的好的代码,很多情况下,我们接手的代码都是又长又臭的“面条代码”,难道我们就不能对这些应用写单测了吗?难道这些应用不是更需要单测的保障吗?

在本文中,我将使用一种全新的单测解决方案,通过这个方案,再复杂的应用也能简单的实现单测覆盖,保障我们的业务正确性。

单测的定义

因为本方案可能会颠覆你对单测的理解,所以在描述方案之前,我有必要对单测作一个定义,当然这个定义不是我发明的,而是马丁·福勒(Martin Fowler)。微服务下的测试策略 by 马丁·福勒

单测大类

从大类上分,单测分为

社交型单测(Sociable unit testing)
独立型单测(solitary unit testing)

下图描述了这两种单测的测试边界:

单测细分

再往下就是针对各个细分场景的单测分类,比如微服务下的单测。他把微服务下的单测分成了三类:

单元测试(Unit Test):类或方法级别的单元测试
集成测试(Integration Test):对于外部系统的测试,只验证外部系统的正确性
组件测试(Component Test):基于完整用例的测试,从上到下贯穿系统,使用内存数据库和mock技术解决外部依赖

下图中Martin Fowler用不同颜色的框框,框出了每一种单测的边界。

我们要解决什么问题?

我们先回到我们要解决的问题,我们不是为了单测而单测,也不是为了覆盖率而写单测,我们是要验证我们“代码的正确性”。

而验证代码正确性,测试同学有非常有效的手段,那就是自动化的集成测试,在盒马,我们的测试同学投入了巨大的精力,构建了庞大的自动化集成测试套件,这些自动化case一次又一次的保障了我们的系统。但这套系统有一些难于解决的问题:

维护成本巨高
问题排查耗时
不能断言问题原因
运行时间长
多测试并行相互影响

因此,我们一直在想,是不是能借用“自动化集成测试套件”,同时又能解决它带来的问题呢?我们对这套系统提出了以下几个要求:

能最大程度的还原“自动化集成测试”
维护成本降到1/10
问题排查简单
能精确断言问题原因
运行时长<1分钟
拔掉网线仍然可正常运行

复杂应用的单测解决方案

我们采用了“组件测试”来解决复杂应用的单元测试,思路与测试同学的集成测试类似,区别在于我们使用了内存数据库并且mock了外部依赖。因为基于测试同学的自动化集成测试,我们使用运行中的数据快照来mock数据,让我们的单测能完整表达业务用例,同时又具有单元测试的诸多优点,比如:运行快、可精确断言、单机可运行等。

方案示意图如下:

当然,上图只是简单的表述了一下方案的思路,在真正实现中还需要解决很多问题。

一、单测容器的选择

使用SpringJUnit4ClassRunner作为测试容器,保证测试运行能在10s内完成。ComponentTestRunner中定制测试类中的方法的执行顺序,Junit默认每个类的方法运行顺序是随机的,为了能在不同机器上保证测试方法运行顺序都一致,我们做了一些扩展。当然最好的单测是可以无顺序执行的,但我们引入了组件测试,组件测试是依赖H2数据库的,测试用例之间有可能会相互影响,为了让单测的维护更简单,因此我们通过扩展Runner来保证了运行顺序。另外类之间的运行顺序,我们也会通过Test Suit来保证,这样就能保证单测在本机运行通过,任何环境上都能通过了。

@RunWith(ComponentTestRunner.class)
@DelegateTo(SpringJUnit4ClassRunner.class)
@ActiveProfiles("test")
@TestPropertySource("classpath:application-test.properties")
@ContextConfiguration(
    classes = {
        DataBaseConfig.class,
        OrderController.class,
        OrderCreateCmdService.class,
        //测试所依赖的类
    })
public abstract class AbstractTest {
    
}

二、数据库的封装

封装mysql与H2,同一套代码支持不同数据库,可灵活切换。通过设置isStartDatabaseWebServer=true,可以在运行时调出内存数据库web页面。

    private DataSource getDataSource() {
        if (dataSourceForWebServer == null) {
            InMemoryDatabaseFactory factory = new InMemoryDatabaseFactory();
            DataSource dataSource = factory.createDatabase(DatabaseType.H2, Arrays.asList("ddl/*.sql"), isStartDatabaseWebServer);
            dataSourceForWebServer = dataSource;
        }

        return dataSourceForWebServer;
    }

内存数据库web页面

三、Spring-Test的增强

在一些老应用中,一个service中可能依赖了几十个其它的service,而我们要测试的方法,可能只使用了一个,这需要我们手动去注入或mock其它不需要的service。因此我们扩展了Spring-Test,定制了一个可以按需自动mock的ContextLoader。

1、通过重写BeanFactory中的findAutowireCandidates方法,定制了一个AutoMockBeanFactory

2、通过重写ContextLoader中的loadContext方法,定制了一个测试用的loader

    protected Map findAutowireCandidates(String beanName, Class requiredType, DependencyDescriptor descriptor) {
        String mockBeanName = Introspector.decapitalize(requiredType.getSimpleName()) + "Mock";
        Map autowireCandidates = new HashMap<>();
        try {
            autowireCandidates = super.findAutowireCandidates(beanName, requiredType, descriptor);
        } catch (UnsatisfiedDependencyException e) {
            if (e.getCause() != null && e.getCause().getCause() instanceof NoSuchBeanDefinitionException) {
                mockBeanName = ((NoSuchBeanDefinitionException) e.getCause().getCause()).getBeanName();
            }
            this.registerBeanDefinition(mockBeanName, BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition());
        }
        //当spring容器中找不到指定类时,自动做mock
        if (autowireCandidates.isEmpty()) {
            //只有required是true时,才需要mock
            if(descriptor.isRequired()) {
                //自动moack需要的类
                final Object mock = mock(requiredType);
                autowireCandidates.put(mockBeanName, mock);
                this.addSingleton(mockBeanName, mock);
            }
        }
        return autowireCandidates;
    }

    public final ConfigurableApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception {
        if (logger.isDebugEnabled()) {
            logger.debug(String.format("Loading ApplicationContext for merged context configuration [%s].",
                mergedConfig));
        }

        validateMergedContextConfiguration(mergedConfig);

        GenericApplicationContext context = new GenericApplicationContext(new AutoMockBeanFactory());

        ApplicationContext parent = mergedConfig.getParentApplicationContext();
        if (parent != null) {
            context.setParent(parent);
        }
        prepareContext(context);
        prepareContext(context, mergedConfig);
        customizeBeanFactory(context.getDefaultListableBeanFactory());
        loadBeanDefinitions(context, mergedConfig);
        AnnotationConfigUtils.registerAnnotationConfigProcessors(context);
        customizeContext(context);
        customizeContext(context, mergedConfig);
        context.refresh();
        context.registerShutdownHook();
        return context;
    }

四、Mock及测试数据

Mock和测试数据没有特别的好方式,对于Mock我们通常只对外部的依赖做mock,使用的是Mockito。测试数据,使用sql插入到内存数据库中,或通过json文件来mock

使用原型一键生成整个解决方案

我们为我们的测试方案制作了一个Archetype,通过以下命令你可能生成一个搭载了我们测试方案的Demo:

mvn archetype:generate \
-DarchetypeGroupId=testdemo \
-DarchetypeArtifactId=component-test-demo-archetype \
-DarchetypeVersion=1.0.0-SNAPSHOT \
-DgroupId=xxx \
-DartifactId=xxx \
-Dversion=xxx

这个Demo中还包括了,基于DDD依赖倒置的分层,CQRS,以及一个DDD的样例,在xxx-test模板中你能找到我们的测试方案

相关文章
|
5天前
|
JSON 前端开发 API
以项目登录接口为例-大前端之开发postman请求接口带token的请求测试-前端开发必学之一-如果要学会联调接口而不是纯写静态前端页面-这个是必学-本文以优雅草蜻蜓Q系统API为实践来演示我们如何带token请求接口-优雅草卓伊凡
以项目登录接口为例-大前端之开发postman请求接口带token的请求测试-前端开发必学之一-如果要学会联调接口而不是纯写静态前端页面-这个是必学-本文以优雅草蜻蜓Q系统API为实践来演示我们如何带token请求接口-优雅草卓伊凡
29 5
以项目登录接口为例-大前端之开发postman请求接口带token的请求测试-前端开发必学之一-如果要学会联调接口而不是纯写静态前端页面-这个是必学-本文以优雅草蜻蜓Q系统API为实践来演示我们如何带token请求接口-优雅草卓伊凡
|
26天前
|
JavaScript NoSQL Java
基于SpringBoot+Vue实现的大学生体质测试管理系统设计与实现(系统源码+文档+数据库+部署)
面向大学生毕业选题、开题、任务书、程序设计开发、论文辅导提供一站式服务。主要服务:程序设计开发、代码修改、成品部署、支持定制、论文辅导,助力毕设!
38 2
|
26天前
|
小程序 前端开发 关系型数据库
uniapp跨平台框架,陪玩系统并发性能测试,小程序源码搭建开发解析
多功能一体游戏陪练、语音陪玩系统的开发涉及前期准备、技术选型、系统设计与开发及测试优化。首先,通过目标用户分析和竞品分析明确功能需求,如注册登录、预约匹配、实时语音等。技术选型上,前端采用Uni-app支持多端开发,后端选用PHP框架确保稳定性能,数据库使用MySQL保证数据一致性。系统设计阶段注重UI/UX设计和前后端开发,集成WebSocket实现语音聊天。最后,通过功能、性能和用户体验测试,确保系统的稳定性和用户满意度。
|
1月前
|
消息中间件 监控 小程序
电竞陪玩系统架构优化设计,陪玩app如何提升系统稳定性,陪玩小程序平台的测试与监控
电竞陪玩系统架构涵盖前端(React/Vue)、后端(Spring Boot/php)、数据库(MySQL/MongoDB)、实时通信(WebSocket)及其他组件(Redis、RabbitMQ、Nginx)。通过模块化设计、微服务架构和云计算技术优化,提升系统性能与可靠性。同时,加强全面测试、实时监控及故障管理,确保系统稳定运行。
|
1月前
|
数据挖掘 测试技术 项目管理
2025年测试用例管理看这一篇就够了 ----Codes 开源免费、全面的测试管理解决方案
Codes 是国内首款重新定义 SaaS 模式的开源项目管理平台,支持云端认证、本地部署、全部功能开放,并且对 30 人以下团队免费。它通过整合迭代、看板、度量和自动化等功能,简化测试协同工作,使敏捷测试更易于实施。并提供低成本的敏捷测试解决方案,如同步在线离线测试用例、流程化管理缺陷、低代码接口自动化测试和 CI/CD,以及基于迭代的测试管理和测试用时的成本计算等,践行敏捷测试。
2025年测试用例管理看这一篇就够了 ----Codes 开源免费、全面的测试管理解决方案
|
2月前
|
Linux Shell 网络安全
Kali Linux系统Metasploit框架利用 HTA 文件进行渗透测试实验
本指南介绍如何利用 HTA 文件和 Metasploit 框架进行渗透测试。通过创建反向 shell、生成 HTA 文件、设置 HTTP 服务器和发送文件,最终实现对目标系统的控制。适用于教育目的,需合法授权。
91 9
Kali Linux系统Metasploit框架利用 HTA 文件进行渗透测试实验
|
3月前
|
数据库连接 Go 数据库
Go语言中的错误注入与防御编程。错误注入通过模拟网络故障、数据库错误等,测试系统稳定性
本文探讨了Go语言中的错误注入与防御编程。错误注入通过模拟网络故障、数据库错误等,测试系统稳定性;防御编程则强调在编码时考虑各种错误情况,确保程序健壮性。文章详细介绍了这两种技术在Go语言中的实现方法及其重要性,旨在提升软件质量和可靠性。
57 1
|
3月前
|
缓存 监控 测试技术
全网最全压测指南!教你如何测试和优化系统极限性能
大家好,我是小米。本文将介绍如何在实际项目中进行性能压测和优化,包括单台服务器和集群压测、使用JMeter、监控CPU和内存使用率、优化Tomcat和数据库配置等方面的内容,帮助你在高并发场景下提升系统性能。希望这些实战经验能助你一臂之力!
191 3
|
3月前
|
JavaScript 测试技术 API
Jest进阶:测试 Vue 组件
Jest进阶:测试 Vue 组件
|
3月前
|
前端开发 JavaScript 安全
学习如何为 React 组件编写测试:
学习如何为 React 组件编写测试:
56 2

热门文章

最新文章