構(gòu)建和測(cè)試環(huán)境即代碼

構(gòu)建和測(cè)試環(huán)境即代碼基礎(chǔ)設(shè)施即代碼,流水線即代碼的概念相似,就是將你的代碼構(gòu)建和測(cè)試環(huán)境使用代碼來(lái)表達(dá),以達(dá)到代碼在本機(jī)正常工作...同時(shí)在任何地方都可以正常工作。

薛定諤的測(cè)試

回想這樣一個(gè)場(chǎng)景,當(dāng)你加入一個(gè)新公司或者加入一個(gè)新項(xiàng)目的時(shí)候,是不是需要安裝很多軟件以使得新項(xiàng)目的服務(wù)或者測(cè)試可以在你本機(jī)運(yùn)行?

假設(shè)這個(gè)新項(xiàng)目使用的編程語(yǔ)言是NodeJs,使用Mysql存儲(chǔ)數(shù)據(jù),那么很可能會(huì)出現(xiàn)如下情況,每個(gè)開(kāi)發(fā)人員本機(jī)安裝的軟件版本都不太相同:

  • 開(kāi)發(fā)小A本機(jī)環(huán)境:NodeJS 8.1, MySQL 5.4
  • 開(kāi)發(fā)小B本機(jī)環(huán)境:NodeJS 8.1, MySQL 5.4
  • 開(kāi)發(fā)小C本機(jī)環(huán)境:NodeJS 9.2, MySQL 5.2
  • 開(kāi)發(fā)小D本機(jī)環(huán)境:NodeJS 12.4, MySQL 5.1

這就是為什么有時(shí)候你編寫(xiě)好的代碼或者測(cè)試在你本機(jī)運(yùn)行成功,但到了別人的機(jī)器上就會(huì)出問(wèn)題,原因很可能是你的代碼依賴了你本地開(kāi)發(fā)環(huán)境的軟件版本。即便你依賴的軟件版本相同,依然可能會(huì)出現(xiàn)錯(cuò)誤,因?yàn)槟惚镜卮a編譯環(huán)境很可能會(huì)依賴于你的操作系統(tǒng)的某些軟件。

這個(gè)問(wèn)題甚至曾經(jīng)在Google也非常嚴(yán)重。在2016的時(shí)候,Google就發(fā)現(xiàn)84%新的失敗測(cè)試都是由于flakiness。什么是flakiness呢?

一個(gè)flaky測(cè)試就是在相同的配置的情況下這個(gè)測(cè)試可能會(huì)成功也可能會(huì)失敗.

這個(gè)測(cè)試是不是很像薛定諤的貓,不運(yùn)行的時(shí)候測(cè)試狀態(tài)處于疊加狀態(tài),既可以是成功也可以是失敗,當(dāng)你運(yùn)行之后才知道是成功還是失敗。自動(dòng)化測(cè)試重要性不必多說(shuō),但flaky測(cè)試顯然令開(kāi)發(fā)人員很疑惑,或者說(shuō)是沮喪。產(chǎn)生flaky測(cè)試的原因有很多,而不一致的測(cè)試環(huán)境常常會(huì)導(dǎo)致測(cè)試的flakiness

如何解決測(cè)試環(huán)境不一致的問(wèn)題?

自動(dòng)化一切事情

將所有手動(dòng)的步驟使用腳本將其過(guò)程自動(dòng)化起來(lái)使得構(gòu)建和測(cè)試的步驟都是一致的。但光使用自動(dòng)化腳本是不夠的,它的問(wèn)題在于:

  • 所依賴的運(yùn)行環(huán)境的操作系統(tǒng)不一致可能會(huì)導(dǎo)致一些問(wèn)題。
  • 端口沖突問(wèn)題。
  • ...

虛擬機(jī)

使用虛擬機(jī)(如Vagrant)可以解決只使用自動(dòng)腳本所存在的問(wèn)題。但是使用虛擬機(jī)是非常消耗資源的,對(duì)于運(yùn)行在虛擬機(jī)里的任務(wù)的性能影響是比較大的。所以通常虛擬機(jī)并不被推薦在持續(xù)集成服務(wù)器上使用。

共享的測(cè)試和集成環(huán)境

我經(jīng)歷過(guò)一些項(xiàng)目,項(xiàng)目中只有單元測(cè)試是可以在本地運(yùn)行的,而其他的測(cè)試都需要將代碼部署到云環(huán)境后才會(huì)運(yùn)行。這種方式有幾個(gè)特別大的缺點(diǎn):

  • 從代碼更改到在測(cè)試環(huán)境中運(yùn)行的周期時(shí)間太長(zhǎng)。
  • 由于測(cè)試和集成環(huán)境是共享的,所以你對(duì)測(cè)試環(huán)境會(huì)影響會(huì)影響到其他開(kāi)發(fā)人員的測(cè)試。這就好比你的測(cè)試依賴于一個(gè)全局變量,而其他人可以隨時(shí)修改這個(gè)變量,那么你的測(cè)試狀態(tài)就是不穩(wěn)定的。

而且,實(shí)現(xiàn)持續(xù)集成的一個(gè)前提條件就是:確保自動(dòng)化測(cè)試可以在一個(gè)隔離的環(huán)境中運(yùn)行

Docker

使用Docker容器技術(shù)可以解決使用虛擬機(jī)所存在的問(wèn)題。使用docker可以輕松解決一個(gè)容器的問(wèn)題,但是現(xiàn)實(shí)項(xiàng)目中的情況是:

  • 一個(gè)應(yīng)用由多個(gè)容器組成。
  • 一個(gè)容器會(huì)依賴于另一個(gè)容器。
  • 容器的啟動(dòng)順序是有依賴的。
  • ...

舉個(gè)栗子:Docker就像是一個(gè)獨(dú)奏的音樂(lè)家,而現(xiàn)實(shí)項(xiàng)目需要的一個(gè)合奏團(tuán)。

合奏團(tuán)

Docker + 基礎(chǔ)設(shè)施即代碼 + 工具

基礎(chǔ)設(shè)施即代碼的概念 + Docker + Docker工具(比如:Docker Compose, Batect, Dojo), 將這些柔和到一起并運(yùn)用到本地開(kāi)發(fā)和測(cè)試環(huán)境中即可實(shí)現(xiàn)構(gòu)建和測(cè)試環(huán)境即代碼,它可以給我們帶來(lái)一些好處:

  • 給自動(dòng)化測(cè)試提供一個(gè)隔離的運(yùn)行環(huán)境。
  • 云環(huán)境和本地只需要安裝Docker,和Git便可拉取代碼并進(jìn)行構(gòu)建和測(cè)試。
  • 由于配置以可執(zhí)行代碼而不是文檔的形式表示,因此與應(yīng)用程序一起分發(fā)和版本化配置更加容易。
  • 測(cè)試一次運(yùn)行成功,在不修改任何代碼的情況下任何地方都可以運(yùn)行成功。

構(gòu)建和測(cè)試環(huán)境即代碼工具

現(xiàn)在比較流行的容器編排工具有: Docker Compose, Dojo和Batect。我們?cè)陧?xiàng)目中選擇使用了Batect,Batect的全名是:Build And Test Environment as Code Tools。選擇它的原因是:

  • 執(zhí)行速度比Docker Compose快17%。主要是對(duì)Mac Docker和Window Docker的優(yōu)化。
  • 提供了development task的一種概念。
  • 非常便于將你的development task配置分享給其他的項(xiàng)目。
  • 提供了一種簡(jiǎn)單的方式可以幫助開(kāi)發(fā)者發(fā)現(xiàn)當(dāng)前項(xiàng)目都有哪些可以使用的task.
  • 無(wú)需安裝。

示例

假設(shè)我們已經(jīng)建立了一個(gè)國(guó)際轉(zhuǎn)賬服務(wù)(international transfer service),該服務(wù)依賴于另一個(gè)團(tuán)隊(duì)維護(hù)的匯率服務(wù)(Exchange Rate Service),并且國(guó)際轉(zhuǎn)賬服務(wù)擁有自己的數(shù)據(jù)存儲(chǔ)區(qū)來(lái)跟蹤客戶的轉(zhuǎn)賬。如果要端到端測(cè)試,我們需要四個(gè)組件:

  • International transfers service。
  • fake的Exchange rate service。
  • fake的database。
  • Test driver。
international transfers service

我們期望項(xiàng)目的測(cè)試包含:

  • unit test,unit test直接在test dirver容器中運(yùn)行(此項(xiàng)目使用java,所以這個(gè)test driver是一個(gè)擁有java構(gòu)建環(huán)境的docker容器)。
  • integration test,integration test需要提前啟動(dòng)Exchange rate service和database容器,等待Exchange rate service和database容器啟動(dòng)成功,并正常運(yùn)行后,在test dirver容器中運(yùn)行集成測(cè)試。
  • journey test,journey test需要提前啟動(dòng)Exchange rate service, database和International transfers service容器,等待Exchange rate service, database和International transfers service容器啟動(dòng)成功,并正常運(yùn)行后,在test driver容器中運(yùn)行journey測(cè)試。

如何使用Batect作為構(gòu)建和測(cè)試環(huán)境即代碼工具呢?完整代碼可以到Github代碼庫(kù)中查看。

這里我們主要看看Batect的配置文件:batect.yml

project_name: international-transfers-service

containers:
  database:
    build_directory: .batect/database
    environment:
      POSTGRES_USER: international-transfers-service
      POSTGRES_PASSWORD: TheSuperSecretPassword
      POSTGRES_DB: international-transfers-service

  exchange-rate-service:
    build_directory: .batect/exchange-rate-service-fake

  international-transfers-service:
    build_directory: .batect/international-transfers-service
    dependencies:
      - database
      - exchange-rate-service

tasks:
  build:
    description: Build the application.
    group: Build tasks
    run:
      container: java-build-env
      command: ./gradlew shadowJar

  unitTest:
    description: Run the unit tests.
    group: Test tasks
    run:
      container: java-build-env
      command: ./gradlew test

  integrationTest:
    description: Run the integration tests.
    group: Test tasks
    dependencies:
      - database
      - exchange-rate-service
    run:
      container: java-build-env
      command: ./gradlew integrationTest

  journeyTest:
    description: Run the journey tests.
    group: Test tasks
    prerequisites:
      - build
    dependencies:
      - international-transfers-service
    run:
      container: java-build-env
      command: ./gradlew journeyTest

  run:
    description: Run the application.
    group: Test tasks
    prerequisites:
      - build
    run:
      container: international-transfers-service
      ports:
        - local: 6001
          container: 6001

include:
  - type: git
    repo: https://github.com/batect/java-bundle.git
    ref: 0.1.0

這個(gè)yml中主要分3大部分:containers, tasks and include

containers

containers配置,顧名思義就是用來(lái)配重容器的地方。這里我們配置了3個(gè)容器,這里的配置基本和Docker Compose是一樣的:

  • international-transfers-service, 對(duì)應(yīng)圖中的International transfers service。.
  • database, 對(duì)應(yīng)到圖中的database。
  • exchange-rate-service,對(duì)應(yīng)到圖中Exchange rate service。

tasks

task是Batect中最小的工作單元。通過(guò)在項(xiàng)目中運(yùn)行./batect --list-tasks可以非常方便的發(fā)現(xiàn)所有tasks:

$ ./batect --list-tasks
Build tasks:
- build: Build the application.

Test tasks:
- integrationTest: Run the integration tests.
- journeyTest: Run the journey tests.
- run: Run the application.
- unitTest: Run the unit tests.

通過(guò)./batect integrationTest便可執(zhí)行integrationTest:

$ ./batect integrationTest
Running integrationTest...
database: running
exchange-rate-service: running
java-build-env: running ./gradlew integrationTest

BUILD SUCCESSFUL in 6s
3 actionable tasks: 3 up-to-date

integrationTest finished with exit code 0 in 11.7s.

運(yùn)行時(shí),可以看到。在執(zhí)行integrationTest之前,它會(huì)首先啟動(dòng)containers: database和exchange-rate-service,然后等待這兩個(gè)容器的狀態(tài)變得health之后才開(kāi)始在java-build-env這個(gè)容器中運(yùn)行./gradlew integrationTest。而這些都是在integraionTest這個(gè)task中定義好的:

tasks:
  integrationTest:
    description: Run the integration tests.
    group: Test tasks
    dependencies:
      - database
      - exchange-rate-service
    run:
      container: java-build-env
      command: ./gradlew integrationTest

task中的run部分用來(lái)配置要執(zhí)行task所使用的容器是什么,執(zhí)行的命令是什么。這種container配置和task分離的方式可以很方便的重用container來(lái)執(zhí)行不同的task。

在task中還可以指定dependencies,dependencies中配置的容器會(huì)提前執(zhí)行,并且只有在他們的staus變health之后,task才開(kāi)始執(zhí)行。

batect如何知道容器變health了呢?它是通過(guò)docker中的HEALTHCHECK來(lái)定義的。

在task中還可以指定prerequisites,prerequisites是用來(lái)指定task的依賴,如: journeyTest:

tasks:
  journeyTest:
    description: Run the journey tests.
    group: Test tasks
    prerequisites:
      - build
    dependencies:
      - international-transfers-service
    run:
      container: java-build-env
      command: ./gradlew journeyTest

prerequisites中的task會(huì)在最開(kāi)始執(zhí)行,所以執(zhí)行順序:prerequisites -> prerequisites -> task。從下面的運(yùn)行輸出上也能反映出這個(gè)順序。

 ./batect journeyTest
Running build...
java-build-env: running ./gradlew shadowJar

BUILD SUCCESSFUL in 5s
2 actionable tasks: 2 up-to-date

build finished with exit code 0 in 8.3s.

Running journeyTest...
database: running
exchange-rate-service: running
international-transfers-service: running
java-build-env: running ./gradlew journeyTest

> Task :journeyTest

com.charleskorn.banking.internationaltransfers.JourneyTest > journeyTest PASSED

BUILD SUCCESSFUL in 11s
2 actionable tasks: 2 executed

journeyTest finished with exit code 0 in 46s.

通過(guò)containers和tasks可以很方便的將構(gòu)建和測(cè)試容器化和代碼化,即As Code。

include

細(xì)心的話,你會(huì)發(fā)現(xiàn)tasks中使用的java-build-env container,我們并沒(méi)定義過(guò)它,它其實(shí)來(lái)源于https://github.com/batect/java-bundle.git。

include:
  - type: git
    repo: https://github.com/batect/java-bundle.git
    ref: 0.1.0

Includes允許您將其他地方定義的配置導(dǎo)入項(xiàng)目。 在許多情況下,它們是一種有用的機(jī)制:

  • 使配置文件較小且易于管理。
  • 在項(xiàng)目之間共享任務(wù)和容器。
  • 利用他人構(gòu)建和維護(hù)的任務(wù)和容器,從而節(jié)省您的時(shí)間和精力。

在docker中運(yùn)行batect

batect依賴也java,如果我們并不想在本機(jī)或者持續(xù)集成服務(wù)器上安裝java的話,我們可以使用docker in docker的方式在docker中運(yùn)行batect,如下,可以在項(xiàng)目中創(chuàng)建/auto/batect:

#! /bin/bash

set -euo pipefail

current_dir=$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)
root_dir=${current_dir}/..

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  -v ${HOME}/.batect:/root/.batect \
  -v ${root_dir}:${root_dir} \
  -v /tmp:/tmp \
  -w ${root_dir} \
  openjdk:12 \
  ./batect --output=simple ${BATECT_ARGS}

然后在使用如下方式運(yùn)行task:

BATECT_ARGS=integrationTest ./auto/batect

相關(guān)閱讀

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容