万字长文带你快速了解并上手Testcontainers

2021-01-27 16:57:09 浏览数 (1)

前言

前段时间,我负责在所属的一个团队内部去推动一项叫做“Testcontainers”的技术。于是在调研并打磨了数天之后,就诞生下文。希望看完本篇文章的你,能够有所收获,感谢阅读!

本文首发:https://www.ebaina.com/articles/140000005317 作者:Alice菌


1. 技术的演进

1.1 传统的测试

我们的项目上线之前,一定会经过大量的测试。早期,如果一个项目所依赖的外部配置比较繁多,那么每次测试,我们都需要将项目所依赖的环境服务启动。如果测试人员的电脑没有对应的开发环境,则还需要花时间在环境搭建上。就算搭建好了, 各种版本的迭代之后,不同版本环境的兼容 , 也有可能导致测试失败,这些都是测试人员应该考虑的问题,这个时候,测试工作的效率往往就显得很低

1.2 Embedded

后来,出现了 “ In-memory Embedded Database ” 这样的方式,能够让我们在程序本地进行基于内存的嵌入式测试 ,而无需手动去启动环境,方便我们在本地编写、运行、调试 。但由于使用不同的服务,需要依赖于不同的第三方的类库,显得十分繁琐,并且很多 “In-memory Embedded Database” 只提供一个特定版本的实现 ,如果其提供的数据库版本与我们实际应用中的版本不一致, 那么就有可能导致很多新的数据库功能在测试里根本覆盖不了。 有些 In-memory Embedded Database 甚至没有实现100%的接口兼容,或者不一样的实现方式,这意味着就算你的测试过了,线上的代码还是可能会出错。这就是典型的生产环境和测试环境不一致性问题。 另外该项目维护不利, 大量缺陷未修复 ,并且缺少更新,导致用户的使用体验也越来越差

1.3 Docker

随着时代的不断发展,以 Docker 为代表的虚拟化容器技术出现了。 Docker 是一个开源的应用容器引擎 , 它可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。另外,容器是完全使用沙箱机制,相互之间不会有任何接口(类似 iPhone 的 app),更重要的是容器性能开销极低

此后,我们的测试工作所需要的环境就可以通过在Linux服务器上启动Docker中的容器来实现。但是docker虽然解决了环境的配置问题,但是我们每次测试,所需要的环境都需要到Linux服务器上通过命令手动启动容器,一旦外部依赖过多,启动容器这个过程所花费的时间也是笔不小的开销。那我们可能就会想,有没有什么方法能够实现通过编程语言远程启动docker中的容器,来代替我们人为的操作呢?

1.4 Testcontainers

为了解决这个问题,在社区成员的努力下,一个叫做 Testcontainers 的开源项目就诞生了。

TestContainers是一个开源项目,它提供可以在Docker容器中运行的任何东西的轻量级,一次性的实例。它具有Java,Python,Rust,Go,Scala和许多其他语言的绑定。其主要针对测试领域、背靠Docker实现环境百宝箱功能。

简单理解就是,testcontainers 能够让你实现通过编程语言去启动Docker容器,并在程序测试结束后,自动关闭容器。这基本上能解决我们大部分的需求。

使用 TestContainers 这种解决方案 还有以下几个优点:

  • 每个Test Group都能像写单元测试那样细粒度地写集成测试,保证每个集成单元的高测试覆盖率。
  • Test Group间是做到依赖隔离的,也就是说它们不共享任何一个Docker容器;假如两个Test Group都要用到Mongo 4.0,会创建两个容器供它们单独使用 。
  • 保证了生产环境和测试环境的一致性,代码部署到线上时不会遇到因为依赖服务接口不兼容而导致的bug 。
  • Test Group可以并行化运行,减少整体测试运行时间。相比较有些 in-memory 的依赖服务实现没有实现很好的资源隔离,比如端口,一旦并行化运行就会出现端口冲突 。
  • 得益于Docker,所有测试都可以在本地环境和 CI/CD环境中运行,测试代码调试和编写就如同写单元测试。

另外,TestContainers使以下类型的测试更加容易:

  • 数据访问层集成测试 :

使用MySQL,PostgreSQL或Oracle数据库的容器化实例测试您的数据访问层代码是否具有完全兼容性,但无需在开发人员的计算机上进行复杂的设置,并且无需担心测试始终以已知的数据库状态。 也可以使用任何其他可以容器化的数据库类型。

  • 应用程序集成测试 :

用于在具有数据库,消息队列或Web服务器等依赖项的短期测试模式下运行应用程序。

  • UI /验收测试 :

使用与Selenium兼容的容器化Web浏览器进行自动UI测试。 每个测试都可以获取浏览器的新实例,而无需担心浏览器状态,插件版本或浏览器自动升级。 您将获得每个测试会话或测试失败的每个会话的视频记录。

  • 更多

我们可以在官网查看其他人贡献的模块,也可以自己基于 GenericContainer ,创建自己的自定义容器类。

注意:

  • test-containers 基于 Docker,所以使用 test-container 前需要安装 Docker环境
  • test-containers 提供的环境不能应用于生产环境、只能用于测试环境等场景

2.Testcontainers所提供的模块

Testcontainers 提供了多种现成的与测试关联的应用程序容器。

其中Databases就支持这么多:

3. 不同语言版本的Testcontainers

Testcontainers 在GitHub上支持包含 java,go,python 等多种语言版本,基于我们项目的实际情况,下面的示例以testcontainers-scala为主。

4. Testcontainers连接策略和要求

因为 java 和 scala 运行都要基于 JVM,所以 testcontainers-scala 运行的环境首先需要满足:

  • JDK >= 1.8

又 test-container 基于 Docker,所以使用test-container前需要安装 Docker环境。

其中,关于Docker的版本,需要满足以下条件:

Testcontainers在运行时将会尝试按如下顺序使用以下策略连接到 Docker 守护程序:

  • 环境变量: – DOCKER_HOST – DOCKER_TLS_VERIFY – DOCKER_CERT_PATH

每个变量的作用:

Use DOCKER_HOST to set the url to the docker server. Use DOCKER_CERT_PATH to load the tls certificates from. Use DOCKER_TLS_VERIFY to enable or disable TLS verification.

  • 默认值 – DOCKER_HOST=https://localhost:2376 – DOCKER_TLS_VERIFY=1 – DOCKER_CERT_PATH=~/.docker
  • 我们可以在程序中显式设置系统变量代替默认值,例如:
代码语言:javascript复制
System.setProperty("DOCKER_HOST","tcp://10.16.2.103:2375")

这样我们在运行测试时,testcontainers 就会去连接指定节点的Docker环境

5. Testcontainers-scala入门需知

ScalaTest 有两种感知特质:

  • ForEachTestContainer : 在每个测试用例之前启动一个新容器,然后停止并删除它。
  • ForAllTestContainer : 对于规范内的所有测试用例,仅启动和停止一次容器 。

我们要开始使用 ScalaTest,只需要扩展这些特质之一,并 重写 container的val值。

代码语言:javascript复制
import com.dimafeng.testcontainers.{ForAllTestContainer, MySQLContainer}

class MysqlSpec extends FlatSpec with ForAllTestContainer {
   
  override val container = MySQLContainer()
  
  it should "do something" in {
    Class.forName(container.driverClassName)
    val connection = DriverManager.getConnection(container.jdbcUrl, container.username, container.password)
    ...
  }
}

对于存在多个测试用例的情况,可以参考下方 MySQL 的测试实例:

代码语言:javascript复制
import org.testcontainers.containers.MySQLContainer

class MysqlSpec extends FlatSpec with ForAllTestContainer {

    override val container = MySQLContainer()

    it should "do something" in {
      ...
    }

    it should "do something 2" in {
      ...
    }
}

此规范启动一个容器,两个测试共享容器的状态。

大多数可用的容器类都允许您提供自定义镜像名称或版本,而不是库中的默认镜像名称或版本。

为了提供自定义镜像名称,您需要传递 DockerImageName 对象 。

代码语言:javascript复制
override val container = MongoDBContainer(DockerImageName.parse("mongo:4.0.10"))

从 testcontainers-java 1.15.0 容器类开始,在初始化期间执行镜像兼容性检查(有关更多详细信息,请参阅此拉取请求)。如果要使用与所选容器类实现兼容的自定义镜像,则必须显式标记为与默认镜像兼容。

代码语言:javascript复制
override val container = MongoDBContainer(DockerImageName.parse("myregistry/mongo:4.0.10").asCompatibleSubstituteFor("mongo"))

6. 容器类型

注意:在 testcontainers 的测试中,有时候我们往往不需要通过输出结果来判断是否测试成功,我们可以通过assert(condition: Boolean)函数来进行断言,测试其逻辑。如果condition返回true,则接收返回值,继续执行,否则会抛出TestFailedException异常

6.1 单个容器

6.1.1 Generic Container

这是最灵活但不太方便的容器类型 , 此容器允许使用自定义配置启动任何 Docker 镜像。

代码语言:javascript复制
class GenericContainerSpec extends FlatSpec with ForAllTestContainer {
  override val container = GenericContainer("nginx:latest",
    exposedPorts = Seq(80),
    waitStrategy = Wait.forHttp("/")
  )

  "GenericContainer" should "start nginx and expose 80 port" in {
    assert(Source.fromInputStream(
      new URL(s"http://${container.containerIpAddress}:${container.mappedPort(80)}/").openConnection().getInputStream
    ).mkString.contains("If you see this page, the nginx web server is successfully installed"))
  }
}
6.1.2 Docker Compose

与通用容器支持类似,我们也可以运行定制的服务集 在 指定 docker-compose.yml 文件中。Compose file 是一个 YAML 文件,用于定义 servicesnetworksvolumes ,我们可以在 texttainers 中通过DockerComposeContainer类中传入该文件的路径,实现启动容器的自定义配置。

代码语言:javascript复制
class ComposeSpec extends FlatSpec with ForAllTestContainer {
  override val container = DockerComposeContainer(new File("src/test/resources/docker-compose.yml"), exposedServices = Seq(ExposedService("redis_1", 6379)))
   
  "DockerComposeContainer" should "retrieve non-0 port for any of services" in {
    assert(container.getServicePort("redis_1", 6379) > 0)
  }
}
6.1.3 Selenium
代码语言:javascript复制
class SeleniumSpec extends FlatSpec with SeleniumTestContainerSuite with WebBrowser {
  override def desiredCapabilities = DesiredCapabilities.chrome()

  "Browser" should "show google" in {
      go to "http://google.com"
  }
}

在这种情况下,您将在容器中获取一个浏览器实例(Firefox/chrome),测试将通过远程驱动程序连接到该实例。

6.1.4 MySQL
代码语言:javascript复制
class MysqlSpec extends FlatSpec with ForAllTestContainer {

  override val container = MySQLContainer()

  "Mysql container" should "be started" in {
    Class.forName(container.driverClassName)
    val connection = DriverManager.getConnection(container.jdbcUrl, container.username, container.password)
      ...
  }
}

容器也可以使用构造函数参数定制,这个代码段将使用特定的模式名和特定的用户名/密码从特定的docker镜像初始化docker容器。

代码语言:javascript复制
class MysqlSpec extends FlatSpec with ForAllTestContainer {

  override val container = MySQLContainer(mysqlImageVersion = DockerImageName.parse("mysql:5.7.18"),
                                          databaseName = "testcontainer-scala",
                                          username = "scala",
                                          password = "scala")

  "Mysql container" should "be started" in {
    Class.forName(container.driverClassName)
    val connection = DriverManager.getConnection(container.jdbcUrl, container.username, container.password)
      ...
  }
}
6.1.5 PostgresQL
代码语言:javascript复制
class PostgresqlSpec extends FlatSpec with ForAllTestContainer  {

  override val container = PostgreSQLContainer()

  "PostgreSQL container" should "be started" in {
    Class.forName(container.driverClassName)
    val connection = DriverManager.getConnection(container.jdbcUrl, container.username, container.password)
      ...
  }
}

补充说明,还有很多与MySQL,PostgresQL使用方式类似的镜像类型,这里就不一一列举了,更多 model 的使用示例请参考官网:https://www.testcontainers.org/

6.2 多个容器

如果需要在测试中测试多个容器 , 只需定义容器并传递给构造函数: MultipleContainers()

代码语言:javascript复制
val mySqlContainer = MySQLContainer()
val genericContainer = GenericContainer(...)

override val container = MultipleContainers(mySqlContainer, genericContainer)

6.3 依赖容器

如果一个容器的配置依赖于另一个容器的运行时状态,则应将容器定义为 :lazy

代码语言:javascript复制
lazy val container1 = Container1()
lazy val container2 = Container2(container1.port)

override val container = MultipleContainers(container1, container2)

6.4 固定主机端口容器

此容器将允许您将容器端口映射到 Docker 主机上的静态定义端口。

代码语言:javascript复制
...
val container = FixedHostPortGenericContainer("nginx:latest",
    waitStrategy = Wait.forHttp("/"),
    exposedHostPort = 8090,
    exposedContainerPort = 80
  )

6.5 内部容器的自定义配置

所有容器类型都具有通用参数的构造函数方法。如果您缺少一些自定义选项,请提供一种优雅的方式来调整嵌套容器。不建议通过下面这种方式,直接访问内部容器:

代码语言:javascript复制
override val container = MySQLContainer().configure { c =>
    c.withNetwork(...)
    c.withStartupAttempts(...)
  }

6.6 启动/停止 挂钩

如果要在容器启动后或容器停止之前执行代码,可以重写 afterStart()beforeStop() 方法 。

代码语言:javascript复制
class MysqlSpec extends FlatSpec with ForAllTestContainer {

  ...

  override def afterStart(): Unit = {
    println("-------------我在容器启动之后执行-------------")
    // your code
  }

  override def beforeStop(): Unit = {
    println("-------------我在容器停止之前执行-------------")
    // your code
  }
}

7. 示例代码

为了让大家对于 testcontainers 的使用有更深刻的印象,下面为大家分别带来 Kafka,Redis,MySQL做测试的例子。为了保证代码的严谨性,以下代码经过多次测试,请大家放心食用。

7.1 Kafka

场景示例:有一个测试目录 file/input 下存放着数据文件student.txt,我们需要读取该文本的内容,并采用「轮询」的方式将数据打入到Kafka的student主题的3个分区中,数据生产完毕之后,再通过消费者拉取数据,进行遍历消费,消费完毕后,手动删除该 topic

KafkaContainerTest

代码语言:javascript复制
import java.util
import java.util.Properties

import com.dimafeng.testcontainers.{ForAllTestContainer, KafkaContainer}
import com.hypers.util.KafkaSupport
import org.apache.kafka.clients.admin.AdminClient
import org.apache.kafka.clients.consumer.KafkaConsumer
import org.apache.kafka.clients.producer.KafkaProducer
import org.scalatest.{BeforeAndAfter, FlatSpec, Matchers}

/*
 * @Author: Alice菌
 * @Date: 2020/12/16 15:35
 * @Description:
 */
class KafkaContainerTest  extends FlatSpec with BeforeAndAfter with ForAllTestContainer with Matchers{

  private val props: Properties = new Properties()
  private var client: AdminClient = _;
  private var producer: KafkaProducer[String, String] = _;
  private var consumer: KafkaConsumer[String, String] = _;

  // 设置系统访问的 Docker节点 IP 和 端口
  System.setProperty("DOCKER_HOST","tcp://10.16.2.103:2375")
  // 手动启动容器
  override val container: KafkaContainer = KafkaContainer()

  "Kafka container" should "be started" in {
    // 创建 Topic 对象
    KafkaSupport.createKafkaTopic(client,"student", 3, 1)
    // 获取所有的 Topic 列表
    var allTopicList: util.Set[String] = KafkaSupport.showAllTopic(client)
    // 断言测试
    assert(allTopicList.contains("student"))
    // 实现 Kafka 生产的功能
    val producerNum: Int = KafkaSupport.KafkaProducer("file/input/", "student.txt", producer, "student", 1000)
    // 实现 Kafka 消费的功能
    val consumerNum: Int = KafkaSupport.KafkaConsumer(consumer, "student")
    // 断言 测试,判断生成和消费的数据条数是否相等
    assert(producerNum == consumerNum)
    // 将 topic 删除
    KafkaSupport.deleteTopic(client,"student")
    allTopicList  = KafkaSupport.showAllTopic(client)
    // 断言测试
    assert(!allTopicList.contains("student"))

  }

  before{
    // 初始化Kafka
    // 基本的配置信息
    props.put("bootstrap.servers", container.bootstrapServers)
    props.put("acks", "-1")
    props.put("retries", "0")
    props.put("batch.size", "16384")
    props.put("linger.ms", "1")
    props.put("buffer.memory", "33554432")
    props.put("group.id", "consumer-tutorial")
    // 设置 key,value的序列化方式
    props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer")
    props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer")
    // 设置 key,value的反序列化方式
    props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
    props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
    // 设置消费策略
    props.put("auto.offset.reset", "earliest")
    // 实例一个 Kafka 的 Client 对象
    client = AdminClient.create(props)
    // 实例一个 Kafka 的 生产者
    producer = new KafkaProducer[String, String](props)
    // 实例一个 Kafka 的 消费者
    consumer = new KafkaConsumer[String, String](props)

  }

  after{
    // 关闭连接
    consumer.close()
    producer.close()
    client.close()
  }
}

KafkaSupport

代码语言:javascript复制
import java.io.{BufferedReader, File, FileReader}
import java.util
import java.util.Collections

import org.apache.kafka.clients.admin.{AdminClient, ListTopicsOptions, ListTopicsResult, NewTopic}
import org.apache.kafka.clients.consumer.{ConsumerRecord, ConsumerRecords, KafkaConsumer}
import org.apache.kafka.clients.producer.{KafkaProducer, ProducerRecord}

import scala.util.control.Breaks

/*
 * @Author: Alice菌
 * @Date: 2020/12/16 15:14
 * @Description: 
    
 */
object KafkaSupport {

  def createKafkaTopic(client:AdminClient,topicName:String,numPartitions:Int,replicationFactor:Short): Unit ={
    // 创建一个主题对象,传入主题名,分区数,副本数
    val newTopic: NewTopic = new NewTopic(topicName, numPartitions, replicationFactor)
    // 创建一个 NewTopic 类型的 ArrayList 集合
    val topics: util.ArrayList[NewTopic] = new util.ArrayList[NewTopic]()
    // 将主题对象添加至集合内
    topics.add(newTopic)
    // 调用客户端 Client 的方法去创建 Topic
    client.createTopics(topics)

  }

  def showAllTopic(client:AdminClient): util.Set[String] ={
    val options: ListTopicsOptions = new ListTopicsOptions
    val topicList: ListTopicsResult = client.listTopics(options)
    val topicNames: util.Set[String] = topicList.names.get

    topicNames
  }

  def KafkaProducer(dirPath:String,fileNames:String,producer: KafkaProducer[String, String],topicName:String,sleepTime:Int): Int ={

    // 定义一个变量,保存数据行数
    var lineNumber:Int = 0

    // 新建一个文件对象   file/input/
    val file: File = new File(dirPath)
    // 新建一个缓存流对象
    var bufferedReader: BufferedReader = null
    // 新建一个字段保存行数据
    var line:String = null
    // 获取目录下的文件类型
    val files: Array[File] = file.listFiles()
    // 创建 Breaks 对象
    val loop: Breaks = new Breaks;

    // 对文件列表进行遍历
    if (files  != null){
      // 如果不为空,则对文件列表下的文件进行遍历
      for(tmpFile <- files) {
        val fileName: String = tmpFile.getName

        // 判断文件名是不是 student.txt
        if (fileName.equals(fileNames)) {
          bufferedReader = new BufferedReader(new FileReader(tmpFile.getAbsolutePath))

          loop.breakable {
            while (true) {
              // 读取行数据
              line = bufferedReader.readLine()
              // 当读取不到数据时,跳出循环
              if (line == null) {
                loop.break()
              }
              // 我们这里采用轮询的方式将数据打入到Kafka
              producer.send(new ProducerRecord[String, String](topicName, line))
              //println(line)
              lineNumber = lineNumber   1
              // 设置休眠时间
              Thread.sleep(sleepTime)
            }
          }
        }
      }
    }

    lineNumber
  }

  def KafkaConsumer(consumer: KafkaConsumer[String, String],topicName:String): Int ={

    // 定义一个变量,保存数据行数
    var lineNumber:Int = 0

    // 需要订阅 topic
    consumer.subscribe(Collections.singletonList(topicName))

    // 拉取数据
    val msgs: ConsumerRecords[String, String] = consumer.poll(2000)
    // 遍历消费数据
    val it: util.Iterator[ConsumerRecord[String, String]] = msgs.iterator()

    while (it.hasNext){
      val msg: ConsumerRecord[String, String] = it.next()
      lineNumber  =lineNumber   1
      //println(s"partition: ${msg.partition()}, offset: ${msg.offset()},value:${msg.value()}")
    }
    lineNumber
  }

  def deleteTopic(client: AdminClient,topicName:String): Unit ={
    client.deleteTopics(Collections.singletonList(topicName))
  }
}

7.2 Redis

场景示例:现有一份豆瓣读书 Top250 的文本数据,希望通过解析,将图书id和图书名存入 redis

RedisContainerTest

代码语言:javascript复制
import java.util

import com.dimafeng.testcontainers.{ForAllTestContainer, GenericContainer}
import com.hypers.util.RedisSupport
import org.scalatest.{BeforeAndAfter, FlatSpec, Matchers}
import redis.clients.jedis.{Jedis, JedisPool}

/*
 * @Author: Alice菌
 * @Date: 2020/12/16 18:01
 * @Description: 
    
 */
class RedisContainerTest extends FlatSpec with BeforeAndAfter with ForAllTestContainer with Matchers {

  var jedisPool: JedisPool = _
  var jedis: Jedis = _

  // 设置系统访问的 Docker节点 IP 和端口
  System.setProperty("DOCKER_HOST","tcp://10.16.2.103:2375")

  // 核心使用 GenericContainer 模拟redis 服务,同时使用了固定端口,使用jedis 访问
  override val  container: GenericContainer =new GenericContainer("redis:latest", exposedPorts = Seq(6379))

  "Redis Connection Test" should  "Print the result"  in {
       //  插入数据
       RedisSupport.insertData(jedis,"file/input/doubanbook.csv")
       // 根据 key 获取到 对应的 value
       val value: String = RedisSupport.getValueByKey(jedis, "3211779")
       // 断言,判断是否能获取到key对应的value
       assert(value != null)
       // 获取到所有的 key
       val set: util.Set[String] = RedisSupport.getAllKey(jedis)
       println(set)
       // 断言,判断存有 key 的set集合长度不为空
       assert(set.size()!=0)
  }

  before{
    // 连接到 Redis客户端
    jedisPool = new JedisPool(container.containerIpAddress,container.mappedPort(6379))
    jedis = jedisPool.getResource
  }

  after{
    // 关闭连接
    jedis.close()
    jedisPool.close()

  }
}

RedisSupport

代码语言:javascript复制
import java.io.{BufferedReader, FileReader}
import java.util

import redis.clients.jedis.Jedis

import scala.util.control.Breaks

/*
 * @Author: Alice菌
 * @Date: 2020/12/16 15:13
 * @Description: 
    
 */
object RedisSupport {

  def insertData(jedis: Jedis,fileName:String): Unit ={

    //  创建一个缓存流对象
    var bufferedReader:BufferedReader = null
    // 新建一个字段保存行数据
    var line:String = null
    // 创建 Breaks 对象
    val loop: Breaks = new Breaks;

    // 读取数据
    bufferedReader = new BufferedReader(new FileReader(fileName))

    loop.breakable {
      while(true){
        // 读取行数据
        line = bufferedReader.readLine()
        // 当读取不到数据时,跳出循环
        if (line == null){
          loop.break()
        }

        // 样例数据
        // 解忧杂货店,https://book.douban.com/subject/25862578/,[日] 东野圭吾 , 南海出版公司 , 2014-5 , 39.50元,8.5,一碗精心熬制的东野牌鸡汤,拒绝很难
        val bookInfoArray: Array[String] = line.split(",")
        // 获取到图书 Id
        val bookId: String = bookInfoArray(1).split("/")(4)
        // 获取到图书名
        val bookName: String = bookInfoArray(0)
        // 存入 redis
        jedis.set(bookId,bookName)
      }
    }

  }


  def getValueByKey(jedis: Jedis,key:String): String ={

    val str: String = jedis.get(key)
    str
  }

  def getAllKey(jedis: Jedis): util.Set[String] ={

    val list: util.Set[String] = jedis.keys("*")
    list

  }
}

7.3 MySQL

场景示例: 现在有一份猫眼电影 top100 热门榜单的 JSON 格式文件maoyan.txt,需要将其解析成对象,存入到MySQL数据库

MysqlContainerTest

代码语言:javascript复制
import java.sql.{Connection, DriverManager, Statement}

import com.dimafeng.testcontainers.{ForAllTestContainer, MySQLContainer}
import com.hypers.util.MysqlSupport
import org.scalatest.{BeforeAndAfter, FlatSpec, Matchers}


class MysqlContainerTest extends FlatSpec with BeforeAndAfter with ForAllTestContainer with Matchers{


  var dataCount:Int = _
  var stmt: Statement = _
  var connection: Connection = _

  // 设置系统访问的 Docker节点 IP 和端口
  System.setProperty("DOCKER_HOST","tcp://10.16.2.103:2375")

  // 启动 MySQL容器
  override val container: MySQLContainer = MySQLContainer()

  "MySQL Connection Test" should  "Print the result" in {

    // 创建表并插入数据
    val lineNumber: Int = MysqlSupport.createTableAndInsertData(stmt, "file/input/maoyan.txt")
    // 断言,判断数据条数是否为0
    assert(lineNumber!=0)
    dataCount = MysqlSupport.queryTableData(stmt)
    // 断言,判断插入的数据和查询所有的数据条数是否相等
    assert(lineNumber == dataCount)

  }

  // 测试前
  before {
    // 加载驱动
    Class.forName(container.driverClassName)
    // 获取连接
    connection = DriverManager.getConnection(container.jdbcUrl, container.username, container.password)
    stmt = connection.createStatement
  }

  // 测试后
  after{
    // 关闭连接
    stmt.close()
    connection.close()
  }
}

MysqlSupport

代码语言:javascript复制
import java.io.{BufferedReader, FileReader}
import java.sql.{ResultSet, Statement}

import com.google.gson.{JsonObject, JsonParser}

import scala.util.control.Breaks

/*
 * @Author: Alice菌
 * @Date: 2020/12/16 15:13
 * @Description: 
    
 */

object MysqlSupport {

  def createTableAndInsertData(stmt:Statement,fileName:String): Int ={
    // 定义一个变量,保存数据行数
    var lineNumber:Int = 0

    //  创建一个缓存流对象
    var bufferedReader:BufferedReader = null
    // 新建一个字段保存行数据
    var line:String = null
    // 创建 Breaks 对象
    val loop: Breaks = new Breaks;

    // 读取数据
    bufferedReader = new BufferedReader(new FileReader(fileName))

    // 执行创建表的 SQL 语句
    var ddl: String = ""

    loop.breakable {

      // 创建表
      ddl = "create table movie(`index` int,image text,title varchar(50),actor text,time varchar(20),score double)"
      stmt.execute(ddl)

      while (true) {
        // 读取行数据
        line = bufferedReader.readLine()
        // 当读取不到数据时,跳出循环
        if (line == null) {
          loop.break()
        }

        // 样例数据
        // {"index": "1", "image": "https://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg@160w_220h_1e_1c", "title": "霸王别姬", "actor": "张国荣,张丰毅,巩俐", "time": "1993-07-26", "score": "9.5"}
        val json: JsonParser = new JsonParser()
        val movie: JsonObject = json.parse(line).asInstanceOf[JsonObject]
        // val movie: Movie = JSON.parseObject(line, classOf[Movie])

        ddl = s"insert  into  movie value (${movie.get("index")},${movie.get("image")},${movie.get("title")},${movie.get("actor")},${movie.get("time")},${movie.get("score")})"
        stmt.executeUpdate(ddl)
        //  lineNumber   1
        lineNumber = lineNumber   1

      }
    }
    lineNumber
  }

  def queryTableData(stmt:Statement): Int ={
    // 定义一个变量,保存数据行数
    var lineNumber:Int = 0

    // 执行查询数据的 SQL 语句
    val sql: String = "select `index`, image, title, actor, time, score from movie"
    val rs: ResultSet = stmt.executeQuery(sql)

    while (rs.next()){
      lineNumber = lineNumber   1
    }
    lineNumber
  }
}

8、小结

本篇文章为大家从Testcontainers的概念出发,为大家较为全面的介绍了其特性和基本的使用要求,并对其包含的容器类型做了分类介绍。最后通过3个场景的实战,为大家带来了testcontainers在不同的场景下的具体应用!

到这里本篇文章就Over,你知道的越多,你不知道的也越多,我是 Alice,我们下一期见!

文章持续更新,可以微信搜一搜「 猿人菌 」第一时间阅读,思维导图,大数据书籍,大数据高频面试题,海量一线大厂面经…期待您的关注!

0 人点赞