MPC 分布式签名系统 - 测试指南
1. 测试架构概览
┌─────────────────────────────────────────────────────────────────────┐
│ 测试金字塔 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ │
│ │ E2E │ ← 端到端测试 (最慢, 最全面) │
│ │ Tests │ tests/e2e/ │
│ ┌─┴─────────┴─┐ │
│ │ Integration │ ← 集成测试 (服务间交互) │
│ │ Tests │ tests/integration/ │
│ ┌─┴─────────────┴─┐ │
│ │ Unit Tests │ ← 单元测试 (最快, 最多) │
│ │ │ tests/unit/ │
│ └─────────────────┘ *_test.go │
│ │
└─────────────────────────────────────────────────────────────────────┘
1.1 测试类型
| 类型 |
位置 |
特点 |
运行时间 |
| 单元测试 |
tests/unit/, *_test.go |
测试单个函数/模块 |
< 1s |
| 集成测试 |
tests/integration/ |
测试 TSS 协议流程 |
1-5 min |
| E2E 测试 |
tests/e2e/ |
测试完整 HTTP API 流程 |
5-10 min |
1.2 测试工具
| 工具 |
用途 |
| testing |
Go 标准测试框架 |
| testify |
断言和 Mock |
| httptest |
HTTP 测试 |
| gomock |
Mock 生成 |
2. 单元测试
2.1 运行单元测试
# 运行所有单元测试
make test-unit
# 或使用 go test
go test -v -short ./...
# 运行特定包
go test -v ./pkg/crypto/...
go test -v ./services/account/domain/...
# 运行特定测试
go test -v -run TestEncryption ./pkg/crypto/...
2.2 单元测试示例
// pkg/crypto/encryption_test.go
package crypto_test
import (
"testing"
"github.com/rwadurian/mpc-system/pkg/crypto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAESCryptoService_EncryptDecrypt(t *testing.T) {
// Arrange
masterKey := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
svc, err := crypto.NewAESCryptoService(masterKey)
require.NoError(t, err)
plaintext := []byte("secret key share data")
// Act
ciphertext, err := svc.Encrypt(plaintext)
require.NoError(t, err)
decrypted, err := svc.Decrypt(ciphertext)
require.NoError(t, err)
// Assert
assert.Equal(t, plaintext, decrypted)
assert.NotEqual(t, plaintext, ciphertext)
}
func TestAESCryptoService_InvalidKey(t *testing.T) {
testCases := []struct {
name string
key string
}{
{"too short", "abcd"},
{"invalid hex", "xyz123"},
{"empty", ""},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, err := crypto.NewAESCryptoService(tc.key)
assert.Error(t, err)
})
}
}
2.3 Mock 使用
// tests/mocks/session_repository_mock.go
type MockSessionRepository struct {
mock.Mock
}
func (m *MockSessionRepository) Save(ctx context.Context, session *entities.Session) error {
args := m.Called(ctx, session)
return args.Error(0)
}
func (m *MockSessionRepository) FindByID(ctx context.Context, id uuid.UUID) (*entities.Session, error) {
args := m.Called(ctx, id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*entities.Session), args.Error(1)
}
// 使用 Mock
func TestCreateSession_Success(t *testing.T) {
mockRepo := new(MockSessionRepository)
mockRepo.On("Save", mock.Anything, mock.Anything).Return(nil)
uc := use_cases.NewCreateSessionUseCase(mockRepo)
output, err := uc.Execute(context.Background(), input)
assert.NoError(t, err)
mockRepo.AssertExpectations(t)
}
3. 集成测试
3.1 TSS 协议集成测试
集成测试验证完整的 MPC 协议流程,无需外部服务。
# 运行所有集成测试
make test-integration
# 或
go test -v -tags=integration ./tests/integration/...
# 运行特定测试
go test -v ./tests/integration/... -run "TestFull2of3MPCFlow"
go test -v ./tests/integration/... -run "Test3of5Flow"
go test -v ./tests/integration/... -run "Test4of7Flow"
3.2 集成测试示例
// tests/integration/mpc_full_flow_test.go
package integration_test
import (
"crypto/ecdsa"
"crypto/sha256"
"testing"
"github.com/rwadurian/mpc-system/pkg/tss"
"github.com/stretchr/testify/require"
)
func TestFull2of3MPCFlow(t *testing.T) {
// Step 1: Key Generation (2-of-3)
threshold := 1 // t=1 means t+1=2 signers required
totalParties := 3
keygenResults, err := tss.RunLocalKeygen(threshold, totalParties)
require.NoError(t, err)
require.Len(t, keygenResults, 3)
publicKey := keygenResults[0].PublicKey
require.NotNil(t, publicKey)
// Verify all parties have same public key
for i, result := range keygenResults {
require.Equal(t, publicKey.X, result.PublicKey.X, "Party %d X mismatch", i)
require.Equal(t, publicKey.Y, result.PublicKey.Y, "Party %d Y mismatch", i)
}
// Step 2: Signing with 2 parties
message := []byte("Hello MPC World!")
messageHash := sha256.Sum256(message)
// Test all 3 combinations of 2 parties
combinations := [][2]int{{0, 1}, {0, 2}, {1, 2}}
for _, combo := range combinations {
signers := []*tss.LocalKeygenResult{
keygenResults[combo[0]],
keygenResults[combo[1]],
}
signResult, err := tss.RunLocalSigning(threshold, signers, messageHash[:])
require.NoError(t, err)
// Step 3: Verify signature
valid := ecdsa.Verify(publicKey, messageHash[:], signResult.R, signResult.S)
require.True(t, valid, "Signature should verify for combo %v", combo)
}
}
func TestSecurityProperties(t *testing.T) {
threshold := 1
totalParties := 3
keygenResults, err := tss.RunLocalKeygen(threshold, totalParties)
require.NoError(t, err)
message := []byte("Security test")
messageHash := sha256.Sum256(message)
// Test: Single party cannot sign
singleParty := []*tss.LocalKeygenResult{keygenResults[0]}
_, err = tss.RunLocalSigning(threshold, singleParty, messageHash[:])
require.Error(t, err, "Single party should not sign")
}
3.3 已验证的阈值方案
| 方案 |
参数 |
密钥生成耗时 |
签名耗时 |
状态 |
| 2-of-3 |
t=1, n=3 |
~93s |
~80s |
PASSED |
| 3-of-5 |
t=2, n=5 |
~198s |
~120s |
PASSED |
| 4-of-7 |
t=3, n=7 |
~221s |
~150s |
PASSED |
4. E2E 测试
4.1 E2E 测试架构
┌─────────────────────────────────────────────────────────────┐
│ E2E Test Runner │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Test Suite (testify/suite) │ │
│ │ - SetupSuite: 启动服务, 等待就绪 │ │
│ │ - TearDownSuite: 清理资源 │ │
│ │ - Test*: 测试用例 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼ HTTP Requests
┌─────────────────────────────────────────────────────────────┐
│ Docker Compose │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │Coordinator│ │ Router │ │ Party×3 │ │PostgreSQL│ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────┘
4.2 运行 E2E 测试
# 使用 Docker 运行 E2E 测试
make test-docker-e2e
# 手动运行 (需要先启动服务)
docker-compose up -d
go test -v -tags=e2e ./tests/e2e/...
# 运行特定 E2E 测试
go test -v -tags=e2e ./tests/e2e/... -run "TestCompleteKeygenFlow"
4.3 E2E 测试示例
// tests/e2e/keygen_flow_test.go
//go:build e2e
package e2e_test
import (
"bytes"
"encoding/json"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/suite"
)
type KeygenFlowTestSuite struct {
suite.Suite
baseURL string
client *http.Client
}
func TestKeygenFlowSuite(t *testing.T) {
if testing.Short() {
t.Skip("Skipping e2e test in short mode")
}
suite.Run(t, new(KeygenFlowTestSuite))
}
func (s *KeygenFlowTestSuite) SetupSuite() {
s.baseURL = "http://localhost:8080"
s.client = &http.Client{Timeout: 30 * time.Second}
s.waitForService()
}
func (s *KeygenFlowTestSuite) waitForService() {
for i := 0; i < 30; i++ {
resp, err := s.client.Get(s.baseURL + "/health")
if err == nil && resp.StatusCode == http.StatusOK {
resp.Body.Close()
return
}
time.Sleep(time.Second)
}
s.T().Fatal("Service not ready")
}
func (s *KeygenFlowTestSuite) TestCompleteKeygenFlow() {
// Step 1: Create session
createResp := s.createSession(CreateSessionRequest{
SessionType: "keygen",
ThresholdT: 2,
ThresholdN: 3,
CreatedBy: "e2e_test",
})
s.Require().NotEmpty(createResp.SessionID)
// Step 2: Join with 3 parties
for i := 0; i < 3; i++ {
joinResp := s.joinSession(JoinSessionRequest{
JoinToken: createResp.JoinToken,
PartyID: fmt.Sprintf("party_%d", i),
DeviceType: "test",
})
s.Assert().Equal(createResp.SessionID, joinResp.SessionID)
}
// Step 3: Mark all parties ready
for i := 0; i < 3; i++ {
s.markPartyReady(createResp.SessionID, fmt.Sprintf("party_%d", i))
}
// Step 4: Start session
s.startSession(createResp.SessionID)
// Step 5: Verify session status
status := s.getSessionStatus(createResp.SessionID)
s.Assert().Equal("in_progress", status.Status)
}
func (s *KeygenFlowTestSuite) TestJoinWithInvalidToken() {
resp, err := s.client.Post(
s.baseURL+"/api/v1/sessions/join",
"application/json",
bytes.NewReader([]byte(`{"join_token":"invalid"}`)),
)
s.Require().NoError(err)
defer resp.Body.Close()
s.Assert().Equal(http.StatusUnauthorized, resp.StatusCode)
}
4.4 Docker E2E 测试配置
# tests/docker-compose.test.yml
version: '3.8'
services:
postgres-test:
image: postgres:14-alpine
environment:
POSTGRES_USER: mpc_user
POSTGRES_PASSWORD: mpc_password
POSTGRES_DB: mpc_system_test
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 5s
timeout: 5s
retries: 5
integration-tests:
build:
context: ..
dockerfile: tests/Dockerfile.test
environment:
TEST_DATABASE_URL: postgres://mpc_user:mpc_password@postgres-test:5432/mpc_system_test
depends_on:
postgres-test:
condition: service_healthy
command: go test -v ./tests/integration/...
e2e-tests:
build:
context: ..
dockerfile: tests/Dockerfile.test
environment:
SESSION_COORDINATOR_URL: http://session-coordinator:8080
depends_on:
- session-coordinator
- message-router
- server-party-1
- server-party-2
- server-party-3
command: go test -v -tags=e2e ./tests/e2e/...
5. 测试覆盖率
5.1 生成覆盖率报告
# 运行测试并生成覆盖率
make test-coverage
# 或手动
go test -v -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
# 查看覆盖率
open coverage.html
5.2 覆盖率目标
| 模块 |
目标覆盖率 |
说明 |
| pkg/tss |
> 80% |
核心加密逻辑 |
| pkg/crypto |
> 90% |
加密工具 |
| domain |
> 85% |
业务规则 |
| use_cases |
> 75% |
用例编排 |
| adapters |
> 60% |
I/O 适配 |
6. 手动测试
6.1 使用 cURL 测试 API
# 健康检查
curl http://localhost:8080/health
# 创建 keygen 会话
curl -X POST http://localhost:8083/api/v1/mpc/keygen \
-H "Content-Type: application/json" \
-d '{
"threshold_n": 3,
"threshold_t": 2,
"participants": [
{"party_id": "user_device", "device_type": "iOS"},
{"party_id": "server_party", "device_type": "server"},
{"party_id": "recovery", "device_type": "recovery"}
]
}'
# 查询会话状态
curl http://localhost:8083/api/v1/mpc/sessions/{session_id}
6.2 使用 grpcurl 测试 gRPC
# 安装 grpcurl
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
# 列出服务
grpcurl -plaintext localhost:50051 list
# 创建会话
grpcurl -plaintext -d '{
"session_type": "keygen",
"threshold_n": 3,
"threshold_t": 2
}' localhost:50051 mpc.coordinator.v1.SessionCoordinator/CreateSession
7. 持续集成
7.1 GitHub Actions 配置
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Run unit tests
run: make test-unit
integration-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Run integration tests
run: make test-integration
timeout-minutes: 30
e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run E2E tests in Docker
run: make test-docker-e2e
timeout-minutes: 30
8. 测试最佳实践
8.1 测试命名
// 函数测试: Test<Function>_<Scenario>
func TestEncrypt_WithValidKey(t *testing.T) {}
func TestEncrypt_WithInvalidKey(t *testing.T) {}
// 表驱动测试
func TestEncrypt(t *testing.T) {
testCases := []struct {
name string
key string
input []byte
wantErr bool
}{
{"valid key", "abc123...", []byte("data"), false},
{"empty key", "", []byte("data"), true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// test logic
})
}
}
8.2 测试隔离
// 使用 t.Parallel() 并行运行
func TestSomething(t *testing.T) {
t.Parallel()
// ...
}
// 使用 t.Cleanup() 清理
func TestWithCleanup(t *testing.T) {
resource := createResource()
t.Cleanup(func() {
resource.Close()
})
}
8.3 避免 Flaky 测试
// 使用重试机制
func waitForCondition(t *testing.T, check func() bool, timeout time.Duration) {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if check() {
return
}
time.Sleep(100 * time.Millisecond)
}
t.Fatal("condition not met within timeout")
}
// 使用固定种子
rand.Seed(42)