Skip to content

❓ 测试覆盖

问题

ipapi.co-skills Go SDK 的单元测试覆盖率达到多少?怎么跑测试、怎么生成覆盖率报告?

简答

✅ 核心包 pkg/ipapi 达到 100.0% 单元测试覆盖率,使用 httptest 模拟真实 API 服务端,无需联网即可全量跑通。

🎨 一图抵千言

下图说明 SDK 测试如何用 httptest 隔离外部依赖,并覆盖全部分支。

SDK 把所有与外部网络相关的逻辑都用 net/http/httptest 起一个本地 mock 服务器来验证,因此测试不依赖 ipapi.co 真实服务,也不会消耗免费额度,可以离线、可重复地在 CI 中运行。

实际执行 go test ./pkg/ipapi/ -cover 的输出为:

text
ok  	github.com/cyberspacesec/ipapi.co-skills/pkg/ipapi	3.146s	coverage: 100.0% of statements

🧪 测试规模一览

📁 源文件🧩 函数数✅ 对应测试
pkg/ipapi/api.go12api_test.go(112 个测试函数)
pkg/ipapi/client.go6client_test.go(7 个测试函数)
pkg/ipapi/errors.go3含于上述测试
pkg/ipapi/models.go5含于上述测试

合计 119 个测试函数,覆盖请求构造、字段映射、重试、错误处理、JSONP、自定义 HTTPClientcontext 取消等全部路径。

📊 测试与源码的对应关系

SDK 的测试遵循 Go 惯例——xxx_test.goxxx.go 同包同名前缀,方便定位:

源文件测试文件重点覆盖点
api.goapi_test.go6 方法、5 格式、重试、状态码映射、字段解析
client.goclient_test.goClient 配置、选项注入、HTTPClient 替换
errors.go含于上述哨兵错误映射、errors.Is
models.go含于上述IPInfo 28 字段、Postal *string 语义

🏃 怎么跑测试

最常用的方式——直接运行全部测试:

bash
go test ./pkg/ipapi/ -v

📊 生成覆盖率报告

加上 -cover-coverprofile 即可输出覆盖率profile文件:

bash
# 1) 生成 coverage.out
go test ./pkg/ipapi/ -cover -coverprofile=coverage.out

# 2) 在终端查看每个函数的覆盖率
go tool cover -func=coverage.out

# 3) 生成可在浏览器查看的 HTML 报告
go tool cover -html=coverage.out -o coverage.html

go tool cover -func 会逐个函数列出覆盖百分比,结尾一行给出整体 total: 100.0%

🛠️ 测试是怎么写的(真实示例片段)

SDK 的测试通过 httptest.NewServer 起一个可控的 mock 服务端,校验请求路径并返回预设 JSON,从而完全隔离外部依赖

go
package ipapi

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestClient_GetIPInfo_Success(t *testing.T) {
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// 校验请求路径是否符合预期
		if r.URL.Path != "/8.8.8.8/json/" {
			t.Errorf("unexpected path: %s", r.URL.Path)
		}
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprint(w, `{
			"ip": "8.8.8.8",
			"city": "Mountain View",
			"country": "US",
			"latitude": 37.3860,
			"longitude": -122.0838
		}`)
	}))
	defer ts.Close()

	// 用 mock 服务端地址构造客户端,避免真实网络调用
	client := NewClient()
	client.BaseURL = ts.URL

	info, err := client.GetIPInfo("8.8.8.8")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if info.IP != "8.8.8.8" {
		t.Errorf("expected ip 8.8.8.8, got %s", info.IP)
	}
	if info.Country != "US" {
		t.Errorf("expected country US, got %s", info.Country)
	}
}

同样的套路还用于覆盖以下场景,确保每条分支都不被遗漏:

  • 🌐 成功路径:正常返回 JSON、字段正确反序列化
  • 🚨 服务端错误:返回 5xx 触发重试,重试用尽后返回包装错误
  • 客户端错误:返回 4xx(如 401 / 404)不重试、直接透传
  • 🔁 网络错误http.Client 返回连接/超时错误时按重试策略重试
  • ⏱️ Context 取消:传入带超时或 cancel()context.Context 时立即返回
  • 🔧 自定义 HTTPClient / ErrorHandler:通过 WithOptions 注入后行为符合预期

🧬 覆盖率怎么解读

go test -coverprofile 生成的 coverage.out 中,每条记录结尾的数字是该代码块被执行的次数。当这个值在所有行上都不为 0 时,即代表 100% 语句覆盖。SDK 当前 profile 中不存在任何计数为 0 的代码块,配合 go tool cover -functotal: 100.0% 可双重确认。

💡 100% 覆盖率 ≠ 无 bug

100% 语句覆盖保证的是每条语句至少被一组测试执行过,但不覆盖:

  • 🧪 边界值:空字符串、极端经纬度等需额外断言
  • 并发竞态:需配合 -race 检测数据竞争
  • 🌍 真实网络抖动:mock 无法复现的真实超时/重置

建议定期跑以下命令做并发与回归验证:

bash
go test ./pkg/ipapi/ -race -count=10

⚠️ 离线测试的边界

httptest mock 无法模拟以下真实场景,必要时需补充集成测试:

  • TLS 握手失败、证书过期
  • 真实的 429 Retry-After 头部延迟
  • 跨地域 DNS 解析差异

相关

基于 MIT 许可证发布