You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

模型专属操作与数据库操作的分离设计及测试Mock优化咨询

优化模型与存储层解耦的建模方案

针对你遇到的模型操作与数据库操作耦合、测试困难以及接口设计的代码异味问题,我们可以通过职责拆分+接口聚焦的方式来重构,既解决测试耦合问题,又符合SOLID设计原则。

核心思路:让模型归模型,存储归存储

我们需要把User的核心行为(密码验证、摘要生成、时间戳设置)完全封装在模型内部,存储层只依赖模型的只读状态,而不是一个大而全的UserAction接口。这样既能保证模型的内聚性,又能让存储层的测试彻底解耦。

一、重构User模型:纯数据载体+核心行为

让User结构体专注于自身的状态管理和业务规则,所有模型专属操作都作为结构体方法,只修改自身状态,不涉及数据库:

import (
    "time"
    "golang.org/x/crypto/bcrypt"
)

type modelError struct {
    Field   string
    Message string
}

func (e *modelError) Error() string {
    return e.Field + ": " + e.Message
}

type User struct {
    ID                   int       `json:"id"`
    Email                string    `json:"email"`
    Password             string    `json:"password"`
    ConfirmationPassword string    `json:"confirmationPassword"`
    passwordDigest       string    `json:"-"` // 保持私有,通过方法访问
    CreatedAt            time.Time `json:"createdAt,omitempty"`
    ModifiedAt           time.Time `json:"modifiedAt,omitempty"`
}

// 模型专属操作:验证密码规则
func (u *User) VerifyPassword() error {
    if len(u.Password) < 6 {
        return &modelError{"Password", "must be at least 6 characters long"}
    }
    if u.Password != u.ConfirmationPassword {
        return &modelError{"ConfirmationPassword", "does not match Password"}
    }
    return nil
}

// 模型专属操作:生成密码摘要(依赖VerifyPassword)
func (u *User) GeneratePasswordDigest() error {
    if err := u.VerifyPassword(); err != nil {
        return err
    }
    digestBytes, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
    if err != nil {
        return err
    }
    u.passwordDigest = string(digestBytes)
    return nil
}

// 模型专属操作:设置时间戳
func (u *User) SetTimestamps() {
    now := time.Now()
    u.CreatedAt = now
    u.ModifiedAt = now
}

// 只读属性访问器:给存储层提供必要的状态
func (u *User) GetEmail() string {
    return u.Email
}

func (u *User) GetPasswordDigest() string {
    return u.passwordDigest
}

func (u *User) GetCreatedAt() time.Time {
    return u.CreatedAt
}

func (u *User) GetModifiedAt() time.Time {
    return u.ModifiedAt
}

二、重构存储层接口:聚焦数据库操作

让UserStore接口只定义数据库交互逻辑,不再依赖宽泛的UserAction,而是直接接收存储所需的参数:

type UserStore interface {
    GetUserByEmailAndPassword(email, passwordDigest string) (User, error)
    UpdatePassword(userID int, newPasswordDigest string) error
    UserExists(email string) (bool, error)
    CreateUser(email, passwordDigest string, createdAt, modifiedAt time.Time) (int, error) // 返回生成的用户ID
}

对应的数据库实现示例:

type DB struct {
    // 数据库连接实例
}

func (db *DB) CreateUser(email, passwordDigest string, createdAt, modifiedAt time.Time) (int, error) {
    exists, err := db.UserExists(email)
    if err != nil {
        return 0, err
    }
    if exists {
        return 0, &modelError{"Email", "already exists in the system"}
    }

    var userID int
    err = db.QueryRow(`
        INSERT INTO users (email, password_digest, created_at, modified_at)
        VALUES ($1, $2, $3, $4) RETURNING id
    `, email, passwordDigest, createdAt, modifiedAt).Scan(&userID)
    if err != nil {
        return 0, err
    }
    return userID, nil
}

三、新增业务协调层:串联模型与存储

新增一个业务逻辑函数(或服务),负责协调模型操作和存储操作,这样既保证了流程的完整性,又让各层职责清晰:

func CreateNewUser(user *User, store UserStore) error {
    // 1. 执行模型专属操作
    if err := user.VerifyPassword(); err != nil {
        return err
    }
    if err := user.GeneratePasswordDigest(); err != nil {
        return err
    }
    user.SetTimestamps()

    // 2. 执行数据库操作
    userID, err := store.CreateUser(
        user.GetEmail(),
        user.GetPasswordDigest(),
        user.GetCreatedAt(),
        user.GetModifiedAt(),
    )
    if err != nil {
        return err
    }
    user.ID = userID
    return nil
}

四、测试方案彻底解耦

1. 模型单元测试:无需数据库

直接测试User的方法,完全独立于存储层:

import "testing"

func TestUser_VerifyPassword(t *testing.T) {
    testCases := []struct {
        name     string
        user     User
        expectErr bool
    }{
        {
            name:     "密码长度不足6位",
            user:     User{Password: "12345", ConfirmationPassword: "12345"},
            expectErr: true,
        },
        {
            name:     "密码与确认密码不匹配",
            user:     User{Password: "abc123", ConfirmationPassword: "abc456"},
            expectErr: true,
        },
        {
            name:     "密码验证通过",
            user:     User{Password: "abc123", ConfirmationPassword: "abc123"},
            expectErr: false,
        },
    }

    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            u := &tc.user
            err := u.VerifyPassword()
            if (err != nil) != tc.expectErr {
                t.Fatalf("VerifyPassword() error = %v, expectErr %v", err, tc.expectErr)
            }
        })
    }
}

2. 存储层测试:Mock或使用测试数据库

存储层的CreateUser只依赖传入的参数,测试时可以直接传入固定值,无需依赖User的行为:

func TestDB_CreateUser(t *testing.T) {
    // 初始化测试数据库连接
    testDB := setupTestDatabase(t)
    defer teardownTestDatabase(t, testDB)

    testEmail := "test@example.com"
    testDigest := "$2a$10$exampleBcryptDigest"
    testTime := time.Now()

    userID, err := testDB.CreateUser(testEmail, testDigest, testTime, testTime)
    if err != nil {
        t.Fatalf("CreateUser failed: %v", err)
    }

    // 验证数据库中是否存在该用户
    var storedEmail string
    err = testDB.QueryRow(`SELECT email FROM users WHERE id = $1`, userID).Scan(&storedEmail)
    if err != nil {
        t.Fatalf("Failed to fetch stored user: %v", err)
    }
    if storedEmail != testEmail {
        t.Errorf("Stored email mismatch: got %v, want %v", storedEmail, testEmail)
    }
}

3. 业务逻辑测试:Mock存储层

测试CreateNewUser时,只需要MockUserStore接口,验证模型操作是否被正确调用,以及存储层是否收到正确的参数:

type mockUserStore struct {
    createUserCalled bool
    receivedEmail    string
}

func (m *mockUserStore) GetUserByEmailAndPassword(email, passwordDigest string) (User, error) {
    return User{}, nil
}

func (m *mockUserStore) UpdatePassword(userID int, newPasswordDigest string) error {
    return nil
}

func (m *mockUserStore) UserExists(email string) (bool, error) {
    return false, nil
}

func (m *mockUserStore) CreateUser(email, passwordDigest string, createdAt, modifiedAt time.Time) (int, error) {
    m.createUserCalled = true
    m.receivedEmail = email
    return 1, nil
}

func TestCreateNewUser(t *testing.T) {
    testUser := &User{
        Email:                "test@example.com",
        Password:             "abc123",
        ConfirmationPassword: "abc123",
    }
    mockStore := &mockUserStore{}

    err := CreateNewUser(testUser, mockStore)
    if err != nil {
        t.Fatalf("CreateNewUser failed: %v", err)
    }

    if !mockStore.createUserCalled {
        t.Error("UserStore.CreateUser was not called")
    }
    if mockStore.receivedEmail != testUser.Email {
        t.Errorf("Received email mismatch: got %v, want %v", mockStore.receivedEmail, testUser.Email)
    }
    if testUser.ID != 1 {
        t.Errorf("User ID not set correctly: got %v, want 1", testUser.ID)
    }
}

方案优势

  1. 职责单一:模型只处理自身业务规则,存储层只处理数据库交互,业务层负责流程协调,完全符合单一职责原则。
  2. 测试解耦:各层测试互不依赖,模型测试无需数据库,存储层测试无需关心模型逻辑,彻底解决重复测试和耦合问题。
  3. 无代码异味:不再需要宽泛的UserAction接口,存储层接口聚焦于数据库操作,避免了不必要的Getter/Setter。
  4. 无循环导入:模型与存储层互相独立,Mock时不会出现循环依赖问题。

内容的提问来源于stack exchange,提问作者AlexManning

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 08:09:31