I am having some issues when it comes to model design, specifically handling Model specific actions vs Database actions. A good example would be my User model.
When creating a user in my DB, I want to:
When testing, I obviously want to have a set of unit test for all 4, however #4 has calls to the other 3, something I don’t want to retest, or risk having #4 test fail if any of those 3 do.
I have come up with creating a separate Interface for ModelActions vs StoreActions, and sending the UserAction interface to a store action when needed, however as I write it out, I am already sensing some serious code smell.
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
}
Is there a better way to model these separate actions so the UserActions can be mocked when testing the DB/Store functions? I tried storing the User
struct as part of the interface, something such as:
type UserAction {
SetTimestamps()
CreateDigest() (string, error)
VerifyPassword() error
ComparePassword(password string) error
User() *User
}
This however causes cyclical imports when creating mocks, and also opens up all fields, which are already arguably available since the model's fields are exportable
I think your user should be a concrete type, and you should use an interface to mock your store.
For instance, a project structure like this makes sense to me:
cmd/
server/
user.go
user_test.go
main.go
store.go
mysql/
mysql.go
user.go
user_test.go
user.go
user_test.go
Your user model is at the root, in user.go
. This file will contain your User
struct, and the functions that will operate on it, like CreateDigest
. These functions should be tested in user_test.go
.
It is worth mentioning that at your root, your package should not be main
, your package name should be the name of your project, we will call it myapp
.
Your mysql
, postgres
, etc. should also be a concrete implementation. You may have a function in that package like:
func (m *MySQL) InsertUser(u *myapp.User) error
This function should be tested in mysql/user_test.go
.
And finally, we can put it all together in server
. This is the binary you actually deploy or run.
In cmd/server/store.go
, you should create an interface that will be implemented by mysql
.
In cmd/server/user_test.go
it is very easy to mock this so that you do not have to hit the real database. I am a believer in that your interfaces should live in your client. In this case, server
is a client of mysql
.
In cmd/server/user.go
you may have functions that look like this:
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)
}
Now that you have a better separation of concerns, everything should be easy to test, and making changes to existing code is easy.