1. Unit Testing và Integration Testing là gì?
Trong quá trình phát triển phần mềm, viết code để triển khai chức năng mới chỉ là một phần của công việc. Lập trình viên còn cần kiểm tra xem chương trình có hoạt động đúng theo yêu cầu hay không, đồng thời bảo đảm những thay đổi mới không làm hỏng các chức năng đã có.
Testing (kiểm thử phần mềm) là quá trình đánh giá chương trình bằng cách thực hiện các thao tác hoặc kiểm tra phù hợp để phát hiện lỗi và xác minh hành vi của hệ thống.
Trong đó, Unit Testing và Integration Testing là hai phương pháp kiểm thử phổ biến.
Unit Testing (kiểm thử đơn vị) tập trung kiểm tra một đơn vị nhỏ của chương trình, thường là một hàm, phương thức hoặc một phần logic độc lập.
Integration Testing (kiểm thử tích hợp) tập trung kiểm tra sự phối hợp giữa nhiều thành phần, chẳng hạn HTTP Handler, Service, Repository và Database.
Có thể hiểu đơn giản:
- Unit Testing kiểm tra một thành phần có hoạt động đúng trong phạm vi của nó hay không.
- Integration Testing kiểm tra các thành phần có tương tác đúng với nhau hay không.
Hai phương pháp này bổ sung cho nhau, giúp lập trình viên phát hiện lỗi ở cả cấp độ logic riêng lẻ và cấp độ tích hợp.
2. Tại sao cần kiểm thử phần mềm?
Một chương trình có thể biên dịch thành công nhưng vẫn chứa lỗi logic hoặc không hoạt động đúng trong những tình huống cụ thể.
Ví dụ, một API tạo sản phẩm có thể chạy bình thường khi nhận dữ liệu hợp lệ nhưng lại chấp nhận giá âm, xử lý sai dữ liệu thiếu hoặc lưu dữ liệu không chính xác vào database.
Nếu chỉ kiểm tra chương trình bằng cách chạy thử thủ công một vài lần, lập trình viên có thể bỏ sót nhiều trường hợp.
Testing giúp giải quyết vấn đề này thông qua một số mục tiêu chính:
Phát hiện lỗi sớm
Các bài kiểm thử có thể phát hiện những trường hợp mà chương trình xử lý sai so với yêu cầu, chẳng hạn dữ liệu đầu vào không hợp lệ hoặc kết quả tính toán không chính xác.
Kiểm tra quy tắc nghiệp vụ
Những quy tắc như giá sản phẩm phải lớn hơn 0, email không được để trống hoặc người dùng không được xóa tài nguyên ngoài quyền hạn có thể được kiểm tra thông qua các test case cụ thể.
Hạn chế lỗi hồi quy
Regression là tình huống một thay đổi mới vô tình làm hỏng chức năng vốn hoạt động đúng trước đó.
Khi có các bài kiểm thử phù hợp, lập trình viên có thể chạy lại chúng sau mỗi lần thay đổi code để phát hiện những lỗi như vậy.
Hỗ trợ bảo trì và mở rộng
Khi chương trình có nhiều chức năng và mối quan hệ phụ thuộc, việc sửa đổi một thành phần có thể ảnh hưởng đến những thành phần khác. Testing giúp xác minh các hành vi quan trọng vẫn được duy trì sau khi thay đổi.
Tuy nhiên, kiểm thử không thể bảo đảm chương trình hoàn toàn không có lỗi. Chất lượng kiểm thử phụ thuộc vào cách thiết kế test case, phạm vi kiểm tra và khả năng bao quát các tình huống quan trọng.
3. Unit Testing hoạt động như thế nào?
3.1. Kiểm thử một đơn vị độc lập
Unit Testing kiểm tra một đơn vị nhỏ của chương trình trong phạm vi được xác định rõ ràng.
Đơn vị đó có thể là:
- Một hàm tính toán.
- Một phương thức kiểm tra dữ liệu.
- Một hàm chuyển đổi dữ liệu.
- Một phương thức thực hiện quy tắc nghiệp vụ.
Ví dụ, xét hàm tính tổng hai số nguyên trong Go:
package main
func Add(a, b int) int {
return a + b
}
Hàm Add() nhận hai số nguyên và trả về tổng của chúng. Ta có thể kiểm tra hành vi này mà không cần khởi chạy HTTP server hay kết nối database.
3.2. Test case, input và expected output
Một test case mô tả tình huống cần kiểm tra, bao gồm dữ liệu đầu vào và kết quả mong đợi.
Với hàm Add(), có thể xây dựng các test case:
| Input | Expected output |
|---|---|
Add(2, 3) |
5 |
Add(0, 5) |
5 |
Add(-2, 3) |
1 |
Add(-2, -3) |
-5 |
Mỗi test case kiểm tra một tình huống cụ thể. Kết quả thực tế được so sánh với kết quả mong đợi để xác định test có thành công hay không.
3.3. Ví dụ Unit Test trong Go
Go cung cấp package testing trong thư viện chuẩn để viết và chạy kiểm thử.
Tạo tệp math_test.go:
package main
import "testing"
func TestAdd(t *testing.T) {
got := Add(2, 3)
want := 5
if got != want {
t.Errorf("Add(2, 3) = %d; want %d", got, want)
}
}
Trong ví dụ này:
TestAddlà hàm kiểm thử.t *testing.Tcung cấp các phương thức để báo cáo kết quả kiểm thử.gotlà kết quả thực tế.wantlà kết quả mong đợi.t.Errorf()báo lỗi nếu hai kết quả không khớp, đồng thời cho phép test tiếp tục chạy.
Để chạy test:
go test
Hoặc chạy kiểm thử cho tất cả package trong module:
go test ./...
Nếu kết quả thực tế khớp với kết quả mong đợi, test sẽ vượt qua. Nếu không, Go sẽ báo test thất bại.
3.4. Kiểm tra các trường hợp biên và lỗi
Không nên chỉ kiểm thử trường hợp đầu vào thông thường. Những trường hợp biên và dữ liệu không hợp lệ cũng cần được xem xét.
Ví dụ, với hàm kiểm tra giá sản phẩm:
func IsValidPrice(price int) bool {
return price > 0
}
Các test case có thể gồm:
| Giá đầu vào | Kết quả mong đợi |
|---|---|
100 |
true |
1 |
true |
0 |
false |
-10 |
false |
Test cho những trường hợp này giúp xác minh hàm thực hiện đúng quy tắc nghiệp vụ đã định nghĩa.
Đối với những hàm phức tạp hơn, test case có thể cần bao gồm dữ liệu rỗng, giá trị lớn, lỗi xác thực hoặc những tổ hợp điều kiện đặc biệt.
4. Integration Testing hoạt động như thế nào?
4.1. Kiểm tra sự phối hợp giữa các thành phần
Trong một ứng dụng Backend, chức năng thường được chia thành nhiều tầng.
Ví dụ:
HTTP Request
|
v
Handler
|
v
Service
|
v
Repository
|
v
PostgreSQL
Mỗi thành phần có một nhiệm vụ riêng:
- Handler: tiếp nhận request và tạo response.
- Service: xử lý nghiệp vụ.
- Repository: thực hiện thao tác truy cập dữ liệu.
- Database: lưu trữ và truy xuất dữ liệu.
Unit Test có thể kiểm tra logic của từng thành phần riêng lẻ. Tuy nhiên, các thành phần vẫn có thể gặp lỗi khi kết hợp, chẳng hạn Handler truyền sai dữ liệu cho Service hoặc Repository tạo truy vấn SQL không đúng.
Integration Testing tập trung kiểm tra những tương tác đó.
Phạm vi của Integration Test có thể chỉ gồm Service và Repository, hoặc rộng hơn, từ HTTP request qua các tầng xử lý đến database. Không phải bài Integration Test nào cũng cần khởi chạy toàn bộ hệ thống.
4.2. Ví dụ Integration Test cho REST API
Giả sử Backend cung cấp endpoint:
POST /products
Content-Type: application/json
Request body:
{
"name": "Keyboard",
"price": 500000
}
Một Integration Test có thể thực hiện các bước sau:
- Khởi tạo môi trường kiểm thử và database thử nghiệm.
- Gửi HTTP request đến endpoint tạo sản phẩm.
- Để Handler, Service và Repository xử lý request như trong luồng thực tế.
- Kiểm tra HTTP status code và response body.
- Truy vấn database thử nghiệm để xác nhận sản phẩm đã được lưu đúng.
Ví dụ, test có thể xác minh response có status code 201 Created và sản phẩm được lưu với đúng tên cùng giá tiền.
Nếu request trả về thành công nhưng dữ liệu không được lưu, bài kiểm thử sẽ phát hiện sự không nhất quán đó.
4.3. Ví dụ Integration Test với Go
Trong Go, package net/http/httptest hỗ trợ kiểm thử HTTP handler mà không cần khởi chạy server thực sự trên một cổng mạng.
Ví dụ sau kiểm tra một handler đơn giản:
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func TestHealthEndpoint(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
router.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
})
})
req := httptest.NewRequest(
http.MethodGet,
"/health",
nil,
)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(
"status = %d; want %d",
rec.Code,
http.StatusOK,
)
}
}
Trong ví dụ này:
httptest.NewRequest()tạo HTTP request để kiểm thử.httptest.NewRecorder()ghi lại response.router.ServeHTTP()chuyển request qua Gin router.rec.Codechứa HTTP status code thực tế.
Đây là ví dụ kiểm thử handler và luồng định tuyến HTTP. Nó chưa phải Integration Test đầy đủ cho toàn bộ Backend vì không có Service, Repository hay database thực sự tham gia.
Để kiểm tra tích hợp với PostgreSQL, có thể mở một database thử nghiệm, cấu hình Repository sử dụng database đó, gửi request qua router và xác minh dữ liệu sau khi xử lý.
5. Phân biệt Unit Testing và Integration Testing
Hai phương pháp khác nhau chủ yếu ở phạm vi kiểm thử và những phụ thuộc được đưa vào quá trình kiểm tra.
| Tiêu chí | Unit Testing | Integration Testing |
|---|---|---|
| Mục tiêu | Kiểm tra hành vi của một đơn vị | Kiểm tra sự phối hợp giữa các thành phần |
| Phạm vi | Nhỏ, tập trung | Rộng hơn, tùy mục tiêu |
| Phụ thuộc bên ngoài | Thường được thay thế bằng test double hoặc loại bỏ | Có thể sử dụng database, HTTP client hoặc thành phần thật trong môi trường test |
| Tốc độ | Thường nhanh | Thường chậm hơn |
| Khoanh vùng lỗi | Thường dễ hơn | Có thể cần kiểm tra nhiều thành phần |
| Ví dụ | Kiểm tra quy tắc tính giá | Kiểm tra API tạo sản phẩm và lưu vào PostgreSQL |
Có thể dùng cùng một chức năng để thực hiện cả hai loại kiểm thử.
Với chức năng tạo sản phẩm:
- Unit Test kiểm tra Service từ chối sản phẩm có giá bằng
0. - Integration Test kiểm tra request tạo sản phẩm đi qua các tầng và dữ liệu được lưu chính xác.
Hai bài test có mục tiêu khác nhau nên không nhất thiết trùng lặp nhau.
6. Các khái niệm quan trọng khi viết test
6.1. Test case và test coverage
Test case là tình huống cụ thể cần kiểm tra, bao gồm đầu vào, điều kiện và kết quả mong đợi.
Ví dụ: khi giá sản phẩm bằng 0, hàm xác thực phải trả về kết quả không hợp lệ.
Test coverage là chỉ số mô tả phần code được thực thi trong quá trình chạy test. Với Go, có thể xem coverage bằng lệnh:
go test -cover ./...
Để tạo báo cáo coverage chi tiết:
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
Coverage cao cho biết nhiều code đã được thực thi, nhưng không chứng minh tất cả hành vi đều được kiểm tra chính xác.
Ví dụ, một test có thể chạy qua một hàm nhưng không xác minh kết quả của hàm đó. Vì vậy, chất lượng test còn phụ thuộc vào việc lựa chọn tình huống và kiểm tra kết quả mong đợi.
6.2. Test Double, Stub và Mock
Trong Unit Testing, một thành phần có thể phụ thuộc vào database, dịch vụ bên ngoài hoặc những thành phần khác. Nếu muốn kiểm tra logic riêng của nó, có thể thay thế các phụ thuộc đó bằng những đối tượng phục vụ kiểm thử.
Khái niệm chung thường được gọi là Test Double.
Một số loại phổ biến:
- Stub: cung cấp dữ liệu hoặc kết quả đã định trước để test sử dụng.
- Mock: thường được cấu hình để kiểm tra những lời gọi hoặc tương tác dự kiến giữa các thành phần.
- Fake: một triển khai đơn giản có thể hoạt động thực tế trong phạm vi kiểm thử, chẳng hạn repository lưu dữ liệu trong bộ nhớ thay cho database.
Ví dụ, khi kiểm thử Service, có thể thay Repository thật bằng một Fake Repository để kiểm tra logic nghiệp vụ mà không cần kết nối PostgreSQL.
Tuy nhiên, việc sử dụng Fake Repository không kiểm tra được truy vấn SQL thực tế. Phần tích hợp với database vẫn cần được kiểm tra bằng một bài test phù hợp.
6.3. Test fixture và test data
Test fixture là trạng thái hoặc dữ liệu được chuẩn bị để phục vụ một bài kiểm thử.
Ví dụ, khi kiểm thử chức năng cập nhật sản phẩm, fixture có thể tạo sẵn một sản phẩm trong database thử nghiệm trước khi gửi request cập nhật.
Test data là dữ liệu được sử dụng trong quá trình kiểm thử, chẳng hạn tên sản phẩm, giá tiền hoặc ID của người dùng.
Dữ liệu kiểm thử nên được thiết kế rõ ràng, có thể tái tạo và không phụ thuộc vào dữ liệu production.
6.4. Assertion và expected result
Assertion là thao tác kiểm tra một điều kiện hoặc kết quả có đúng với mong đợi hay không.
Ví dụ:
got := Add(2, 3)
want := 5
if got != want {
t.Errorf("got %d; want %d", got, want)
}
Điều kiện got != want là phép so sánh được dùng để xác định test có thất bại hay không.
Go không bắt buộc phải sử dụng thư viện assertion bên ngoài. Có thể kiểm tra kết quả trực tiếp bằng các câu lệnh điều kiện và phương thức của testing.T.
7. Cách tổ chức và chạy test trong Go
7.1. Tệp _test.go
Go nhận diện các tệp kiểm thử dựa trên hậu tố _test.go.
Ví dụ:
product/
├── product.go
├── product_test.go
├── service.go
└── service_test.go
Thông thường, test được đặt gần code cần kiểm thử để dễ tìm và bảo trì.
Tùy nhu cầu, test có thể nằm cùng package với code được kiểm tra hoặc trong một package test riêng để giới hạn việc truy cập vào các thành phần nội bộ.
7.2. Package testing
Package testing của Go cung cấp các công cụ để viết và chạy test, bao gồm:
testing.T: phục vụ kiểm thử thông thường.t.Errorf()vàt.Fatalf(): báo lỗi khi điều kiện không đạt.t.Run(): tổ chức các test case con.t.Parallel(): cho phép một test chạy song song khi phù hợp.
Một hàm test thông thường có dạng:
func TestFunctionName(t *testing.T) {
// Chuẩn bị dữ liệu.
// Thực hiện hành động.
// Kiểm tra kết quả.
}
Tên hàm test thường bắt đầu bằng Test và có tham số *testing.T.
7.3. Chạy test bằng go test
Một số lệnh thường dùng:
# Chạy test trong package hiện tại
go test
# Chạy test trong toàn bộ module
go test ./...
# Hiển thị chi tiết từng test
go test -v ./...
# Chạy test và hiển thị coverage
go test -cover ./...
Những lệnh này giúp lập trình viên chạy lại các bài kiểm thử sau khi thay đổi code và phát hiện lỗi sớm.
7.4. Table-driven tests
Table-driven testing là cách tổ chức nhiều test case trong một bảng dữ liệu, sau đó lặp qua từng trường hợp để kiểm tra cùng một hàm.
Ví dụ:
func TestIsValidPrice(t *testing.T) {
tests := []struct {
name string
price int
want bool
}{
{
name: "positive price",
price: 100,
want: true,
},
{
name: "zero price",
price: 0,
want: false,
},
{
name: "negative price",
price: -10,
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := IsValidPrice(tt.price)
if got != tt.want {
t.Errorf(
"IsValidPrice(%d) = %t; want %t",
tt.price,
got,
tt.want,
)
}
})
}
}
Cách viết này giúp các test case có cấu trúc thống nhất, dễ bổ sung và dễ đọc.
7.5. Thiết lập môi trường Integration Test
Integration Test thường cần thiết lập các thành phần mà chương trình sử dụng trong thực tế, chẳng hạn database hoặc dịch vụ HTTP thử nghiệm.
Một quy trình thường gặp:
- Chuẩn bị môi trường test riêng.
- Khởi tạo cấu hình và dữ liệu cần thiết.
- Khởi chạy những thành phần cần kiểm tra.
- Thực hiện request hoặc thao tác cần kiểm thử.
- Xác minh kết quả.
- Dọn dẹp dữ liệu và tài nguyên sau khi test hoàn thành.
Với PostgreSQL, có thể sử dụng một database riêng cho test hoặc một database tạm thời được khởi tạo trong quá trình chạy kiểm thử.
Điều quan trọng là không để Integration Test sửa đổi dữ liệu production và cần bảo đảm các test có thể chạy lại một cách ổn định.
8. Những lưu ý khi viết Unit Test và Integration Test
8.1. Không phụ thuộc vào dữ liệu production
Test nên sử dụng dữ liệu được tạo riêng cho môi trường kiểm thử.
Nếu một test phụ thuộc vào bản ghi có sẵn trong database thật, kết quả có thể thay đổi khi dữ liệu bị sửa hoặc xóa. Điều này khiến test khó tái tạo và có nguy cơ làm ảnh hưởng dữ liệu thực tế.
8.2. Test phải ổn định và có thể chạy lặp lại
Một bài test tốt nên cho kết quả nhất quán khi chạy nhiều lần trong cùng điều kiện.
Để đạt được điều đó, cần hạn chế những phụ thuộc không được kiểm soát như thời gian thực, dữ liệu dùng chung, thứ tự thực thi hoặc dịch vụ bên ngoài không ổn định.
Đối với test sử dụng database, nên chuẩn bị và dọn dẹp dữ liệu phù hợp để các lần chạy không ảnh hưởng lẫn nhau.
8.3. Tránh kiểm thử quá nhiều chi tiết triển khai
Test nên tập trung vào hành vi cần bảo đảm thay vì phụ thuộc quá mức vào cấu trúc nội bộ của code.
Ví dụ, nếu chỉ cần xác minh Service từ chối giá âm, test không nhất thiết phải kiểm tra từng lời gọi nội bộ nếu những chi tiết đó không thuộc yêu cầu cần kiểm tra.
Khi test phụ thuộc quá nhiều vào cách triển khai, việc tái cấu trúc code có thể khiến hàng loạt test thất bại dù hành vi bên ngoài vẫn đúng.
8.4. Không nhầm coverage cao với chất lượng test cao
Coverage là chỉ số hữu ích nhưng không phải thước đo duy nhất của chất lượng kiểm thử.
Một test có thể thực thi toàn bộ các dòng code nhưng không phát hiện lỗi vì không kiểm tra kết quả hoặc không bao gồm những tình huống quan trọng.
Vì vậy, cần chú trọng đến chất lượng test case, các điều kiện biên, những lỗi có khả năng xảy ra và mức độ quan trọng của từng chức năng.
8.5. Không nhất thiết phải mock mọi phụ thuộc
Mock có thể giúp cô lập logic, nhưng việc sử dụng quá nhiều mock có thể khiến test không phản ánh đúng cách các thành phần tương tác trong thực tế.
Nên dùng mock hoặc stub khi cần kiểm tra logic của một đơn vị độc lập; đồng thời sử dụng Integration Test để xác minh những tương tác quan trọng với database, HTTP hoặc các thành phần khác.
9. Kết luận
Unit Testing và Integration Testing là hai phương pháp kiểm thử bổ trợ cho nhau trong quá trình phát triển phần mềm.
Unit Testing giúp kiểm tra logic của từng đơn vị và phát hiện lỗi trong phạm vi nhỏ. Integration Testing giúp xác minh các thành phần phối hợp đúng khi được kết nối với nhau, chẳng hạn từ HTTP Handler qua Service và Repository đến database.
Trong Go, package testing, các tệp _test.go, lệnh go test và table-driven tests cung cấp nền tảng để tổ chức và thực hiện kiểm thử. Khi cần kiểm tra sự tương tác giữa các thành phần, lập trình viên có thể thiết lập môi trường Integration Test phù hợp với hệ thống.
Tuy nhiên, mục tiêu không phải là viết càng nhiều test càng tốt hoặc đạt coverage tuyệt đối bằng mọi giá. Điều quan trọng là lựa chọn đúng phạm vi, kiểm tra những hành vi có ý nghĩa và bảo đảm test có thể chạy lại một cách đáng tin cậy.
Viết code giúp triển khai chức năng; kiểm thử giúp xác minh chức năng đó hoạt động đúng trong những tình huống đã xác định và hỗ trợ bảo vệ hành vi của hệ thống khi code tiếp tục thay đổi.








Khoá học lập trình game con rắn cho trẻ em




