当涉及到模型设计时,我遇到了一些问题,特别是处理特定于模型的操作与数据库操作。我的用户模型就是一个很好的例子。
在我的数据库中创建用户时,我希望:
在测试时,我显然希望对所有4个都进行一组单元测试,但是#4调用了其他3个,这是我不想重新测试的,否则如果这3个调用中的任何一个都会有#4测试失败的风险。
我已经想出了为ModelActions和StoreActions创建一个单独的接口,并在需要时将UserAction接口发送到存储操作,然而,当我写出它时,我已经感觉到了一些严重的代码味道。
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"`
}
//UserStore is the interface for all User functions that interact with the database
type UserStore interface {
GetUserByEmailAndPassword(email, password string) (User, error)
UpdatePassword(u UserAction, previousPassword, password, confirmationPassword string) error
UserExists(email string) (bool, error)
CreateUser(u UserAction) error
}
// I am going against design Principles by having GetID, GetEmail, since JSON unmarshalling needs the struct fields to be capitalized, which is already a warning sign for me
type UserAction interface {
GetID() int
GetEmail() string
Timestamps() (time.Time, time.Time)
SetID(id int)
SetTimestamps()
SetPassword(password, confirmation string)
SetDigest(digest string)
CreateDigest() (string, error)
VerifyPassword() error
ComparePassword(password string) error
}
// Example of UserActions
func (u *User) CreateDigest() (string, error) {
var digest string
if err := u.VerifyPassword(); err != nil {
return digest, err
}
passwordByte, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
return digest, err
}
digest = string(passwordByte)
return digest, nil
}
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
}
// Example of DB Action
func (db *DB) CreateUser(ua UserAction) error {
if exists, err := db.UserExists(ua.GetEmail()); err != nil {
return err
} else if exists {
return &modelError{"Email", "already exists in the system"}
}
// set password
digest, err := ua.CreateDigest()
if err != nil {
return err
}
ua.SetDigest(digest)
ua.SetTimestamps()
createdAt, modifiedAt := ua.Timestamps()
rows, err := db.Query(`
INSERT INTO users (email, password_digest, created_at, modified_at)
VALUES ($1, $2, $3, $4)
RETURNING id
`, ua.GetEmail(), digest, createdAt, modifiedAt)
if err != nil {
return err
}
defer rows.Close()
var id int
for rows.Next() {
if err := rows.Scan(&id); err != nil {
return err
}
}
ua.SetID(id)
return nil
}有没有一种更好的方法来对这些独立的操作建模,以便在测试DB/Store函数时可以模拟UserActions?我尝试将User结构存储为接口的一部分,例如:
type UserAction {
SetTimestamps()
CreateDigest() (string, error)
VerifyPassword() error
ComparePassword(password string) error
User() *User
}然而,这会在创建模拟时导致循环导入,还会打开所有字段,因为模型的字段是可导出的,因此可以说这些字段已经可用
发布于 2018-05-31 08:55:21
我认为你的用户应该是一个具体的类型,你应该使用一个界面来模拟你的商店。
例如,这样的项目结构对我来说很有意义:
cmd/
server/
user.go
user_test.go
main.go
store.go
mysql/
mysql.go
user.go
user_test.go
user.go
user_test.go您的用户模型位于根目录user.go中。该文件将包含您的User结构,以及将在其上操作的函数,如CreateDigest。这些函数应该在user_test.go中进行测试。
值得一提的是,在你的根目录下,你的包不应该是main,你的包的名称应该是你的项目的名称,我们将它称为myapp。
您的mysql、postgres等也应该是一个具体的实现。您可能在该包中有一个函数,如:
func (m *MySQL) InsertUser(u *myapp.User) error此函数应在mysql/user_test.go中测试。
最后,我们可以将它们放在server中。这是您实际部署或运行的二进制文件。
在cmd/server/store.go中,您应该创建一个将由mysql实现的接口。
在cmd/server/user_test.go中,可以很容易地模拟这一点,这样您就不必访问真正的数据库。我相信你的界面应该存在于你的客户端中。在本例中,server是mysql的客户端。
在cmd/server/user.go中,您可能具有如下所示的函数:
func CreateUser(w http.ResponseWriter, r *http.Request) {
var u myapp.user
err := json.NewDecoder(r.Body).Decode(&u)
if err != nil {
panic(err) // don't do this for real
}
d := myapp.CreateDigest(u.Password)
u.Digest = d
// s is the interface, defined in `cmd/server/store.go`, but is implemented by mysql
err = s.InsertUser(&u)
if err != nil {
panic(err)
}
// Since we pass a pointer, you can have your store set the ID of the user
fmt.Println(u.ID)
}现在您有了更好的关注点分离,一切都应该很容易测试,并且对现有代码进行更改也很容易。
https://stackoverflow.com/questions/50588831
复制相似问题