模型专属操作与数据库操作的分离设计及测试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) } }
方案优势
- 职责单一:模型只处理自身业务规则,存储层只处理数据库交互,业务层负责流程协调,完全符合单一职责原则。
- 测试解耦:各层测试互不依赖,模型测试无需数据库,存储层测试无需关心模型逻辑,彻底解决重复测试和耦合问题。
- 无代码异味:不再需要宽泛的
UserAction接口,存储层接口聚焦于数据库操作,避免了不必要的Getter/Setter。 - 无循环导入:模型与存储层互相独立,Mock时不会出现循环依赖问题。
内容的提问来源于stack exchange,提问作者AlexManning
相关产品推荐
相关产品推荐

