|
| 1 | +package io.github.truenine.composeserver.testtoolkit.testcontainers |
| 2 | + |
| 3 | +import jakarta.annotation.Resource |
| 4 | +import java.sql.DriverManager |
| 5 | +import java.sql.SQLException |
| 6 | +import kotlin.test.assertEquals |
| 7 | +import kotlin.test.assertFailsWith |
| 8 | +import kotlin.test.assertNotNull |
| 9 | +import kotlin.test.assertTrue |
| 10 | +import org.junit.jupiter.api.Test |
| 11 | +import org.springframework.boot.test.context.SpringBootTest |
| 12 | +import org.springframework.core.env.Environment |
| 13 | +import org.springframework.jdbc.core.JdbcTemplate |
| 14 | + |
| 15 | +@SpringBootTest |
| 16 | +class IDatabaseMysqlContainerTest : IDatabaseMysqlContainer { |
| 17 | + lateinit var environment: Environment |
| 18 | + @Resource set |
| 19 | + |
| 20 | + lateinit var jdbcTemplate: JdbcTemplate |
| 21 | + @Resource set |
| 22 | + |
| 23 | + @Test |
| 24 | + fun `验证 MySQL 容器成功启动`() { |
| 25 | + assertNotNull(mysqlContainer, "MySQL 容器应该存在") |
| 26 | + assertTrue(mysqlContainer?.isRunning == true, "MySQL 容器应该处于运行状态") |
| 27 | + |
| 28 | + // 通过执行简单查询来验证容器是否正常工作 |
| 29 | + val version = jdbcTemplate.queryForObject("SELECT VERSION()", String::class.java) |
| 30 | + assertNotNull(version, "应该能够获取 MySQL 版本信息") |
| 31 | + assertTrue(version.contains("8.0"), "数据库应该是 MySQL 8.0") |
| 32 | + } |
| 33 | + |
| 34 | + @Test |
| 35 | + fun `验证 Spring 环境中包含数据源配置`() { |
| 36 | + // 验证必要的数据源配置属性是否存在 |
| 37 | + assertNotNull(environment.getProperty("spring.datasource.url"), "数据源 URL 应该存在") |
| 38 | + assertNotNull(environment.getProperty("spring.datasource.username"), "数据源用户名应该存在") |
| 39 | + assertNotNull(environment.getProperty("spring.datasource.password"), "数据源密码应该存在") |
| 40 | + assertNotNull(environment.getProperty("spring.datasource.driver-class-name"), "数据源驱动类名应该存在") |
| 41 | + |
| 42 | + // 验证 URL 是否指向 TestContainers 的 MySQL |
| 43 | + val jdbcUrl = environment.getProperty("spring.datasource.url") |
| 44 | + assertTrue(jdbcUrl?.contains("jdbc:mysql") == true, "JDBC URL 应该是 MySQL 连接") |
| 45 | + } |
| 46 | + |
| 47 | + @Test |
| 48 | + fun `验证数据库连接可以成功建立`() { |
| 49 | + val connection = jdbcTemplate.dataSource?.connection |
| 50 | + assertNotNull(connection, "应该能够获取数据库连接") |
| 51 | + |
| 52 | + connection.use { conn -> |
| 53 | + assertTrue(conn.isValid(5), "数据库连接应该有效") |
| 54 | + assertEquals("MySQL", conn.metaData.databaseProductName, "数据库类型应该是 MySQL") |
| 55 | + |
| 56 | + // 验证数据库连接状态 |
| 57 | + val stmt = conn.createStatement() |
| 58 | + val rs = stmt.executeQuery("SELECT CONNECTION_ID() as id") |
| 59 | + assertTrue(rs.next(), "应该能够查询到当前连接的ID") |
| 60 | + val connectionId = rs.getLong("id") |
| 61 | + assertTrue(connectionId > 0, "连接ID应该大于0") |
| 62 | + } |
| 63 | + } |
| 64 | + |
| 65 | + @Test |
| 66 | + fun `验证数据库基本操作正常`() { |
| 67 | + // 验证可以执行基本的 SQL 操作 |
| 68 | + val result = jdbcTemplate.queryForObject("SELECT 1", Int::class.java) |
| 69 | + assertEquals(1, result, "应该能够执行基本的 SQL 查询") |
| 70 | + |
| 71 | + // 验证可以创建和删除临时表 |
| 72 | + jdbcTemplate.execute("CREATE TEMPORARY TABLE test_table (id int)") |
| 73 | + |
| 74 | + // 验证表结构 - 对于临时表,直接通过 DESCRIBE 命令验证 |
| 75 | + val columnInfo = jdbcTemplate.queryForList("DESCRIBE test_table") |
| 76 | + assertTrue(columnInfo.isNotEmpty(), "临时表应该有列定义") |
| 77 | + assertTrue(columnInfo.any { it["Field"] == "id" }, "临时表应该包含 id 列") |
| 78 | + |
| 79 | + // 验证表是否可以正常操作 |
| 80 | + jdbcTemplate.execute("INSERT INTO test_table (id) VALUES (999)") |
| 81 | + val tableOperationTest = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM test_table WHERE id = 999", Int::class.java) |
| 82 | + assertEquals(1, tableOperationTest, "应该能够向临时表插入数据并查询") |
| 83 | + |
| 84 | + // 验证表的可操作性 - 添加另一条记录并验证总数 |
| 85 | + jdbcTemplate.execute("INSERT INTO test_table (id) VALUES (1)") |
| 86 | + val insertedCount = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM test_table", Int::class.java) |
| 87 | + assertEquals(2, insertedCount, "临时表应该包含所有插入的记录") |
| 88 | + |
| 89 | + // 验证可以删除数据 |
| 90 | + jdbcTemplate.execute("DELETE FROM test_table WHERE id = 1") |
| 91 | + val remainingCount = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM test_table", Int::class.java) |
| 92 | + assertEquals(1, remainingCount, "删除后应该只剩下一条记录") |
| 93 | + } |
| 94 | + |
| 95 | + @Test |
| 96 | + fun `验证容器端口映射正确`() { |
| 97 | + val mappedPort = mysqlContainer?.getMappedPort(3306) |
| 98 | + assertNotNull(mappedPort, "MySQL 端口应该被正确映射") |
| 99 | + assertTrue(mappedPort > 0, "映射端口应该是有效的端口号") |
| 100 | + |
| 101 | + // 验证端口可访问性 |
| 102 | + val databaseName = mysqlContainer?.databaseName |
| 103 | + assertNotNull(databaseName, "数据库名称不应为空") |
| 104 | + |
| 105 | + val jdbcUrl = "jdbc:mysql://localhost:$mappedPort/$databaseName" |
| 106 | + val username = mysqlContainer?.username |
| 107 | + val password = mysqlContainer?.password |
| 108 | + |
| 109 | + assertNotNull(username, "数据库用户名不应为空") |
| 110 | + assertNotNull(password, "数据库密码不应为空") |
| 111 | + |
| 112 | + DriverManager.getConnection(jdbcUrl, username, password).use { conn -> |
| 113 | + assertTrue(conn.isValid(5), "应该能够通过映射端口建立连接") |
| 114 | + |
| 115 | + // 验证连接的数据库名称 |
| 116 | + assertEquals(databaseName, conn.catalog, "连接的数据库名称应该正确") |
| 117 | + |
| 118 | + // 验证数据库名称格式 |
| 119 | + assertTrue(databaseName.matches(Regex("^[a-zA-Z_][a-zA-Z0-9_]*$")), "数据库名称应符合标准格式") |
| 120 | + |
| 121 | + // 验证连接属性 |
| 122 | + assertTrue(conn.metaData.supportsTransactions(), "应支持事务") |
| 123 | + assertTrue(conn.metaData.supportsStoredProcedures(), "应支持存储过程") |
| 124 | + } |
| 125 | + } |
| 126 | + |
| 127 | + @Test |
| 128 | + fun `验证无效连接时抛出异常`() { |
| 129 | + val invalidJdbcUrl = "jdbc:mysql://localhost:1234/nonexistent" |
| 130 | + assertFailsWith<SQLException>("使用无效连接应该抛出异常") { DriverManager.getConnection(invalidJdbcUrl) } |
| 131 | + } |
| 132 | + |
| 133 | + @Test |
| 134 | + fun `验证数据库字符集配置`() { |
| 135 | + val charset = jdbcTemplate.queryForObject("SELECT @@character_set_database", String::class.java) |
| 136 | + assertNotNull(charset, "数据库字符集应该存在") |
| 137 | + |
| 138 | + // MySQL 8.0 默认字符集是 utf8mb4 |
| 139 | + assertTrue(charset.contains("utf8") || charset == "utf8mb4", "数据库字符集应该是 UTF8 相关 (actual: $charset)") |
| 140 | + |
| 141 | + // 验证客户端连接编码 |
| 142 | + val connectionCharset = jdbcTemplate.queryForObject("SELECT @@character_set_connection", String::class.java) |
| 143 | + assertNotNull(connectionCharset, "连接字符集应该存在") |
| 144 | + } |
| 145 | + |
| 146 | + @Test |
| 147 | + fun `验证数据库时区配置`() { |
| 148 | + val timezone = jdbcTemplate.queryForObject("SELECT @@system_time_zone", String::class.java) |
| 149 | + assertNotNull(timezone, "数据库时区设置应该存在") |
| 150 | + |
| 151 | + // 验证时区设置不为空 |
| 152 | + assertTrue(timezone.isNotEmpty(), "时区设置不应为空") |
| 153 | + |
| 154 | + // 验证可以获取当前时间 |
| 155 | + val currentTime = jdbcTemplate.queryForObject("SELECT NOW()", java.sql.Timestamp::class.java) |
| 156 | + assertNotNull(currentTime, "应该能获取当前时间") |
| 157 | + |
| 158 | + // 验证时间的合理性 - 考虑时区差异,允许更大的时间差 |
| 159 | + val now = System.currentTimeMillis() |
| 160 | + val timeDiff = kotlin.math.abs(currentTime.time - now) |
| 161 | + // 允许最大24小时的时区差异加上1分钟的执行时间差 |
| 162 | + val maxAllowedDiff = 24 * 60 * 60 * 1000 + 60000 // 24小时 + 1分钟 |
| 163 | + assertTrue(timeDiff < maxAllowedDiff, "数据库时间应在合理范围内 (差值: ${timeDiff}ms, 约${timeDiff / 3600000}小时)") |
| 164 | + } |
| 165 | + |
| 166 | + @Test |
| 167 | + fun `验证 MySQL 特性支持`() { |
| 168 | + // 验证 MySQL 版本特性 |
| 169 | + val version = jdbcTemplate.queryForObject("SELECT VERSION()", String::class.java) |
| 170 | + assertNotNull(version, "MySQL 版本信息不应为空") |
| 171 | + assertTrue(version!!.startsWith("8.0"), "应该是 MySQL 8.0 版本") |
| 172 | + |
| 173 | + // 验证存储引擎支持 |
| 174 | + val engines = jdbcTemplate.queryForList("SHOW ENGINES") |
| 175 | + assertTrue(engines.any { (it["Engine"] as String).contains("InnoDB") }, "应该支持 InnoDB 存储引擎") |
| 176 | + |
| 177 | + // 验证 SQL 模式 |
| 178 | + val sqlMode = jdbcTemplate.queryForObject("SELECT @@sql_mode", String::class.java) |
| 179 | + assertNotNull(sqlMode, "SQL 模式应该存在") |
| 180 | + } |
| 181 | +} |
0 commit comments