You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Ktor集成测试中复用或关闭MySQL连接池?

Ktor集成测试中Hikari连接池导致MySQL连接数超限的解决方法

问题描述

我按照Ktor的Testing指南为后端应用做集成测试,采用Docker部署的真实MySQL数据库(搭配Hikari连接池和Exposed ORM)。测试能正常运行,但存在连接泄漏问题:每个testApplication测试用例执行完毕后,连接池不会被销毁,会一直保留到所有测试结束。结果每个测试都会新建一个HikariPool,例如设置maximumPoolSize = 4时,跑20个测试就会创建80个连接,触发MySQL的Too Many Connection错误。我需要在测试间复用连接池,或能手动销毁连接,且不想在GitHub Actions中为每个测试重启MySQL。


现有代码结构

集成测试代码

class ExampleIntegrationTest {

    @Test
    fun `Integration Test for the Example endpoints`() = testApplication {
        environment {
           config = ApplicationConfig(application_integration_test.conf)
        }
        val response = client.post("/path/example/test") {
             header("Authorization", dummyToken)
             header(contentType, APPLICATION_JSON)
             body?.let { setBody(body) }
        }
        assertEquals(201, response.status)
        assertEquals("[]", response.bodyAsText())
        ...
    }
}

测试配置文件application_integration_test.conf

database {
    driverClassName = com.mysql.cj.jdbc.Driver
    jdbcUrl = "jdbc:mysql://localhost:3307/test?useSSL=false"
    username = testuser
    password = password
    maximumPoolSize = 4
    isAutoCommit = false
    transactionIsolation = TRANSACTION_REPEATABLE_READ
    connectionTimeout = 60000
    maxLifetime = 20000000
    idleTimeout = 10000000
    leakDetectionThreshold = 8000
}

Docker Compose配置

version: '3'
services:
  mysql-for-unit-tests:
    image: test/mysql-test
    platform: linux/x86_64
    build:
      context: ../
      dockerfile: docker/mysqltest/Dockerfile
    ports:
      - "3307:3307"
    environment:
      MYSQL_DATABASE: test
      MYSQL_USER: testuser
      MYSQL_PASSWORD: password
      MYSQL_ROOT_PASSWORD: root
    volumes:
      - ./mysqltest/db:/var/lib/mysql

MySQL Dockerfile

FROM mysql:5.7-debian

# copy conf file
COPY ./docker/mysqltest/mysql.conf.d/test-api.cnf /etc/mysql/mysql.conf.d/test-api.cnf

# Set debian default locale to ja_JP.UTF-8

RUN apt-get update && \
    apt-get install -y locales && \
    rm -rf /var/lib/apt/lists/* && \
    echo "ja_JP.UTF-8 UTF-8" > /etc/locale.gen && \
    locale-gen ja_JP.UTF-8
ENV LC_ALL ja_JP.UTF-8

解决方法

1. 测试类级别复用单个连接池

将连接池的初始化和销毁移到测试类的@BeforeAll和@AfterAll方法中,让整个测试类共用一个连接池,避免重复创建:

class ExampleIntegrationTest {
    private lateinit var dataSource: HikariDataSource

    @BeforeAll
    fun setupDataSource() {
        val config = HikariConfig().apply {
            jdbcUrl = "jdbc:mysql://localhost:3307/test?useSSL=false"
            username = "testuser"
            password = "password"
            maximumPoolSize = 4
            // 同步application_integration_test.conf中的其他配置
            isAutoCommit = false
            transactionIsolation = "TRANSACTION_REPEATABLE_READ"
            connectionTimeout = 60000
        }
        dataSource = HikariDataSource(config)
    }

    @AfterAll
    fun shutdownDataSource() {
        dataSource.close() // 测试类结束后销毁连接池
    }

    @Test
    fun `Integration Test for the Example endpoints`() = testApplication {
        environment {
           config = ApplicationConfig(application_integration_test.conf)
           // 覆盖应用模块中的数据源,使用预创建的全局连接池
           module {
               // 假设应用通过DI容器获取数据源,这里替换为全局dataSource
               single<DataSource> { dataSource }
           }
        }
        // 原有测试逻辑不变
        val response = client.post("/path/example/test") {
             header("Authorization", dummyToken)
             header(contentType, APPLICATION_JSON)
             body?.let { setBody(body) }
        }
        assertEquals(201, response.status)
        assertEquals("[]", response.bodyAsText())
        ...
    }
}

2. 单个测试用例结束后手动销毁连接池

如果必须为每个测试用例单独创建连接池,可在测试结束后手动关闭数据源:

@Test
fun `Integration Test for the Example endpoints`() = testApplication {
    environment {
       config = ApplicationConfig(application_integration_test.conf)
    }
    // 原有测试逻辑
    val response = client.post("/path/example/test") {
         header("Authorization", dummyToken)
         header(contentType, APPLICATION_JSON)
         body?.let { setBody(body) }
    }
    assertEquals(201, response.status)
    assertEquals("[]", response.bodyAsText())
    
    // 从应用中获取数据源并关闭
    val dataSource = application.attributes.get(DataSourceKey) // 需提前定义DataSourceKey存储数据源
    (dataSource as HikariDataSource).close()
}

注意:需确保Ktor应用将DataSource实例存入ApplicationAttributes或DI容器,方便测试时获取。

3. 优化Hikari配置减少连接占用

调整Hikari参数,让闲置连接更快释放,避免连接堆积:

database {
    // 保持其他原有配置不变
    maxLifetime = 600000      // 缩短连接最大生命周期为10分钟
    idleTimeout = 300000      // 闲置连接5分钟后回收
    maximumPoolSize = 2       // 根据测试需求降低连接池上限
}

4. 可选:用Testcontainers管理数据库生命周期

若允许测试时重启数据库,Testcontainers可自动为每个测试类/用例创建和销毁MySQL容器,彻底避免连接泄漏。但此方案不适合需要复用同一数据库实例的场景,可根据需求选择。


内容的提问来源于stack exchange,提问作者shokdjsa-dkfdk.ss

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 14:34:56