597 lines
17 KiB
Markdown
597 lines
17 KiB
Markdown
# 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 运行单元测试
|
||
|
||
```bash
|
||
# 运行所有单元测试
|
||
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 单元测试示例
|
||
|
||
```go
|
||
// 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 使用
|
||
|
||
```go
|
||
// 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 协议流程,无需外部服务。
|
||
|
||
```bash
|
||
# 运行所有集成测试
|
||
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 集成测试示例
|
||
|
||
```go
|
||
// 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 测试
|
||
|
||
```bash
|
||
# 使用 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 测试示例
|
||
|
||
```go
|
||
// 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 测试配置
|
||
|
||
```yaml
|
||
# 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 生成覆盖率报告
|
||
|
||
```bash
|
||
# 运行测试并生成覆盖率
|
||
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
|
||
|
||
```bash
|
||
# 健康检查
|
||
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
|
||
|
||
```bash
|
||
# 安装 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 配置
|
||
|
||
```yaml
|
||
# .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 测试命名
|
||
|
||
```go
|
||
// 函数测试: 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 测试隔离
|
||
|
||
```go
|
||
// 使用 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 测试
|
||
|
||
```go
|
||
// 使用重试机制
|
||
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)
|
||
```
|