バックエンド ユーザーデータのバインディング

テストコード

func TestUserCreate(t *testing.T) {
gin.SetMode(gin.TestMode)
defaultWriter := gin.DefaultWriter
gin.DefaultWriter = io.Discard

t.Run("accept empty body", func(t *testing.T) {
router := gin.New()
ctrl := controller.UserController{}
router.POST("/users", ctrl.Create)

w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/users", nil)
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, "", w.Body.String())
})

gin.DefaultWriter = defaultWriter
}

プロダクトコード

type UserController struct {
}

type UserInfo struct {
UserId string `json:"userId" binding:"required"`
Password string `json:"password" binding:"required"`
}

func (ctrl *UserController) Create(ctx *gin.Context) {
var userInfo UserInfo
err := ctx.BindJSON(&userInfo)
if err != nil {
ctx.AbortWithStatus(http.StatusBadGateway)
return
}
}

セッションの削除

テストコード

func TestDeleteSessionSuccessCase(t *testing.T) {
writer := gin.DefaultErrorWriter
gin.DefaultWriter = io.Discard
gin.SetMode(gin.TestMode)

// Create a new mock session
sessionCtrl := gomock.NewController(t)
defer sessionCtrl.Finish()

s := controller.NewMockSession(sessionCtrl)

s.
EXPECT().
Clear().
AnyTimes()
s.
EXPECT().
Save().
Return(nil).
AnyTimes()

sessionConstructor := controller.NewSession
controller.NewSession = func(c *gin.Context) sessions.Session {
return s
}

// test controller

router := gin.New()
store := cookie.NewStore([]byte("secret"))
router.Use(sessions.Sessions("mysession", store))
router.DELETE("/session", controller.DeleteSession)

w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodDelete, "/session", nil)
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, ``, w.Body.String())

controller.NewSession = sessionConstructor
gin.DefaultWriter = writer
}

プロダクトコード

func DeleteSession(c *gin.Context) {
// セッションを取得してクリアするだけ。
s := newSession(c)
s.Clear()
err := s.Save()
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": `unknown error happen`,
})
return
}

c.Status(http.StatusOK)
}

kemotalk バックエンド開発(11/03) セッションの作成処理のテスト

テストコード

func TestCreateSessionSuccessCase(t *testing.T) {
writer := gin.DefaultErrorWriter
gin.DefaultWriter = io.Discard
gin.SetMode(gin.TestMode)

// Create a new mock session
sessionCtrl := gomock.NewController(t)
defer sessionCtrl.Finish()

s := controller.NewMockSession(sessionCtrl)

s.
EXPECT().
Get(gomock.Eq("userId")).
Return(nil)
s.
EXPECT().
Set(gomock.Eq("userId"), gomock.Eq(uint(1)))
s.
EXPECT().
Save().
Return(nil)

sessionConstructor := controller.NewSession
controller.NewSession = func(c *gin.Context) sessions.Session {
return s
}

// Create a new mock db
dbCtrl := gomock.NewController(t)
defer dbCtrl.Finish()

db := repository.NewMockUserRespository(dbCtrl)

u := model.User{
UserId: "user123",
HashedPassword: "$2a$10$AU2kV8mMRjsJ4HAcpHLRm.R0BGaaHflxke52Qm9VLAbjQCbA/yZjC",
}
u.ID = 1
db.
EXPECT().
Get(gomock.Eq("user123")).
Return(
&u,
nil,
).
AnyTimes()

controller.UserRepository = db

// test controller

router := gin.New()
store := cookie.NewStore([]byte("secret"))
router.Use(sessions.Sessions("mysession", store))
router.POST("/session", controller.CreateSession)

credential := strings.NewReader(`{"userId": "user123", "password": "pass123"}`)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/session", credential)
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, ``, w.Body.String())

controller.NewSession = sessionConstructor
gin.DefaultWriter = writer
}

プロダクトコード

// セッションを作成
s.Set("userId", user.ID)
if err = s.Save(); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": `unknown error happen`,
})
return
}

kemotalk バックエンド開発(22/11/02) 認証失敗時の処理

// 認証
if user.Authenticate(credential.Password) != nil {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"message": `fail authentication`,
})
return
}

 

func TestCreateSessionFailAuthentication(t *testing.T) {
writer := gin.DefaultErrorWriter
gin.DefaultWriter = io.Discard
gin.SetMode(gin.TestMode)

// Create a new mock db
ctrl := gomock.NewController(t)
defer ctrl.Finish()

m := repository.NewMockUserRespository(ctrl)

m.
EXPECT().
Get(gomock.Eq("Hoge")).
Return(
&model.User{
UserId: "Hoge",
HashedPassword: "dummy",
},
nil,
).
AnyTimes()

controller.UserRepository = m

router := gin.New()
store := cookie.NewStore([]byte("secret"))
router.Use(sessions.Sessions("mysession", store))
router.POST("/session", controller.CreateSession)

credential := strings.NewReader(`{"userId": "Hoge", "password": "Bar"}`)

w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/session", credential)
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, `{"message":"fail authentication"}`, w.Body.String())

gin.DefaultWriter = writer
}

kemotalk バックエンド開発記録(11/1) データ取得失敗時のエラー処理

今回したこと

モックを作成

mockgen -source=user_repository.go -destination=user_repository_mock.go -package=repository

コード

インターフェース

type UserRespository interface {
Get(userId string) (*model.User, error)
}

コントローラに以下を追加

// ユーザを取得
_, err = UserRepository.Get(credential.UserId)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"message": `database error occurred`,
})
return
}

テストを作成

func TestCreateSessionFailToGetUser(t *testing.T) {
writer := gin.DefaultErrorWriter
gin.DefaultWriter = io.Discard
gin.SetMode(gin.TestMode)

// Create a new mock db
ctrl := gomock.NewController(t)
defer ctrl.Finish()

m := repository.NewMockUserRespository(ctrl)

m.
EXPECT().
Get(gomock.Eq("Hoge")).
Return(nil, errors.New("not found(this is dummy error)")).
AnyTimes()

controller.UserRepository = m

router := gin.New()
store := cookie.NewStore([]byte("secret"))
router.Use(sessions.Sessions("mysession", store))
router.POST("/session", controller.CreateSession)

credential := strings.NewReader(`{"userId": "Hoge", "password": "Bar"}`)

w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/session", credential)
router.ServeHTTP(w, req)

assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, `{"message":"database error occurred"}`, w.Body.String())

gin.DefaultWriter = writer
}

テスト結果

ok      bitbucket.org/furtoon_studio/backend/controller 0.346s

メモ

・tddでコントローラを作るときには失敗から先に作ってくとレッド・グリーン・リファクタリングの手順でコードを書きやすい気がする。

・関数ベースで処理を作成するとモックの入れ替えがしにくい?

関数用のグローバル変数を作ればいいがアンチパターンなのでは?

ついでに今repositoryに関してもグローバル変数に入れて処理しているのでアンチパターンのことをしている。改善するにはコントローラを構造体にしてフィールド変数に追加すれば良さそう。

kemotalk

今回したこと

ユーザーが見つかった時のテストを追加

コード

プロダクションコード

func GetUser(userId string) (*model.User, error) {
var u model.User
result := GormDB.Where("user_id = ?", userId).Find(&u)
if result.Error != nil {
return nil, result.Error
}
return &u, result.Error
}

テストコード

t.Run("user found", func(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
defer db.Close()

database.GormDB, err = gorm.Open(postgres.New(postgres.Config{
Conn: db,
}), &gorm.Config{})
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a database connection", err)
}

mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "users" WHERE user_id = $1 AND "users"."deleted_at" IS NULL`)).
WithArgs("user123").
WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "hashed_password"}).AddRow(1, "user123", "ecd71870d1963316a97e3ac3408c9835ad8cf0f3c1bc703527c30265534f75ae"))

// now we execute our method
u, err := database.GetUser("user123")

assert.NotNil(t, u)
assert.Nil(t, err)

// we make sure that all expectations were met
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
})

テスト結果

ok      bitbucket.org/furtoon_studio/backend/database   0.347s

メモ

関係ないけど「サイトを離れますか」という仕組みいいね。

どうやるんだろう。

kemotalk バックエンド開発記録(6回目) ログインの実装

作ろうとしているアプリ

ビデオチャット

現在取り組んでいる機能

ログイン

今回したこと

sql-mockを使ってデータベースのテストの作成

コード

プロダクションコード

func GetUser(userId string) (*model.User, error) {
var u model.User
result := GormDB.Where("user_id = ?", userId).Find(&u)
return nil, result.Error
}

 

テストコード

func TestGetUser(t *testing.T) {
t.Run("user not found", func(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
defer db.Close()

database.GormDB, err = gorm.Open(postgres.New(postgres.Config{
Conn: db,
}), &gorm.Config{})
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a database connection", err)
}

mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "users" WHERE user_id = $1 AND "users"."deleted_at" IS NULL`)).
WithArgs("user123").
WillReturnError(gorm.ErrRecordNotFound)

// now we execute our method
u, err := database.GetUser("user123")

assert.Nil(t, u)
assert.NotNil(t, err)

// we make sure that all expectations were met
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
})
}

テスト結果

ok      bitbucket.org/furtoon_studio/backend/database   0.426s

メモ

特になし