Тестирование — важный шаг на всех этапах разработки ПО. Но не все компоненты имеют очевидные, известные и понятные пути тестирования. К примеру, образы Docker либо не тестируют вообще, либо тестируют только на пригодность к запуску. В этой статье я расскажу, как протестировать образ Docker так, чтобы убедиться в том, что он на 100% выполняет свои задачи.
Юнит‑тестирование (или модульное тестирование) — это процесс в разработке программного обеспечения, позволяющий проверить работоспособность отдельных модулей исходного кода. Такое тестирование привычно применяется в разработке непосредственно программного обеспечения, однако с ходу сложно себе представить юнит‑тестирование образа Docker.
Взглянем на простейший Dockerfile:
FROM busybox:1.32.1 RUN echo 'Hello, World!' > /test.txt
Здесь мы выполняем единственное действие — добавляем файл со строкой Hello, World!
в файл /test.txt
.
Как можно проверить, что мы достигаем желаемого результата? Можно запустить собранный контейнер и посмотреть, что, во‑первых, нужный файл присутствует, а во‑вторых, его содержимое равно ожидаемому.
$ docker build -t test .[+] Building 7.7s (6/6) FINISHED $ docker run --rm test ls -lha /test.txt -rw-r--r-- 1 root root 14 Feb 20 19:26 /test.txt $ docker run --rm test cat /test.txt Hello, World!
Не слишком удобно, не так ли? К счастью, существует фреймворк terratest. Он позволяет писать тесты на Golang для Docker (и docker-compose) так же, как и для обычного кода!
Взглянем на программную реализацию данного теста:
package docker_testimport ( "testing" "github.com/gruntwork-io/terratest/modules/docker" "github.com/stretchr/testify/assert")func TestDockerImage(t *testing.T) { // Определяем название образа для тестирования tag := "test" buildOptions := &docker.BuildOptions{ Tags: []string{tag}, } // Собираем образ из Dockerfile’а docker.Build(t, "../", buildOptions) // Фактически выставляем как опции запуск контейнера со следующими командами // Команда, которая вернет 'exists', если файл существует eOpts := &docker.RunOptions{Command: []string{"sh", "-c", "[ -f /test.txt ] && echo exists"}} // Команда, которая вернет содержимое файла cOpts := &docker.RunOptions{Command: []string{"cat", "/test.txt"}} // Запускаем контейнер с проверкой на наличие файла chkExisting := docker.Run(t, tag, eOpts) // Проверяем, что вывод равен желаемому assert.Equal(t, "exists", chkExisting) // Запускаем контейнер с выводом содержимого файла chkContent := docker.Run(t, tag, cOpts) // Проверяем, что вывод равен желаемому assert.Equal(t, "Hello, World!", chkContent)}
Стало ощутимо удобнее! Благодаря полноценному языку программирования мы можем создавать намного более сложные сценарии тестирования, использовать API докер и так далее.
К сожалению, примеры вроде Hello World редко объясняют реальные кейсы применения технологии, поэтому давай представим несколько более сложный случай. К примеру, есть Golang-приложение (простой HTTP-сервер):
package mainimport ( "fmt" "net/http")func hello(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "hello")}func main() { http.HandleFunc("/hello", hello) http.ListenAndServe(":8000", nil)}
Предположим, приложению также требуется бинарник curl
для работы. Тогда Dockerfile будет выглядеть следующим образом:
# Первым делом собираем само приложениеFROM golang:1.16 as builder WORKDIR /src/app COPY ./main.go /src/app RUN CGO_ENABLED=0 go build -o /go/bin/app main.go # Далее собираем базовый образ из alpine, добавляя туда бинарник curlFROM alpine:3.13.2 AS basis RUN apk add --no-cache curl # Следующим номером открываем порт 8080 и добавляем бинарник из шага сборкиFROM basis AS production EXPOSE 8080 COPY --from=builder /go/bin/app /usr/bin/app ENTRYPOINT [ "/usr/bin/app" ]
Что здесь можно проверить:
curl
;Взглянем, какие можно написать тесты (код полностью доступен в конце статьи).
Вынесем сборку образов в отдельную функцию, чтобы не повторяться:
func BuildWithTarget(t *testing.T, dCtx string, tag string, target string) { buildOptions := &docker.BuildOptions{ Tags: []string{tag}, // Target для сборки multi-stage Target: target, } docker.Build(t, dCtx, buildOptions)}
Первым тестом проверим, как и в предыдущем примере, наличие бинарника curl
:
func TestBasisLayer(t *testing.T) { tag := fmt.Sprintf("go_demo:%s", BasisTarget) // Собирается образ с нужным таргетом BuildWithTarget(t, "../", tag, BasisTarget) // И далее схожим образом проверяем наличие файла curl opts := &docker.RunOptions{ Command: []string{"sh", "-c", "[ -f /usr/bin/curl ] && echo exists"}, Remove: true, } chkExisting := docker.Run(t, tag, opts) assert.Equal(t, "exists", chkExisting)}
Вторым — доступен ли HTTP-сервер. Здесь уже сложнее:
func TestProductionLayerServerAvailability(t *testing.T) { tag := fmt.Sprintf("go_demo:%s", ProdTarget) BuildWithTarget(t, "../", tag, ProdTarget) // Обязательно выставляем параметр Detach, в противном случае // процесс зависнет на выводе запущенного контейнера. // Параметр -P позволит пробросить порт на случайный свободный // порт на хосте, тем самым позволяя избежать ошибки с выбором занятого порта opts := &docker.RunOptions{ Remove: true, Detach: true, OtherOptions: []string{"-P"}, } // Далее запускаем контейнер и получаем его ID cntId := docker.RunAndGetID(t, tag, opts) // Через интерфейс функции Inspect получаем проброшенный порт cntInsp := docker.Inspect(t, cntId) hostPort := cntInsp.GetExposedHostPort(uint16(8000)) url := fmt.Sprintf("http://localhost:%d/hello", int(hostPort)) // Используя http_helper из библиотеки terratest, можно сделать // запрос к выбранному URL и проверить результаты запроса status, _ := http_helper.HttpGet(t, url, &tls.Config{}) assert.Equal(t, 200, status) // В последнюю очередь удаляем использованный контейнер docker.Stop(t, []string{cntId}, &docker.StopOptions{})}
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
1 год9300 р. |
1 месяц870 р. |
Я уже участник «Xakep.ru»
Читайте также
Последние новости