rwadurian/backend/mpc-system/docs/04-testing-guide.md

17 KiB
Raw Permalink Blame History

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)