Mở đầu — vì sao bài này quan trọng
Bạn đã bao giờ nghe câu than quen thuộc: "Trên máy tôi chạy ngon mà!"? Đây là nỗi ám ảnh kinh điển của cả developer lẫn tester. Test suite chạy xanh mượt trên laptop của bạn, nhưng khi đồng nghiệp kéo code về hoặc khi chạy trên server CI thì đỏ lòm hàng loạt. Nguyên nhân hiếm khi là "code sai" — thường là do môi trường khác nhau: phiên bản Chrome lệch nhau, Java 11 vs Java 17, thiếu một thư viện hệ thống, timezone máy chủ là UTC còn máy bạn là GMT+7, hay database có sẵn dữ liệu rác từ lần test trước.
Với vai trò một người làm automation testing, môi trường (test environment) chính là nền móng của mọi thứ bạn xây. Test tự động chỉ đáng tin khi nó được chạy trong một môi trường tái lập được (reproducible) và cô lập (isolated). Nếu mỗi lần chạy lại cho kết quả khác nhau vì môi trường trôi dạt, bạn sẽ mất niềm tin vào chính bộ test của mình — và một bộ test không ai tin thì vô dụng.
Docker chính là công cụ giải quyết gốc rễ vấn đề này. Nó cho phép bạn đóng gói toàn bộ môi trường — hệ điều hành nền, runtime, thư viện, trình duyệt, database — vào một "container" chạy giống hệt nhau ở mọi nơi: máy bạn, máy đồng nghiệp, server CI, hay cloud. Trong bài này, chúng ta sẽ đi sâu vào cách Docker thay đổi cách bạn dựng và quản lý môi trường test, cách viết cấu hình cho một bộ test automation chạy trong container, và những cạm bẫy thực tế mà tôi đã chứng kiến nhiều đội QA vấp phải.
Khái niệm cốt lõi
Container là gì và khác gì máy ảo?
Hãy tưởng tượng máy ảo (VM) giống như bạn xây một căn nhà riêng biệt hoàn chỉnh — có móng, tường, mái, hệ thống điện nước riêng — mỗi lần cần một môi trường là xây cả căn nhà. Rất nặng, tốn tài nguyên, khởi động chậm (vài phút). Container thì giống như bạn chia một tòa nhà lớn thành nhiều căn hộ: chúng dùng chung phần "móng" (kernel của hệ điều hành host) nhưng mỗi căn hộ vẫn có không gian riêng, cách âm với nhau. Kết quả là container siêu nhẹ, khởi động trong vài giây, và bạn có thể chạy hàng chục container trên một máy.
Với testing, sự khác biệt này cực kỳ quan trọng: bạn có thể quay (spin up) một container mới cho mỗi lần chạy test, xong việc thì hủy đi, lần sau lại có môi trường sạch tinh khôi.
Image và Container — bản thiết kế và ngôi nhà
Có hai khái niệm dễ nhầm mà bạn phải phân biệt rõ:
- Image là bản thiết kế đóng băng, chỉ đọc. Ví dụ image
selenium/standalone-chromechứa sẵn Chrome, ChromeDriver và Selenium server. Image được xây từ một file mô tả gọi làDockerfile. - Container là một thực thể đang chạy được tạo ra từ image. Từ một image bạn có thể tạo ra nhiều container giống hệt nhau.
Tại sao Docker giải quyết được "works on my machine"
Khi bạn khai báo môi trường trong Dockerfile, mọi thứ trở thành code — thứ có thể đưa vào Git, review, và version. Không còn chuyện "à quên chưa cài thư viện X trên server". Toàn bộ đội, và cả server CI, cùng dùng chung một định nghĩa môi trường. Chạy ở đâu cũng giống nhau đến từng byte.
Isolation — cô lập cho mỗi phiên test
Đây là siêu năng lực thứ hai của Docker cho testing. Mỗi lần test, bạn có thể khởi tạo một database container mới toanh, nạp dữ liệu mẫu, chạy test, rồi hủy container. Không còn chuyện test A làm bẩn dữ liệu khiến test B thất bại (test pollution). Không còn chuyện hai người chạy test song song trên cùng một database chung rồi giẫm chân nhau. Mỗi phiên test là một thế giới riêng, độc lập.
Docker Compose — dàn nhạc cho nhiều container
Một hệ thống thực tế hiếm khi chỉ có một thành phần. Bạn có thể cần: một container chạy trình duyệt Selenium, một container database PostgreSQL, một container ứng dụng cần test. docker-compose.yml là file cho phép bạn khai báo tất cả các container này và cách chúng nối mạng với nhau, rồi bật/tắt toàn bộ chỉ bằng một lệnh. Đây là công cụ bạn sẽ dùng nhiều nhất khi làm test automation với Docker.
Tình huống thực tế
Ví dụ 1: Tiki và cơn ác mộng "phiên bản Chrome"
Một đội QA tại một sàn thương mại điện tử lớn ở Việt Nam (bối cảnh tương tự Tiki) có bộ Selenium test khoảng 400 test case chạy trên UI. Vấn đề: laptop của mỗi tester cài phiên bản Chrome khác nhau, và Chrome tự động cập nhật. Cứ vài tuần lại có một đợt test đỏ hàng loạt không phải vì bug sản phẩm, mà vì Chrome vừa cập nhật lên phiên bản mới còn ChromeDriver thì chưa tương thích. Đội mất trung bình 3–4 giờ mỗi đợt chỉ để "sửa môi trường".
Giải pháp: họ chuyển sang chạy test qua image selenium/standalone-chrome với một phiên bản cố định, ví dụ selenium/standalone-chrome:119.0. Phiên bản Chrome và ChromeDriver được ghim chặt trong image. Ai chạy test cũng dùng đúng container đó, và server CI cũng vậy. Việc nâng phiên bản Chrome trở thành một quyết định chủ động — sửa một dòng trong file cấu hình, tạo Pull Request, review, rồi merge — chứ không còn là tai nạn bất ngờ.
Bài học: Ghim phiên bản (pin version) trình duyệt và driver vào image giúp bạn kiểm soát hoàn toàn thứ mà trước đây là "biến số hoang dã". Đừng bao giờ dùng tag :latest cho môi trường test — nó là mầm mống của flakiness.
Ví dụ 2: Startup fintech ở Singapore và bài toán database bẩn
Một startup fintech quy mô khoảng 30 kỹ sư ở Singapore có bộ integration test đọc/ghi trực tiếp vào một database staging dùng chung. Vấn đề nghiêm trọng: khi CI chạy song song ba nhánh test cùng lúc, cả ba cùng ghi vào một database, dữ liệu chồng chéo, test thất bại ngẫu nhiên khoảng 15% số lần chạy. Không ai dám tin kết quả nữa, và mọi người bắt đầu có thói quen tệ hại là "cứ retry cho tới khi xanh".
Họ dùng Docker Compose để mỗi lần chạy CI khởi tạo một container PostgreSQL riêng, nạp schema và dữ liệu mẫu bằng migration, chạy test, rồi hủy container khi xong. Mỗi nhánh CI có database của riêng nó, hoàn toàn cô lập. Tỷ lệ flaky do database rớt về gần 0%. Thời gian dựng database chỉ mất khoảng 8 giây nhờ container nhẹ, chấp nhận được so với lợi ích về độ tin cậy.
Bài học: Cô lập database theo từng phiên test loại bỏ cả một lớp lỗi flaky mà bạn không bao giờ debug được bằng cách sửa code test. Vấn đề nằm ở kiến trúc môi trường, và Docker giải quyết nó ở đúng tầng đó.
Ví dụ 3: Đội ngoài (outsourcing) ở Đà Nẵng onboarding nhân sự mới
Một công ty gia công phần mềm ở Đà Nẵng thường xuyên có tester mới gia nhập dự án. Trước đây, việc dựng môi trường test cho một người mới mất trung bình 1,5 ngày công: cài đúng Java, đúng Maven, đúng Chrome, đúng Node, cấu hình biến môi trường, xử lý mấy lỗi vặt "tại sao máy tôi không chạy được". Nhân với hàng chục người mỗi năm, đó là con số lãng phí khổng lồ.
Sau khi đóng gói toàn bộ môi trường test vào Docker, quy trình onboarding rút xuống còn: cài Docker, clone repo, chạy docker compose up. Người mới có môi trường chạy được trong vòng 30 phút, giống hệt mọi người trong đội. Người quản lý dự án nói vui rằng họ "tiết kiệm được cả một tuần công mỗi quý" chỉ nhờ bước này.
Bài học: Môi trường-dưới-dạng-code không chỉ giúp test chạy ổn định mà còn giảm chi phí vận hành đội ngũ. Đây là lợi ích ít ai nhắc tới nhưng lại rất thực tế với các đội đông người, biến động nhân sự cao như ở Việt Nam.
Hướng dẫn từng bước
Chúng ta sẽ dựng một môi trường test Selenium hoàn chỉnh bằng Docker Compose, gồm một container trình duyệt và container chạy test của bạn.
Bước 1 — Cài Docker. Tải Docker Desktop (macOS/Windows) hoặc cài Docker Engine (Linux). Kiểm tra:
docker --version
docker compose version
Bước 2 — Viết Dockerfile cho bộ test. Giả sử test viết bằng Python + pytest + Selenium. Tạo file Dockerfile:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["pytest", "-v", "--maxfail=1"]
Lưu ý: COPY requirements.txt trước rồi mới COPY . . để tận dụng cache của Docker — khi code thay đổi mà dependency không đổi, Docker không cần cài lại thư viện, build nhanh hơn nhiều.
Bước 3 — Viết docker-compose.yml khai báo hai dịch vụ.
services:
chrome:
image: selenium/standalone-chrome:119.0
shm_size: 2gb
ports:
- "4444:4444" tests:
build: .
depends_on:
- chrome
environment:
- SELENIUM_URL=http://chrome:4444/wd/hub
Chú ý ba điểm quan trọng: shm_size: 2gb để tránh Chrome sập vì thiếu shared memory (lỗi kinh điển), phiên bản Chrome được ghim cứng, và test kết nối tới trình duyệt qua tên dịch vụ chrome — Docker tự lo phần mạng nội bộ.
Bước 4 — Trỏ test tới remote WebDriver. Trong code, thay vì khởi tạo Chrome cục bộ, hãy dùng Remote:
import os
from selenium import webdriverdriver = webdriver.Remote(
command_executor=os.environ["SELENIUM_URL"],
options=webdriver.ChromeOptions()
)
Bước 5 — Chạy toàn bộ.
docker compose up --build --abort-on-container-exit
Docker sẽ build image test, khởi động container Chrome, chạy pytest bên trong container test, và tự dừng khi test kết thúc. --abort-on-container-exit đảm bảo khi container test xong thì cả stack dừng lại, trả về đúng exit code — cực kỳ quan trọng để CI biết pass hay fail.
Bước 6 — Dọn dẹp.
docker compose down -v
Cờ -v xóa luôn volume, đảm bảo lần sau bắt đầu từ trạng thái sạch.
Lỗi thường gặp & mẹo
Dùng tag :latest. Đây là lỗi số một. latest hôm nay khác latest tuần sau, phá vỡ chính tính tái lập mà bạn dùng Docker để đạt được. Luôn ghim phiên bản cụ thể cho image trình duyệt, ngôn ngữ, và database.
Quên shm_size khiến Chrome sập. Chrome dùng bộ nhớ chia sẻ (/dev/shm), mặc định container chỉ cấp 64MB. Kết quả là Chrome crash với lỗi mơ hồ kiểu "session deleted because of page crash". Luôn đặt shm_size: 2gb cho container trình duyệt, hoặc dùng cờ --disable-dev-shm-usage trong ChromeOptions.
Kết nối bằng localhost giữa các container. Bên trong mạng Docker Compose, mỗi container gọi nhau bằng tên dịch vụ (chrome, db), không phải localhost. Nếu container test gọi http://localhost:4444 nó sẽ tự trỏ vào chính nó và thất bại. Đây là lỗi rất hay gặp với người mới.
Không dùng healthcheck khiến test chạy trước khi database sẵn sàng. depends_on chỉ đảm bảo container được khởi động, không đảm bảo dịch vụ bên trong đã sẵn sàng nhận kết nối. Với database, hãy thêm healthcheck và cho test chờ tới khi healthy, hoặc thêm logic retry kết nối trong code khởi tạo.
Image phình to. Dùng base image -slim hoặc alpine khi có thể, gộp các lệnh RUN, và thêm file .dockerignore để loại node_modules, .git, file tạm ra khỏi build context. Image nhẹ build nhanh, đẩy lên registry nhanh, CI tiết kiệm thời gian.
Mẹo tận dụng cache layer. Sắp xếp Dockerfile theo thứ tự từ ít thay đổi đến hay thay đổi: cài dependency trước, copy source code sau. Điều này giúp mỗi lần sửa code chỉ rebuild layer cuối, tiết kiệm hàng phút mỗi lần build trên CI.
Bài tập thực hành
- Dựng môi trường Selenium cơ bản. Lấy một bộ test Selenium bất kỳ bạn đã viết (hoặc viết một test đơn giản mở trang chủ Tiki/Shopee và kiểm tra tiêu đề). Đóng gói nó bằng
Dockerfile+docker-compose.ymlnhư hướng dẫn trên, dùng imageselenium/standalone-chrome. Chạy được test quadocker compose uplà đạt.
- Thêm container database cô lập. Mở rộng
docker-compose.ymlthêm một dịch vụ PostgreSQL (postgres:16), khai báohealthcheck, và viết một integration test đơn giản ghi rồi đọc một bản ghi. Chạy hai lần liên tiếp và xác nhận lần chạy sau không bị ảnh hưởng bởi lần trước (nhờdocker compose down -v).
- Cố tình gây lỗi rồi sửa. Đổi image Chrome sang tag
:latest, xóashm_size, đổichromethànhlocalhosttrong biến môi trường. Chạy và quan sát từng lỗi phát sinh. Sau đó sửa lại từng cái. Bài tập này giúp bạn nhận diện lỗi trong thực tế thay vì chỉ đọc lý thuyết.
- Tối ưu image. Đo kích thước image test hiện tại bằng
docker images. Thêm.dockerignore, đổi sang base image-slim, sắp xếp lại thứ tự layer. Đo lại và ghi nhận mức giảm dung lượng cùng thời gian build.
Tóm tắt
Docker biến môi trường test từ một biến số hoang dã, dễ trôi dạt thành code có thể version, review và tái lập chính xác ở mọi nơi. Hai siêu năng lực cốt lõi của nó với testing là: tính tái lập — chấm dứt vĩnh viễn câu "works on my machine", và tính cô lập — mỗi phiên test có môi trường sạch riêng, xóa bỏ cả một lớp lỗi flaky do dữ liệu bẩn hay xung đột tài nguyên.
Bạn đã học cách phân biệt image với container, dùng Docker Compose để dàn dựng nhiều thành phần, viết Dockerfile và docker-compose.yml cho một bộ test Selenium chạy remote, và né những cạm bẫy phổ biến như tag latest, thiếu shm_size, hay nhầm localhost. Qua ba tình huống thực tế, bạn cũng thấy Docker không chỉ giúp test ổn định hơn mà còn tiết kiệm chi phí vận hành đội ngũ — đặc biệt giá trị với các đội đông người, nhiều biến động nhân sự như phổ biến ở Việt Nam.
Khi bạn đã làm chủ Docker cho môi trường test, bạn sẽ nhận ra đây chính là nền tảng để tiến tới những bước cao hơn: chạy test headless, chạy song song hàng chục container, và tích hợp mượt mà vào pipeline CI/CD. Môi trường vững chắc là điều kiện tiên quyết cho mọi thứ đó.