# 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_ 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) ```