验证什么
集成测试让两个或多个真实组件通过实际接口协作:应用与数据库、生产者与消息代理、浏览器与 API。单元测试用替身替换这些协作者,因此发现不了列类型错误、缺失迁移、队列名配置错误或序列化不匹配。集成测试正是为了捕获这类装配缺陷。
窄集成与宽集成
宽集成测试启动许多服务、接近生产环境,但速度慢且难以定位故障。窄测试把边界控制得很小:一个服务加真实数据库,或一个消费者加真实消息代理。与端到端测试相比,窄集成测试运行更快,能直接指向损坏的接缝。多数团队需要许多窄测试和少量宽路径测试。
契约测试不能替代
契约测试证明消费者的期望与提供者接口一致,却不能证明提供者能连上数据库、凭证有效或事务已经提交。接口漂移用契约测试,真实基础设施行为用集成测试。
实践建议与权衡
用容器或临时环境提供依赖,按测试隔离数据并在运行之间重置状态,使用真实夹具而不是空 schema,断言持久化状态和发出的消息而不只是响应码。如果套件能在几分钟内完成,就在每个拉取请求上运行;否则放在合并或部署前。集成测试消耗基础设施和时间,也是不稳定测试的高发区。例如,重试三次才通过的套件可能掩盖代码或测试框架中的竞态。只在配置和真实协议带来风险的地方使用,其余保持快速。
代码示例
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"sync/atomic"
)
type StubResponse struct {
Status int
Body string
}
func NewStubServer(responses map[string]StubResponse, hits *int64) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if hits != nil {
atomic.AddInt64(hits, 1)
}
response, ok := responses[r.URL.Path]
if !ok {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(response.Status)
_, _ = io.WriteString(w, response.Body)
}))
}
func main() {
var hits int64
server := NewStubServer(map[string]StubResponse{
"/users/1": {Status: http.StatusOK, Body: `{"id":1,"name":"ada"}`},
"/health": {Status: http.StatusOK, Body: `{"status":"ok"}`},
}, &hits)
defer server.Close()
for _, requestPath := range []string{"/users/1", "/health", "/missing"} {
response, err := http.Get(server.URL + requestPath)
if err != nil {
panic(err)
}
body, err := io.ReadAll(response.Body)
response.Body.Close()
if err != nil {
panic(err)
}
fmt.Printf("%s -> %d %s\n", requestPath, response.StatusCode, string(body))
}
fmt.Println("total requests:", atomic.LoadInt64(&hits))
}
package main
import (
"errors"
"testing"
)
var errNotFound = errors.New("order not found")
func ShippingCost(weightKg int, express bool, tier string) (int, error) {
if weightKg <= 0 {
return 0, errors.New("weight must be positive")
}
if weightKg > 30 {
return 0, errors.New("weight exceeds courier limit")
}
base := 8
if weightKg > 10 {
base += (weightKg - 10) * 2
}
if express {
base *= 2
}
if tier == "gold" {
base -= 4
}
return base, nil
}
func TestShippingCost(t *testing.T) {
t.Parallel()
tests := []struct {
name string
weight int
express bool
tier string
want int
wantErr error
}{
{name: "light standard", weight: 2, express: false, tier: "", want: 8},
{name: "light express", weight: 2, express: true, tier: "", want: 16},
{name: "heavy adds per kg", weight: 15, express: false, tier: "", want: 18},
{name: "heavy express gold", weight: 15, express: true, tier: "gold", want: 32},
{name: "gold floor at zero", weight: 1, express: false, tier: "gold", want: 4},
{name: "zero weight rejected", weight: 0, express: false, tier: "", wantErr: errors.New("weight must be positive")},
{name: "over courier limit", weight: 31, express: false, tier: "", wantErr: errors.New("weight exceeds courier limit")},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := ShippingCost(tt.weight, tt.express, tt.tier)
if tt.wantErr != nil {
if err == nil || err.Error() != tt.wantErr.Error() {
t.Fatalf("ShippingCost(%d, %v, %q) error = %v, want %v",
tt.weight, tt.express, tt.tier, err, tt.wantErr)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tt.want {
t.Errorf("ShippingCost(%d, %v, %q) = %d, want %d",
tt.weight, tt.express, tt.tier, got, tt.want)
}
})
}
}