亚欧色一区w666天堂,色情一区二区三区免费看,少妇特黄A片一区二区三区,亚洲人成网站999久久久综合,国产av熟女一区二区三区

  • 發布文章
  • 消息中心
點贊
收藏
評論
分享
原創

TestContainers

2023-03-30 08:53:41
103
0

背(bei)景

當前DTS項目(mu)使用(yong)(yong)pytest測(ce)(ce)試(shi)框架運行自(zi)(zi)動化測(ce)(ce)試(shi)用(yong)(yong)例,但由于測(ce)(ce)試(shi)環境的資源(yuan)(yuan)(yuan)(yuan)限制,只為(wei)自(zi)(zi)動化測(ce)(ce)試(shi)項目(mu)部署(shu)了(le)一個(ge)源(yuan)(yuan)(yuan)(yuan)數(shu)據庫(ku)和一個(ge)目(mu)標數(shu)據庫(ku),這導致(zhi)有(you)一些測(ce)(ce)試(shi)用(yong)(yong)例無法開(kai)發。比(bi)如MYSQL->MYSQL的版(ban)本檢(jian)查(cha)、源(yuan)(yuan)(yuan)(yuan)庫(ku)binlog存在性(xing)檢(jian)查(cha)、源(yuan)(yuan)(yuan)(yuan)庫(ku)binlog是否(fou)開(kai)啟檢(jian)查(cha)、源(yuan)(yuan)(yuan)(yuan)庫(ku)binlog影像類型檢(jian)查(cha)、源(yuan)(yuan)(yuan)(yuan)庫(ku)用(yong)(yong)戶權限檢(jian)查(cha)、源(yuan)(yuan)(yuan)(yuan)庫(ku)連(lian)通性(xing)檢(jian)查(cha)、MySQL參數(shu)lower_case_table_names一致(zhi)性(xing)檢(jian)查(cha)等等。

上面列舉(ju)的(de)(de)測試(shi)用例(li)只是開發(fa)了正向(xiang)用例(li),因(yin)為只有一套數(shu)據(ju)庫可用,無(wu)法做反向(xiang)用例(li),如果通過更改數(shu)據(ju)庫配(pei)置來滿(man)足反向(xiang)用例(li)的(de)(de)條件:

1、手工執(zhi)行(xing)重(zhong)啟(qi)(qi)命令并指(zhi)定啟(qi)(qi)動(dong)參數(shu),繁瑣而且(qie)這也(ye)不是(shi)自動(dong)化測試了(le);

2、需(xu)要的(de)時間(jian)長,拖慢自(zi)動化測(ce)試(shi)的(de)進度;

3、更改(gai)重要(yao)的參數(shu)再改(gai)回來可能(neng)會影響其(qi)他一些測例的執行(xing)。

4、數據庫(ku)版(ban)本檢查的測例只(zhi)能(neng)通過部署(shu)多套不同版(ban)本的數據庫(ku)來完成。

綜合(he)以上的(de)(de)問題,目前DTS的(de)(de)自動化(hua)測試用例還是缺少很多,只能由測試人員(yuan)手工(gong)執行(xing),這消(xiao)耗了測試人員(yuan)的(de)(de)很多時間。

 

解(jie)決(jue)方案

容器(qi)(qi)以輕量、方便而著稱,但是測試環境資源有(you)限(xian),測試用例(li)多(duo),啟動多(duo)個容器(qi)(qi)實例(li)所需的資源無法滿足(zu),手(shou)工管理也(ye)很麻(ma)煩。

如果通過編(bian)程語言(yan)遠(yuan)程啟(qi)動docker容(rong)器(qi)(qi)來代替人為操(cao)作(zuo),需要時啟(qi)動容(rong)器(qi)(qi),用完及時釋放資源(yuan)(yuan),就可(ke)以大(da)大(da)節省人力和(he)硬件(jian)資源(yuan)(yuan)。直接使用python的docker依賴(lai)來進行容(rong)器(qi)(qi)操(cao)作(zuo)是一(yi)種可(ke)能的實施(shi)方案,如下啟(qi)動一(yi)個MySQL容(rong)器(qi)(qi)。

    client = docker.from_env()
   # 啟動MySQL容器
   container: Container = client.containers.run(
       'mysql:5.7',
       command='--lower_case_table_names=1',
       detach=True,
       name='mysql-container',
       ports={'3306/tcp': 13306},
       environment={
           'MYSQL_ROOT_PASSWORD': 'password',
           'MYSQL_USER': 'user',
           'MYSQL_PASSWORD': 'password',
           'MYSQL_DATABASE': 'mydb'
      }
  )
  # 刪除(chu)容器
  container.remove()

不過開源(yuan)框架testcontainers已經封(feng)裝提供了更(geng)為方便的實(shi)現。

 

TestContainers

官網地(di)址://www.testcontainers.org

TestContainers是一個開源項目,它提(ti)供諸多可以(yi)在Docker容(rong)器中運行(xing)的組件,非(fei)常輕量、方便,需要做的只(zhi)是安裝(zhuang)docker服務,無需其(qi)他配(pei)置(zhi)。它支持Java,Python,Rust,Go,.net等多種語言,可以(yi)提(ti)供測試所需的多種環境(jing)。

testcontainers支(zhi)持(chi)眾多(duo)常用的主流組件(jian)(jian),以(yi)Java為例,支(zhi)持(chi)如下組件(jian)(jian)

 

image-20230327111058154

其中支持的Databases:

image-20230327111246510

不是所用語言的庫都支(zhi)持(chi)(chi)如(ru)此多的組件,go、node.js的testcontainers庫只支(zhi)持(chi)(chi)寥寥幾種組件。

 

 

使用介紹

使用前提:test-containers 基于 Docker,所(suo)以(yi)使用 test-container 前需要安裝(zhuang) Docker環境。

不同版(ban)本testcontainers-python的(de)(de)(de)API差異很大(da),這(zhe)里使用的(de)(de)(de)相(xiang)關(guan)依賴的(de)(de)(de)版(ban)本如下

SQLAlchemy~=1.4.46
testcontainers~=3.7.0
urllib3~=1.25.11

 

官方示例

testcontainers-python文檔地址://testcontainers-python.readthedocs.io/en/latest/README.html

testcontainers-python給出(chu)的(de)官方示例(li)代碼都很(hen)短(duan)小

def test_docker_run_mysql():
   config = MySqlContainer('mysql:5.7.17')
   with config as mysql:
       engine = sqlalchemy.create_engine(mysql.get_connection_url())
       with engine.begin() as connection:
           result = connection.execute(sqlalchemy.text("select version()"))
           for row in result:
               assert row[0].startswith('5.7.17')

以上python代碼啟(qi)動(dong)了(le)5.7.17版本(ben)的容(rong)器(qi)實(shi)例,并且(qie)使(shi)用python的ORM框架sqlalchemy去連(lian)接(jie)數據庫(ku),獲取數據庫(ku)連(lian)接(jie)后執(zhi)行SQL語句并返回結(jie)果。

 

繼(ji)承關(guan)系

事實上,所有的特性Container都(dou)直(zhi)接或(huo)間(jian)接派生自DockerContainer。以(yi)下是繼承關(guan)系(xi):

DockerContainer
|-KafkaContainer
|-ElasticSearchContainer
|-NginxContainer
|-DbContainer
|-MySqlContainer
|-SqlServerContainer
|-OracleDbContainer

DockerContainer封裝了(le)容器的(de)通(tong)用操作,DbContainer封裝了(le)數據庫的(de)通(tong)用操作。而DockerContainer底(di)層使用python的(de)docker依賴(lai)來進行(xing)容器操作的(de)。

 

DockerContainer

這是(shi)最(zui)靈活(huo)也是(shi)不太(tai)方(fang)便的(de)容器類(lei)型, 此容器允許使用啟動任何Docker鏡像。

 

DockerContainer的構造函數

    def __init__(self, image, docker_client_kw: dict = None, **kwargs):
       self.env = {}
       self.ports = {}
       self.volumes = {}
       self.image = image
       self._docker = DockerClient(**(docker_client_kw or {}))
       self._container = None
       self._command = None
       self._name = None
       self._kwargs = kwargs

with_env方法用于設置容器的環境變量

    def with_env(self, key: str, value: str) -> 'DockerContainer':
       self.env[key] = value
       return self

with_command方法指定容器啟動(dong)時的參數

    def with_command(self, command: str) -> 'DockerContainer':
       self._command = command
       return self

with_bind_ports用于設置一對綁(bang)定的(de)端口

    def with_bind_ports(self, container: int,
                       host: int = None) -> 'DockerContainer':
       self.ports[container] = host
       return self

with_exposed_ports方法用于暴露一個內部端(duan)口,綁定的(de)宿主機(ji)端(duan)口是隨機(ji)的(de)

    def with_exposed_ports(self, *ports) -> 'DockerContainer':
       for port in list(ports):
           self.ports[port] = None
       return self

get_exposed_port方法用(yong)來獲取指定(ding)的內(nei)部端(duan)口所綁定(ding)的宿主機端(duan)口

    @wait_container_is_ready()
   def get_exposed_port(self, port) -> str:
       mapped_port = self.get_docker_client().port(self._container.id, port)
       if inside_container():
           gateway_ip = self.get_docker_client().gateway_ip(self._container.id)
           host = self.get_docker_client().host()
?
           if gateway_ip == host:
               return port
       return mapped_port

with_volume_mapping方法用于掛(gua)載數據卷

    def with_volume_mapping(self, host: str, container: str,
                           mode: str = 'ro') -> 'DockerContainer':
       # '/home/user1/': {'bind': '/mnt/vol2', 'mode': 'rw'}
       mapping = {'bind': container, 'mode': mode}
       self.volumes[host] = mapping
       return self

exec方法用于在容(rong)器內(nei)部執行指(zhi)令

    def exec(self, command):
       if not self._container:
           raise ContainerStartException("Container should be started before")
       return self.get_wrapped_container().exec_run(command)

get_wrapped_container方法返回container對(dui)象(xiang),這才(cai)真(zhen)正對(dui)應著一(yi)個(ge)里面有所有的(de)容器信息

    def get_wrapped_container(self) -> Container:
       return self._container

 

可以使用DockerContainer啟(qi)動(dong)任何類型的容(rong)器,比如(ru)啟(qi)動(dong)一個MySQL容(rong)器

    container = DockerContainer("mysql:5.7")
   container.with_env("MYSQL_ROOT_PASSWORD", "afcer554KCJ5")
   container.with_command("--lower_case_table_names=1")
   container.with_exposed_ports(3306)
   container.start()

事實上,MysqlContainer與DockerContainer差(cha)異(yi)很小,MysqlContainer只是(shi)(shi)多做了一點事情(設置環境變量(liang)、檢查容器(qi)是(shi)(shi)否啟動(dong)就(jiu)緒),自動(dong)化測試直接使用DockerContainer也能滿足需求,當然使用MysqlContainer會更加方便一些。

 

MysqlContainer

MySqlContainer繼承自DbContainer,DbContainer繼承自DockerContainer。

MysqlContainer構造函數(shu)

    def __init__(self,
                image="mysql:latest",
                MYSQL_USER=None,
                MYSQL_ROOT_PASSWORD=None,
                MYSQL_PASSWORD=None,
                MYSQL_DATABASE=None,
                **kwargs):
       super(MySqlContainer, self).__init__(image, **kwargs)
       self.port_to_expose = 3306
       self.with_exposed_ports(self.port_to_expose)
       self.MYSQL_USER = MYSQL_USER or environ.get('MYSQL_USER', 'test')
       self.MYSQL_ROOT_PASSWORD = MYSQL_ROOT_PASSWORD or environ.get('MYSQL_ROOT_PASSWORD', 'test')
       self.MYSQL_PASSWORD = MYSQL_PASSWORD or environ.get('MYSQL_PASSWORD', 'test')
       self.MYSQL_DATABASE = MYSQL_DATABASE or environ.get('MYSQL_DATABASE', 'test')
?
       if self.MYSQL_USER == 'root':
           self.MYSQL_ROOT_PASSWORD = self.MYSQL_PASSWORD

鏡像默認是latest,最新版。

 

啟動容器(qi)

創建一(yi)個MysqlContainer對(dui)象后,只(zhi)需簡單調用(yong)start方法即可啟(qi)動容器(qi)

    # 啟(qi)動容(rong)器
  mysqlContainer.start()

start方(fang)法(fa)的(de)定義(yi)

class DbContainer(DockerContainer):
?
   @wait_container_is_ready(*ADDITIONAL_TRANSIENT_ERRORS)
   def _connect(self):
       import sqlalchemy
       engine = sqlalchemy.create_engine(self.get_connection_url())
       engine.connect()
?
   def start(self):
  # 配(pei)置環境變(bian)量
       self._configure()
       # 這一步就是把(ba)容(rong)器啟(qi)動起(qi)來
       super().start()
       # 驗證容器服務是(shi)否可用
       self._connect()
       return self
?

在start方法里的(de)最后調用_connect()方法是為了驗(yan)證容器服務是否可(ke)用。_connect方(fang)法使用SQLAlchemy來(lai)連接數據庫(ku),SQLAlchemy通(tong)過(guo)容器(qi)的get_connection_url()方(fang)法獲取數據庫(ku)地址進(jin)行連接。_connect方法添(tian)加(jia)了@wait_container_is_ready注解,會一直等待直到數(shu)據(ju)庫連接成功(gong),超(chao)時時間(jian)120s。

SQLAlchemy是一個開源的Python ORM(對(dui)象(xiang)關系映射)框架。

 

在(zai)windows上(shang)進行(xing)測試時(shi),MysqlContainer的get_connection_url方法返回如下的URL對(dui)象

mysql+pymysql://test_asd:afrvte54657hnngf@localnpipe:65303/test

PostgresContainer的get_connection_url方(fang)法返回如下的URL對象(xiang)

postgresql+psycopg2://postgres:***@localnpipe:64211/postgres

這(zhe)里顯(xian)然是通(tong)過named pipe來連接數(shu)據庫,然而連接失敗次(ci)數(shu)達到指(zhi)定(ding)值(zhi),容器啟動失敗。

Windows命名管(guan)道只(zhi)能用于Windows主機(ji)上(shang)的(de)進程(cheng)間通信,WSL上(shang)運(yun)行(xing)(xing)的(de)Docker容器被視為獨立(li)的(de)進程(cheng)空間,因此無法通過命名管(guan)道進行(xing)(xing)通信。與WSL2中(zhong)運(yun)行(xing)(xing)的(de)Docker容器進行(xing)(xing)通信,需要使用網絡(luo)通信協議,如TCP協議。

即使(shi)我在Windows本地啟動MySQL服務,使(shi)用SQLAlchemy連接以上的(de)URL對(dui)象也是失敗。

 

閱讀了(le)testcontainers-python源碼,通(tong)過繼(ji)承MySqlContainer重寫其get_connection_url()來解決。

from testcontainers.mysql import MySqlContainer
?
?
class CustomMysqlContainer(MySqlContainer):
   def get_connection_url(self):
       return 'mysql+pymysql://{0}:{1}@127.0.0.1:{2}/{3}'.format('root',
                                                                 self.MYSQL_ROOT_PASSWORD,
                                                                 self.get_exposed_port(3306),
                                                                 self.MYSQL_DATABASE)

將host寫死為(wei)127.0.0.1,這樣即(ji)可解(jie)決。

 

而(er)testcontainers-java則是使用localhost連接的:

19:09:15.663 [main] INFO ?? [mysql:5.7.34] - Container mysql:5.7.34 is starting: 18f8abf99e470a467d86d5b37c09be2b7cfd94e5b270d7e633b001fbb3db2ae2
19:09:16.095 [main] INFO ?? [mysql:5.7.34] - Waiting for database connection to become available at jdbc:mysql://localhost:64513/test using query 'SELECT 1'
19:09:24.280 [main] INFO ?? [mysql:5.7.34] - Container is started (JDBC URL: jdbc:mysql://localhost:64513/test)
19:09:24.281 [main] INFO ?? [mysql:5.7.34] - Container mysql:5.7.34 started in PT8.7804708S

 

 

 

創建(jian)mysql賬號

MysqlContainer構造函(han)數(shu)中,可以指定(ding)參數(shu)MYSQL_USER、MYSQL_PASSWORD、MYSQL_ROOT_PASSWORD、MYSQL_DATABASE來創建(jian)賬戶相(xiang)關信息。

    mysqlContainer = MysqlContainer(image='mysql:5.7',
                             MYSQL_USER='test_asd',
                             MYSQL_ROOT_PASSWORD='afrvte54657hnngf',
                             MYSQL_PASSWORD='afrvte54657hnngf',
                             MYSQL_DATABASE='test')

在(zai)容(rong)器(qi)啟動過(guo)程中看到如下日志

[Warning] root@localhost is created with an empty password ! Please consider switching off the --initialize-insecure option.

MySQL初始化(hua)時root@localhost用(yong)(yong)(yong)戶(hu)(hu)是空(kong)密(mi)(mi)碼。如果設(she)置了(le)MYSQL_ROOT_PASSWORD,容器會在MySQL服務運行后立即設(she)置root@localhost用(yong)(yong)(yong)戶(hu)(hu)密(mi)(mi)碼,并創建一個新用(yong)(yong)(yong)戶(hu)(hu)root@%,兩者密(mi)(mi)碼一致。

 

指定端口

MySQL容器內的(de)MySQL服務默認運(yun)行在3306號端口,綁定的(de)宿主機(ji)端口是隨機(ji)的(de)。

可以(yi)在(zai)容器啟動前將其綁定到指定的(de)宿主(zhu)機端口(kou),例如

    mysqlContainer = MysqlContainer(image='mysql:5.7',
                             MYSQL_USER='test_asd',
                             MYSQL_ROOT_PASSWORD='afrvte54657hnngf',
                             MYSQL_PASSWORD='afrvte54657hnngf',
                             MYSQL_DATABASE='test')
   mysqlContainer.with_bind_ports(3306, 26788)

而with_exposed_ports方法也用于暴露指定(ding)的(de)內部端口,但綁(bang)定(ding)的(de)宿主機端口是隨機的(de)。

使用(yong)with_bind_ports方法綁定(ding)指定(ding)的宿主機(ji)(ji)端(duan)口(kou)需要確保宿主機(ji)(ji)該端(duan)口(kou)空閑,而with_exposed_ports會自動尋找宿主機(ji)(ji)上空閑的一(yi)個可用(yong)端(duan)口(kou)。

 

指定啟(qi)動參數(shu)

如果(guo)想(xiang)要指(zhi)定(ding)MySQL的運行參數(shu),可以在容(rong)器啟動前使用with_command方法來指(zhi)定(ding)MySQL參數(shu),方法的參數(shu)格式如下

    mysqlContainer = MysqlContainer(image='mysql:5.7',
                             MYSQL_USER='test_asd',
                             MYSQL_ROOT_PASSWORD='afrvte54657hnngf',
                             MYSQL_PASSWORD='afrvte54657hnngf',
                             MYSQL_DATABASE='test')
   mysqlContainer.with_command('--lower-case-table-names=1')
   # 將把上面設置的command覆蓋
   mysqlContainer.with_command('--character_set_server=utf8mb4')
   # 想要同時(shi)設置多(duo)個MySQL服務啟動參數,按如(ru)下格式傳遞參數
   mysqlContainer.with_command('--lower-case-table-names=1 --character_set_server=utf8mb4')

進入容(rong)器查(cha)看

image-20230301183941451

 

 

容器操作

DockerContainer提供了start和stop方法(fa)(fa)(fa),每(mei)次調用start方法(fa)(fa)(fa)就會啟動一個(ge)新容器實(shi)例并返回,stop方法(fa)(fa)(fa)不是停止(zhi)容器而是刪除容器。

    def start(self):
       logger.info("Pulling image %s", self.image)
       docker_client = self.get_docker_client()
       self._container = docker_client.run(self.image,
                                           command=self._command,
                                           detach=True,
                                           environment=self.env,
                                           ports=self.ports,
                                           name=self._name,
                                           volumes=self.volumes,
                                           **self._kwargs
                                          )
       logger.info("Container started: %s", self._container.short_id)
       return self
?
   def stop(self, force=True, delete_volume=True):
       self.get_wrapped_container().remove(force=force, v=delete_volume)

為(wei)了避免容器一(yi)直運行,容器使(shi)用完后一(yi)定要調用DockerContainer的stop方法刪(shan)除容器實例。

DockerContainer封裝了具體(ti)的容(rong)器實例,提(ti)供了一些工(gong)具方法(fa)如(ru)with_env、with_command,它的實例變量_container才對(dui)應著一個具體(ti)的容器實例(li)。

_container是Container類型的對象(xiang),是python的docker依賴(lai)提供的。

class Container(Model):
   """ Local representation of a container object. Detailed configuration may
      be accessed through the :py:attr:`attrs` attribute. Note that local
      attributes are cached; users may call :py:meth:`reload` to
      query the Docker daemon for the current properties, causing
      :py:attr:`attrs` to be refreshed.
  """
   
   //...
   
   def pause(self):
       """
      Pauses all processes within this container..
      """
       return self.client.api.pause(self.id)
?
   
   def remove(self, **kwargs):
       """
      Remove this container. Similar to the ``docker rm`` command.
      """
       return self.client.api.remove_container(self.id, **kwargs)
   
   
   def start(self, **kwargs):
       """
      Start this container. Similar to the ``docker start`` command, but
      """
       return self.client.api.start(self.id, **kwargs)
   
   
   def stop(self, **kwargs):
       """
      Stops a container. Similar to the ``docker stop`` command.
      """
       return self.client.api.stop(self.id, **kwargs)
   
   
   def unpause(self):
       """
      Unpause all processes within the container.
      """
       return self.client.api.unpause(self.id)

想要(yao)對容器實例進行(xing)其(qi)他操(cao)作,可以通(tong)過DockerContainer的(de)get_wrapped_container方(fang)法獲取_container實例(li)變量。

    container = mysqlContainer.get_wrapped_container()
   #停止活動
   container.pause()
   #繼(ji)續活動(dong)
   container.unpause()

我們可(ke)以調用(yong)pause方法來模擬數據庫不可(ke)用(yong)狀態。

 

性能表現和資源占(zhan)用

測試(shi)環(huan)境:Windows WSL2+Windows docker desktop

啟動(dong)容器(qi)(qi)的(de)(de)start方法是同(tong)步的(de)(de),在等(deng)待容器(qi)(qi)完全(quan)啟動(dong)后才會返回,這里的(de)(de)容器(qi)(qi)啟動(dong)時間7.7s

image-20230301180134161

內存占用

image-20230301183539867

使用(yong)testcontainers-python創建(jian)容器時,推(tui)薦使用(yong)官方的Docker鏡像(xiang),是經過優化(hua)和(he)配置的,因(yin)此(ci)占用(yong)的內存資源是相對較少的。

第一(yi)次啟動容器需要拉取鏡像(xiang)耗時會比(bi)較(jiao)久(jiu)。

 

 

控制容器啟動超(chao)時(shi)時(shi)間

在start方法里,testcontainers通(tong)過連接(jie)數據庫來(lai)判斷服務是否可用。testcontainers默(mo)認設置的最大連接(jie)重試次數為120次,間隔1s。

os.environ.setdefault("TC_MAX_TRIES", '120')

調(diao)試階段可以(yi)適當調(diao)小此值(zhi)。

雖然單個(ge)容(rong)器(qi)啟動速度(du)很快,測試發現(xian),如果連(lian)續(xu)啟動4個(ge)MySQL容(rong)器(qi),容(rong)器(qi)的啟動時間超(chao)過甚至會超(chao)過120s導致(zhi)測試失(shi)敗(偶爾)。

 

 

PostgresContainer

指定參(can)數

postgresContainer.with_command("-c wal_level=logical -c max_connections=134 -c shared_buffers=2GB")

資源占用

image-20230307093509025

 

 

docker-compose

testcontainers也支(zhi)持(chi)使用docker-compose。

 

創建docker-compose.yml文件,啟(qi)動(dong)一(yi)個MySQL和一(yi)個postgres。

version: '3'
services:
db:
  image: mysql:5.7
  environment:
    MYSQL_ROOT_PASSWORD: root
    MYSQL_DATABASE: test_db
    MYSQL_USER: test_user
    MYSQL_PASSWORD: test_password
  ports:
    - "13306:3306"
postgres:
  image: postgres:13
  environment:
    POSTGRES_DB: test_db
    POSTGRES_USER: test_user
    POSTGRES_PASSWORD: test_password
  ports:
    - "15432:5432"

創建(jian)DockerCompose,指定docker-compose文件在當前(qian)目錄下,啟動DockerCompose

from testcontainers.compose import DockerCompose
?
?
def test_compose():
   compose = DockerCompose('.')
   compose.start()
?
   mysql_host = compose.get_service_host('db', 3306)
   print(mysql_host)
   mysql_port = compose.get_service_port('db', 3306)
   print(mysql_port)
?
   '''
      Returns tuple[str, str, int] stdout, stderr, return code
  '''
   return_tuple = compose.exec_in_container('db', ['ls'])
   print(return_tuple)
?
   time.sleep(1)
?
   compose.stop()

 

 

 

testcontainers-java

@Testcontainers: 用于啟用 Testcontainers 支持。它可以用在(zai)類或方法級別,讓 JUnit 在(zai)測試執(zhi)行(xing)前啟動 Docker 容器。例(li)如:

@Testcontainers
public class MyTestClass {
// ...
}

@Container: 用于將 Docker 容器作為測(ce)試(shi)類的(de)靜態字段啟動。這個注(zhu)解可以與 DockerComposeContainerGenericContainer 和(he)其他擴(kuo)展類一起使用(yong)。例如(ru):

@Container
public static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer();

 

 

@Testcontainers
public class MyTest {
   
@Container
   private static final MySQLContainer mySQLContainer = (MySQLContainer) new MySQLContainer("mysql:5.7.34")
          .withDatabaseName("test")
          .withUsername("dds")
          .withPassword("asfrer54765dsc")
          .withInitScript("mysqlScript.sql")
      .withEnv("name","value")
          .withCommand("--lower-case-table-names=1 --character_set_server=utf8mb4");
   
   @Test
   public void testMysql() throws SQLException {
       System.out.println(mySQLContainer.getMappedPort(3306));
       try (Connection conn = DriverManager.getConnection(mySQLContainer.getJdbcUrl(), mySQLContainer.getUsername(), mySQLContainer.getPassword())) {
           String sql = "SELECT 2 + 2";
           try (PreparedStatement stmt = conn.prepareStatement(sql)) {
               try (ResultSet rs = stmt.executeQuery()) {
                   rs.next();
                   int result = rs.getInt(1);
                   Assertions.assertEquals(4, result);
             ; }
          }
      }
  }

withPassword方法設置的(de)密碼也是root用戶的(de)密碼;

withInitScript可指定MySQL的初(chu)始化(hua)腳本;

withCommand可指定(ding)MySQL參數(shu);

withEnv設(she)置容器環境(jing)變量;

getJdbcUrl返(fan)回JDBC連接urljdbc:mysql://localhost:51319/test

getMappedPort方法返回容(rong)器內(nei)部(bu)端(duan)口(kou)所綁定的宿主機端(duan)口(kou)。

 

 

 

0條評論
0 / 1000
don
5文章(zhang)數
0粉絲數
don
5 文章(zhang) | 0 粉(fen)絲
原創

TestContainers

2023-03-30 08:53:41
103
0

背景

當前DTS項目(mu)使用(yong)(yong)pytest測試框架運行自動(dong)化測試用(yong)(yong)例,但由于測試環境的資源(yuan)限制(zhi),只(zhi)為(wei)自動(dong)化測試項目(mu)部署了(le)一個(ge)源(yuan)數(shu)(shu)據(ju)庫(ku)(ku)(ku)和(he)一個(ge)目(mu)標數(shu)(shu)據(ju)庫(ku)(ku)(ku),這導致(zhi)有(you)一些測試用(yong)(yong)例無(wu)法開發。比如(ru)MYSQL->MYSQL的版(ban)本檢(jian)查、源(yuan)庫(ku)(ku)(ku)binlog存在性(xing)檢(jian)查、源(yuan)庫(ku)(ku)(ku)binlog是否開啟檢(jian)查、源(yuan)庫(ku)(ku)(ku)binlog影像類型檢(jian)查、源(yuan)庫(ku)(ku)(ku)用(yong)(yong)戶權限檢(jian)查、源(yuan)庫(ku)(ku)(ku)連通性(xing)檢(jian)查、MySQL參數(shu)(shu)lower_case_table_names一致(zhi)性(xing)檢(jian)查等等。

上面(mian)列舉的測試用(yong)例(li)只是開(kai)發了正向用(yong)例(li),因為(wei)只有(you)一(yi)套數據庫可用(yong),無法(fa)做反向用(yong)例(li),如(ru)果(guo)通過更改數據庫配置來(lai)滿足(zu)反向用(yong)例(li)的條(tiao)件:

1、手工執行重啟命令并指定啟動參數,繁瑣(suo)而(er)且這也(ye)不是自動化測試了;

2、需要的時間長,拖慢自(zi)動化測試的進度;

3、更(geng)改(gai)重要的參數再改(gai)回來可能(neng)會影(ying)響其他一些(xie)測(ce)例的執行。

4、數據(ju)庫(ku)(ku)版本檢(jian)查的(de)測例只能通過部署多套不同(tong)版本的(de)數據(ju)庫(ku)(ku)來完成。

綜合以(yi)上的問題,目前DTS的自動(dong)化測(ce)試(shi)(shi)用例還是缺(que)少很(hen)多,只能由測(ce)試(shi)(shi)人員手工執(zhi)行,這(zhe)消耗了測(ce)試(shi)(shi)人員的很(hen)多時間。

 

解(jie)決方案

容(rong)器以輕(qing)量、方便(bian)而(er)著(zhu)稱,但(dan)是測(ce)試環境資源有限,測(ce)試用(yong)例多,啟動多個容(rong)器實例所需的資源無法滿足,手工管理也很麻煩。

如(ru)果通過編程(cheng)語(yu)言遠程(cheng)啟動docker容(rong)(rong)器來(lai)代替人(ren)為操作(zuo),需要時(shi)(shi)啟動容(rong)(rong)器,用完及時(shi)(shi)釋(shi)放資(zi)源(yuan),就可以大(da)大(da)節省(sheng)人(ren)力(li)和硬件(jian)資(zi)源(yuan)。直接使用python的docker依賴來(lai)進行容(rong)(rong)器操作(zuo)是一種可能的實施方案(an),如(ru)下啟動一個MySQL容(rong)(rong)器。

    client = docker.from_env()
   # 啟動MySQL容(rong)器(qi)
   container: Container = client.containers.run(
       'mysql:5.7',
       command='--lower_case_table_names=1',
       detach=True,
       name='mysql-container',
       ports={'3306/tcp': 13306},
       environment={
           'MYSQL_ROOT_PASSWORD': 'password',
           'MYSQL_USER': 'user',
           'MYSQL_PASSWORD': 'password',
           'MYSQL_DATABASE': 'mydb'
      }
  )
  # 刪除容器
  container.remove()

不過開源框(kuang)架(jia)testcontainers已(yi)經封裝提供了(le)更為方(fang)便的實現。

 

TestContainers

官網地(di)址://www.testcontainers.org

TestContainers是一個開(kai)源項目(mu),它提(ti)供諸多(duo)可以在Docker容器中運行的組件(jian),非常(chang)輕量(liang)、方(fang)便,需要做(zuo)的只是安裝docker服務,無需其他(ta)配置。它支持Java,Python,Rust,Go,.net等多(duo)種語言,可以提(ti)供測試所需的多(duo)種環境。

testcontainers支持眾多常用的主流組(zu)件(jian),以(yi)Java為例,支持如下(xia)組(zu)件(jian)

 

image-20230327111058154

其中支持(chi)的(de)Databases:

image-20230327111246510

不是(shi)所用語言(yan)的(de)庫都支持如此多的(de)組件,go、node.js的(de)testcontainers庫只支持寥(liao)寥(liao)幾種組件。

 

 

使用介(jie)紹

使用前提:test-containers 基(ji)于 Docker,所以使用 test-container 前需要安裝(zhuang) Docker環境。

不同版(ban)本testcontainers-python的API差異很大,這里使用的相(xiang)關依賴的版(ban)本如下

SQLAlchemy~=1.4.46
testcontainers~=3.7.0
urllib3~=1.25.11

 

官方示例

testcontainers-python文檔地址://testcontainers-python.readthedocs.io/en/latest/README.html

testcontainers-python給出的(de)官方(fang)示例代(dai)碼都(dou)很短小

def test_docker_run_mysql():
   config = MySqlContainer('mysql:5.7.17')
   with config as mysql:
       engine = sqlalchemy.create_engine(mysql.get_connection_url())
       with engine.begin() as connection:
           result = connection.execute(sqlalchemy.text("select version()"))
           for row in result:
               assert row[0].startswith('5.7.17')

以上python代(dai)碼啟動了5.7.17版本的容器(qi)實(shi)例,并且使(shi)用python的ORM框架sqlalchemy去連接數據庫,獲取數據庫連接后執行SQL語句并返(fan)回結果。

 

繼承(cheng)關(guan)系

事實上,所有的特性(xing)Container都直接(jie)(jie)或間接(jie)(jie)派生自DockerContainer。以下是繼承關(guan)系:

DockerContainer
|-KafkaContainer
|-ElasticSearchContainer
|-NginxContainer
|-DbContainer
|-MySqlContainer
|-SqlServerContainer
|-OracleDbContainer

DockerContainer封裝了(le)容(rong)器(qi)的(de)(de)通用(yong)操作(zuo)(zuo),DbContainer封裝了(le)數據庫(ku)的(de)(de)通用(yong)操作(zuo)(zuo)。而DockerContainer底層使(shi)用(yong)python的(de)(de)docker依(yi)賴來進行容(rong)器(qi)操作(zuo)(zuo)的(de)(de)。

 

DockerContainer

這是(shi)最靈活(huo)也是(shi)不太方便(bian)的容器類型, 此容器允許(xu)使用啟動任何Docker鏡像(xiang)。

 

DockerContainer的構造函數

    def __init__(self, image, docker_client_kw: dict = None, **kwargs):
       self.env = {}
       self.ports = {}
       self.volumes = {}
       self.image = image
       self._docker = DockerClient(**(docker_client_kw or {}))
       self._container = None
       self._command = None
       self._name = None
       self._kwargs = kwargs

with_env方(fang)法用于設置容器的環(huan)境變量

    def with_env(self, key: str, value: str) -> 'DockerContainer':
       self.env[key] = value
       return self

with_command方法(fa)指定(ding)容器啟動時的參數(shu)

    def with_command(self, command: str) -> 'DockerContainer':
       self._command = command
       return self

with_bind_ports用于設(she)置一(yi)對綁(bang)定(ding)的端口(kou)

    def with_bind_ports(self, container: int,
                       host: int = None) -> 'DockerContainer':
       self.ports[container] = host
       return self

with_exposed_ports方(fang)法用于暴(bao)露一個內部(bu)端口,綁定的(de)宿(su)主機端口是隨機的(de)

    def with_exposed_ports(self, *ports) -> 'DockerContainer':
       for port in list(ports):
           self.ports[port] = None
       return self

get_exposed_port方(fang)法用來獲(huo)取指定的內部端(duan)口(kou)所綁(bang)定的宿主機(ji)端(duan)口(kou)

    @wait_container_is_ready()
   def get_exposed_port(self, port) -> str:
       mapped_port = self.get_docker_client().port(self._container.id, port)
       if inside_container():
           gateway_ip = self.get_docker_client().gateway_ip(self._container.id)
           host = self.get_docker_client().host()
?
           if gateway_ip == host:
               return port
       return mapped_port

with_volume_mapping方法(fa)用于掛載數據(ju)卷

    def with_volume_mapping(self, host: str, container: str,
                           mode: str = 'ro') -> 'DockerContainer':
       # '/home/user1/': {'bind': '/mnt/vol2', 'mode': 'rw'}
       mapping = {'bind': container, 'mode': mode}
       self.volumes[host] = mapping
       return self

exec方法(fa)用于在容器內部執行指令

    def exec(self, command):
       if not self._container:
           raise ContainerStartException("Container should be started before")
       return self.get_wrapped_container().exec_run(command)

get_wrapped_container方(fang)法返回container對(dui)(dui)象,這(zhe)才真正對(dui)(dui)應著(zhu)一個里面有(you)所有(you)的容器信息

    def get_wrapped_container(self) -> Container:
       return self._container

 

可以(yi)使用(yong)DockerContainer啟(qi)動任何(he)類型的容器(qi),比(bi)如啟(qi)動一個MySQL容器(qi)

    container = DockerContainer("mysql:5.7")
   container.with_env("MYSQL_ROOT_PASSWORD", "afcer554KCJ5")
   container.with_command("--lower_case_table_names=1")
   container.with_exposed_ports(3306)
   container.start()

事實上(shang),MysqlContainer與(yu)DockerContainer差(cha)異很(hen)小(xiao),MysqlContainer只是多做(zuo)了(le)一點(dian)事情(設置環境變量、檢查容(rong)器是否啟動就緒),自動化(hua)測試(shi)直(zhi)接(jie)使用(yong)DockerContainer也能(neng)滿足需求,當然使用(yong)MysqlContainer會更加方便一些。

 

MysqlContainer

MySqlContainer繼承自(zi)DbContainer,DbContainer繼承自(zi)DockerContainer。

MysqlContainer構造(zao)函數

    def __init__(self,
                image="mysql:latest",
                MYSQL_USER=None,
                MYSQL_ROOT_PASSWORD=None,
                MYSQL_PASSWORD=None,
                MYSQL_DATABASE=None,
                **kwargs):
       super(MySqlContainer, self).__init__(image, **kwargs)
       self.port_to_expose = 3306
       self.with_exposed_ports(self.port_to_expose)
       self.MYSQL_USER = MYSQL_USER or environ.get('MYSQL_USER', 'test')
       self.MYSQL_ROOT_PASSWORD = MYSQL_ROOT_PASSWORD or environ.get('MYSQL_ROOT_PASSWORD', 'test')
       self.MYSQL_PASSWORD = MYSQL_PASSWORD or environ.get('MYSQL_PASSWORD', 'test')
       self.MYSQL_DATABASE = MYSQL_DATABASE or environ.get('MYSQL_DATABASE', 'test')
?
       if self.MYSQL_USER == 'root':
           self.MYSQL_ROOT_PASSWORD = self.MYSQL_PASSWORD

鏡像默認是latest,最新版。

 

啟(qi)動容器

創(chuang)建一個MysqlContainer對(dui)象后,只需簡單調(diao)用start方(fang)法即可啟動(dong)容(rong)器

    # 啟動容(rong)器
  mysqlContainer.start()

start方法(fa)的(de)定義(yi)

class DbContainer(DockerContainer):
?
   @wait_container_is_ready(*ADDITIONAL_TRANSIENT_ERRORS)
   def _connect(self):
       import sqlalchemy
       engine = sqlalchemy.create_engine(self.get_connection_url())
       engine.connect()
?
   def start(self):
  # 配置(zhi)環境(jing)變(bian)量
       self._configure()
       # 這一步就是把容器(qi)啟動(dong)起來
       super().start()
       # 驗證容器(qi)服務是否可用
       self._connect()
       return self
?

在start方法里的最后調用_connect()方法是為了驗(yan)證(zheng)容器服務是否可用。_connect方法使用(yong)SQLAlchemy來(lai)連(lian)接數據庫,SQLAlchemy通過容器的get_connection_url()方法獲取(qu)數據庫地(di)址進行(xing)連(lian)接。_connect方(fang)法添加了@wait_container_is_ready注解,會一(yi)直等(deng)待直到(dao)數據庫連接成功(gong),超時時間120s。

SQLAlchemy是一個(ge)開源(yuan)的Python ORM(對(dui)象關系映(ying)射(she))框(kuang)架。

 

在windows上進行測(ce)試時,MysqlContainer的get_connection_url方法(fa)返回如下的URL對(dui)象(xiang)

mysql+pymysql://test_asd:afrvte54657hnngf@localnpipe:65303/test

PostgresContainer的get_connection_url方法返回(hui)如下的URL對象

postgresql+psycopg2://postgres:***@localnpipe:64211/postgres

這里顯然(ran)(ran)是通過(guo)named pipe來連接(jie)數據庫(ku),然(ran)(ran)而連接(jie)失(shi)敗次數達到指定值,容器啟動失(shi)敗。

Windows命名(ming)管道(dao)只能用于Windows主機上的(de)進(jin)(jin)程間(jian)通(tong)信(xin)(xin)(xin),WSL上運行的(de)Docker容器(qi)被(bei)視為獨(du)立的(de)進(jin)(jin)程空間(jian),因(yin)此無法通(tong)過(guo)命名(ming)管道(dao)進(jin)(jin)行通(tong)信(xin)(xin)(xin)。與WSL2中運行的(de)Docker容器(qi)進(jin)(jin)行通(tong)信(xin)(xin)(xin),需要使(shi)用網絡(luo)通(tong)信(xin)(xin)(xin)協議(yi),如(ru)TCP協議(yi)。

即(ji)使我在Windows本地啟動MySQL服務,使用SQLAlchemy連接以上的URL對(dui)象(xiang)也是失敗。

 

閱讀(du)了testcontainers-python源(yuan)碼(ma),通過(guo)繼承MySqlContainer重寫(xie)其get_connection_url()來(lai)解決。

from testcontainers.mysql import MySqlContainer
?
?
class CustomMysqlContainer(MySqlContainer):
   def get_connection_url(self):
       return 'mysql+pymysql://{0}:{1}@127.0.0.1:{2}/{3}'.format('root',
                                                                 self.MYSQL_ROOT_PASSWORD,
                                                                 self.get_exposed_port(3306),
                                                                 self.MYSQL_DATABASE)

將(jiang)host寫(xie)死為127.0.0.1,這樣即可解決。

 

而(er)testcontainers-java則是(shi)使用(yong)localhost連接的(de):

19:09:15.663 [main] INFO ?? [mysql:5.7.34] - Container mysql:5.7.34 is starting: 18f8abf99e470a467d86d5b37c09be2b7cfd94e5b270d7e633b001fbb3db2ae2
19:09:16.095 [main] INFO ?? [mysql:5.7.34] - Waiting for database connection to become available at jdbc:mysql://localhost:64513/test using query 'SELECT 1'
19:09:24.280 [main] INFO ?? [mysql:5.7.34] - Container is started (JDBC URL: jdbc:mysql://localhost:64513/test)
19:09:24.281 [main] INFO ?? [mysql:5.7.34] - Container mysql:5.7.34 started in PT8.7804708S

 

 

 

創建mysql賬號

MysqlContainer構造函數(shu)中,可(ke)以指定參(can)數(shu)MYSQL_USER、MYSQL_PASSWORD、MYSQL_ROOT_PASSWORD、MYSQL_DATABASE來創建賬戶(hu)相關信息。

    mysqlContainer = MysqlContainer(image='mysql:5.7',
                             MYSQL_USER='test_asd',
                             MYSQL_ROOT_PASSWORD='afrvte54657hnngf',
                             MYSQL_PASSWORD='afrvte54657hnngf',
                             MYSQL_DATABASE='test')

在容器啟動過(guo)程中看到如下日(ri)志

[Warning] root@localhost is created with an empty password ! Please consider switching off the --initialize-insecure option.

MySQL初始化時root@localhost用戶(hu)(hu)是空密碼(ma)。如果設(she)置了MYSQL_ROOT_PASSWORD,容(rong)器會在MySQL服務(wu)運行(xing)后立即設(she)置root@localhost用戶(hu)(hu)密碼(ma),并創建(jian)一個新(xin)用戶(hu)(hu)root@%,兩者密碼(ma)一致。

 

指定端口

MySQL容器內的(de)(de)(de)MySQL服務默認運行在3306號(hao)端(duan)口,綁定的(de)(de)(de)宿主機(ji)端(duan)口是隨機(ji)的(de)(de)(de)。

可(ke)以在容器啟動前(qian)將(jiang)其綁定到指定的宿主機端口(kou),例(li)如

    mysqlContainer = MysqlContainer(image='mysql:5.7',
                             MYSQL_USER='test_asd',
                             MYSQL_ROOT_PASSWORD='afrvte54657hnngf',
                             MYSQL_PASSWORD='afrvte54657hnngf',
                             MYSQL_DATABASE='test')
   mysqlContainer.with_bind_ports(3306, 26788)

而with_exposed_ports方法也用于暴(bao)露(lu)指定的內部端(duan)口,但(dan)綁定的宿主機端(duan)口是隨機的。

使用with_bind_ports方法(fa)綁定指定的(de)宿(su)主(zhu)機端口(kou)需要(yao)確保(bao)宿(su)主(zhu)機該端口(kou)空閑,而with_exposed_ports會自動尋找宿(su)主(zhu)機上空閑的(de)一(yi)個可用端口(kou)。

 

指(zhi)定(ding)啟(qi)動參數

如(ru)果想要指(zhi)定(ding)(ding)MySQL的(de)運行(xing)參數(shu)(shu),可(ke)以在容器(qi)啟動前(qian)使用with_command方(fang)法(fa)(fa)來指(zhi)定(ding)(ding)MySQL參數(shu)(shu),方(fang)法(fa)(fa)的(de)參數(shu)(shu)格式如(ru)下(xia)

    mysqlContainer = MysqlContainer(image='mysql:5.7',
                             MYSQL_USER='test_asd',
                             MYSQL_ROOT_PASSWORD='afrvte54657hnngf',
                             MYSQL_PASSWORD='afrvte54657hnngf',
                             MYSQL_DATABASE='test')
   mysqlContainer.with_command('--lower-case-table-names=1')
   # 將把上面(mian)設置的command覆蓋
   mysqlContainer.with_command('--character_set_server=utf8mb4')
   # 想要同時設(she)置多(duo)個MySQL服(fu)務啟動參數,按如(ru)下格(ge)式傳遞參數
   mysqlContainer.with_command('--lower-case-table-names=1 --character_set_server=utf8mb4')

進入容器查看

image-20230301183941451

 

 

容器操(cao)作(zuo)

DockerContainer提供了start和stop方(fang)法,每次調(diao)用start方(fang)法就(jiu)會(hui)啟動(dong)一個新(xin)容器實(shi)例(li)并返回(hui),stop方(fang)法不是停止容器而是刪除容器。

    def start(self):
       logger.info("Pulling image %s", self.image)
       docker_client = self.get_docker_client()
       self._container = docker_client.run(self.image,
                                           command=self._command,
                                           detach=True,
                                           environment=self.env,
                                           ports=self.ports,
                                           name=self._name,
                                           volumes=self.volumes,
                                           **self._kwargs
                                          )
       logger.info("Container started: %s", self._container.short_id)
       return self
?
   def stop(self, force=True, delete_volume=True):
       self.get_wrapped_container().remove(force=force, v=delete_volume)

為了避免容器一(yi)(yi)直運行,容器使用完(wan)后一(yi)(yi)定要調用DockerContainer的stop方法(fa)刪(shan)除容器實例。

DockerContainer封裝了具(ju)體的容器實(shi)例(li)(li),提供(gong)了一些工具(ju)方法如with_env、with_command,它(ta)的實(shi)例(li)(li)變量(liang)_container才對應著一個具體的容器實(shi)例(li)。

_container是(shi)Container類(lei)型的對象,是(shi)python的docker依賴(lai)提供的。

class Container(Model):
   """ Local representation of a container object. Detailed configuration may
      be accessed through the :py:attr:`attrs` attribute. Note that local
      attributes are cached; users may call :py:meth:`reload` to
      query the Docker daemon for the current properties, causing
      :py:attr:`attrs` to be refreshed.
  """
   
   //...
   
   def pause(self):
       """
      Pauses all processes within this container..
      """
       return self.client.api.pause(self.id)
?
   
   def remove(self, **kwargs):
       """
     ; Remove this container. Similar to the ``docker rm`` command.
      """
       return self.client.api.remove_container(self.id, **kwargs)
   
   
   def start(self, **kwargs):
       """
      Start this container. Similar to the ``docker start`` command, but
      """
       return self.client.api.start(self.id, **kwargs)
   
   
   def stop(self, **kwargs):
       """
      Stops a container. Similar to the ``docker stop`` command.
      """
       return self.client.api.stop(self.id, **kwargs)
   
   
   def unpause(self):
       """
      Unpause all processes within the container.
      """
       return self.client.api.unpause(self.id)

想要對容器實例進(jin)行其(qi)他操作,可以通過DockerContainer的get_wrapped_container方(fang)法獲取_container實例(li)變量。

    container = mysqlContainer.get_wrapped_container()
   #停止活動
   container.pause()
   #繼(ji)續活(huo)動
   container.unpause()

我們可以調(diao)用pause方法來(lai)模擬數據庫不可用狀(zhuang)態(tai)。

 

性能表現(xian)和資源占用(yong)

測(ce)試(shi)環境(jing):Windows WSL2+Windows docker desktop

啟(qi)動容器(qi)的(de)start方法是(shi)同(tong)步的(de),在等待容器(qi)完全啟(qi)動后(hou)才會返回(hui),這里的(de)容器(qi)啟(qi)動時間7.7s

image-20230301180134161

內存占用

image-20230301183539867

使(shi)(shi)用testcontainers-python創建容(rong)器時,推薦使(shi)(shi)用官方的Docker鏡(jing)像,是經過優化和配置的,因此占用的內存資(zi)源(yuan)是相對較少(shao)的。

第一次啟動容器需要(yao)拉取(qu)鏡(jing)像耗(hao)時會比較(jiao)久。

 

 

控(kong)制容器啟動超時(shi)時(shi)間(jian)

在start方(fang)法(fa)里,testcontainers通(tong)過連接數據庫來判斷服務是否可用。testcontainers默認設置的最(zui)大連接重(zhong)試次數為120次,間(jian)隔1s。

os.environ.setdefault("TC_MAX_TRIES", '120')

調試階段(duan)可以適(shi)當調小此值。

雖然單(dan)個容(rong)器(qi)啟(qi)動(dong)(dong)(dong)速(su)度(du)很快,測試發現(xian),如果連續啟(qi)動(dong)(dong)(dong)4個MySQL容(rong)器(qi),容(rong)器(qi)的啟(qi)動(dong)(dong)(dong)時間超過(guo)甚至(zhi)會超過(guo)120s導致測試失敗(偶爾)。

 

 

PostgresContainer

指定參(can)數

postgresContainer.with_command("-c wal_level=logical -c max_connections=134 -c shared_buffers=2GB")

資源占(zhan)用(yong)

image-20230307093509025

 

 

docker-compose

testcontainers也支持使(shi)用docker-compose。

 

創建(jian)docker-compose.yml文件(jian),啟動一(yi)(yi)個MySQL和一(yi)(yi)個postgres。

version: '3'
services:
db:
  image: mysql:5.7
  environment:
    MYSQL_ROOT_PASSWORD: root
    MYSQL_DATABASE: test_db
    MYSQL_USER: test_user
    MYSQL_PASSWORD: test_password
  ports:
    - "13306:3306"
postgres:
  image: postgres:13
  environment:
    POSTGRES_DB: test_db
    POSTGRES_USER: test_user
    POSTGRES_PASSWORD: test_password
  ports:
    - "15432:5432"

創建DockerCompose,指定(ding)docker-compose文件在當(dang)前目錄下,啟動DockerCompose

from testcontainers.compose import DockerCompose
?
?
def test_compose():
   compose = DockerCompose('.')
   compose.start()
?
   mysql_host = compose.get_service_host('db', 3306)
   print(mysql_host)
   mysql_port = compose.get_service_port('db', 3306)
   print(mysql_port)
?
   '''
      Returns tuple[str, str, int] stdout, stderr, return code
  '''
   return_tuple = compose.exec_in_container('db', ['ls'])
   print(return_tuple)
?
   time.sleep(1)
?
   compose.stop()

 

 

 

testcontainers-java

@Testcontainers: 用(yong)于啟用(yong) Testcontainers 支(zhi)持。它可以用(yong)在類(lei)或方法(fa)級別,讓 JUnit 在測試(shi)執行前啟動 Docker 容器(qi)。例如:

@Testcontainers
public class MyTestClass {
// ...
}

@Container: 用(yong)于(yu)將 Docker 容器(qi)作為測試類(lei)的靜態字段啟動(dong)。這個注解可(ke)以與 DockerComposeContainerGenericContainer 和其他(ta)擴展(zhan)類(lei)一起使(shi)用。例如:

@Container
public static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer();

 

 

@Testcontainers
public class MyTest {
   
@Container
   private static final MySQLContainer mySQLContainer = (MySQLContainer) new MySQLContainer("mysql:5.7.34")
          .withDatabaseName("test")
          .withUsername("dds")
          .withPassword("asfrer54765dsc")
          .withInitScript("mysqlScript.sql")
      .withEnv("name","value")
          .withCommand("--lower-case-table-names=1 --character_set_server=utf8mb4");
   
   @Test
   public void testMysql() throws SQLException {
       System.out.println(mySQLContainer.getMappedPort(3306));
       try (Connection conn = DriverManager.getConnection(mySQLContainer.getJdbcUrl(), mySQLContainer.getUsername(), mySQLContainer.getPassword())) {
           String sql = "SELECT 2 + 2";
           try (PreparedStatement stmt = conn.prepareStatement(sql)) {
               try (ResultSet rs = stmt.executeQuery()) {
                   rs.next();
                   int result = rs.getInt(1);
                   Assertions.assertEquals(4, result);
              }
          }
      }
  }

withPassword方(fang)法設置的(de)密碼也是root用戶的(de)密碼;

withInitScript可指定MySQL的(de)初始化腳本;

withCommand可指(zhi)定MySQL參數(shu);

withEnv設置容(rong)器環境(jing)變(bian)量;

getJdbcUrl返回JDBC連接urljdbc:mysql://localhost:51319/test

getMappedPort方(fang)法返回容器內部端(duan)口所綁定(ding)的宿(su)主(zhu)機端(duan)口。

 

 

 

文章來自個人專欄
文章 | 訂閱(yue)
0條評論
0 / 1000
請輸入你的評論
0
0