Intro Golang implement Unit test ฉบับสมบูรณ์
System requirements
- go 1.15.14 up
- mockery
- richgo
How to unit test ?
unit test คือ implememt code เพื่อใช้ test mechanism ของส่วนต่างๆของ Service ไม่ว่าจะเป็นส่วนของ handler service repository หรือ ว่าส่วนที่เชื่อมต่อ external interfaces ต่างก็สามารถเขียน unit test ได้
ข้อดีของการเขียน Unit test
- ทำให้ลดการผิดพลาดของ mechanism ของ service เราได้
- ทำให้คนที่มาเขียนต่อเข้าใจ mechanism โดยสามารถอ่านจาก unit test ได้
- เพิ่มความมั่นใจมากขึ้นเมื่อต้องการ implement หรือ fix ในส่วนนั้นๆว่าจะไม่กระทบจุดอื่นๆ
แนวคิดของการเริ่มเขียนTest และ การออกแบบ
เราควรเริ่มเขียน Test ตั้งแต่ตรงไหนบาง
- Case success คือเขียนค่าที่ถูกต้องเพื่อป้อนไปให้ Function ที่เรา implement ไว้ให้Output ออกมาแบบที่เราคิดไว้ จุดที่เราควรเขียนคือ
1. จุดที่เราเขียน condition ต่างๆเอาไว้
2. จุดที่เราเขียน การ maping data ต่างๆ
3. จุดที่เราทำการ Validate - Case Fail คือเขียนค่าที่ไม่ถูกต้องเพื่อป้อนไปให้ Function ที่เรา implement ไว้ให้Output ออกมาแบบที่เราเขียน ดัก Case error ไว้ จุดที่เราควรเขียนคือ
1. จุดที่เราเขียน condition ต่างๆเอาไว้
2. จุดที่เราเขียน การ maping data ต่างๆ
3. จุดที่เราทำการ Validate
4. จุดที่ มีโอกาศ ได้ค่าว่าง เช่น การGet ข้อมูลจากที่ต่างๆ ไม่ว่าจะเป็น Database หรือ 3rd Party
5. จุดที่มีโอกาส เกิด panic
Mockery ทำอะไรได้บาง
mockery ใช้สำหรับ autogenerator mock โดย mockery จะทำการ generate จาก interface ที่เราเขียนไว้ โดยมีตัวอย่างดังนี้
package company
import (
"context"
"github.com/touchtechnologies-product/go-blueprint-clean-architecture/domain"
"github.com/touchtechnologies-product/go-blueprint-clean-architecture/service/company/out"
"github.com/touchtechnologies-product/go-blueprint-clean-architecture/service/company/companyin"
)//โดยตัว Mockery จะดูจากส่วนนี้
//go:generate mockery --name=Service
type Service interface {
List(ctx context.Context, opt *domain.PageOption) (total int, items []*out.CompanyView, err error)
Create(ctx context.Context, input *companyin.CreateInput) (ID string, err error)
Read(ctx context.Context, input *companyin.ReadInput) (company *out.CompanyView, err error)
Update(ctx context.Context, input *companyin.UpdateInput) (err error)
Delete(ctx context.Context, input *companyin.DeleteInput) (err error)
}
Command ที่ใช้ Generate
go generate ./...
รู้จักกับAssertต่างๆ ของ Unit test ที่ใช้งานบ่อยๆ
NoError
คือ Assert ที่ใช้สำหรับตรวจสอบ Function ของเราซึ่งปกติ Function ที่เราเขียนจะมีการ return errors ออกมาด้วยเมื่อเราเขียนใช้งาน Function แล้วถ้าเริ่มใส่ Assert Noerror ถ้าหาก error ที่ return ออกมาเป็น nil ก็จะถือว่าผ่าน
func (suite *PackageTestSuite) TestCreate() {
givenInput := companyin.MakeTestCreateInput()
givenCompany := suite.makeDataTestCompany
suite.validator.On("Validate", givenInput).Once().Return(nil)
suite.repo.On("Create", mock.Anything, givenCompany).Once().Return(givenCompany.ID, nil)
actualID, err := suite.service.Create(suite.ctx, givenInput)
suite.NoError(err)
}
Error
คือ Assert ที่ใช้สำหรับตรวจสอบ Function ของเราซึ่งปกติ Function ที่เราเขียนจะมีการ return errors ออกมาด้วยเมื่อเราเขียนใช้งาน Function แล้วถ้าเริ่มใส่ Assert error ถ้าหาก error ที่ return ออกมาเป็นค่า Error ตามที่เราเขียน เงื่อนไขไว้ก็จะถือว่าผ่าน
func (suite *PackageTestSuite) TestCreateFail() {
expectedErr := errors.New("Test")
suite.Error(expectedErr)
}
NotEqual
คือ Assert ที่ใช้สำหรับตรวจสอบ param ต่างๆหรือส่วนที่ ผ่าน mechanism ต่างๆ เพื่อตรวจสอบว่าตรงกับ condition ที่เราคิดไว้หรือไม่ Assert ตัวนี้คือ ไม่เท่ากับ
func (suite *PackageTestSuite) TestCreate() {
input := companyin.MakeTestCreateInput()
req, resp, err := suite.makeCreateReq(input)
suite.NoError(err)
newID := "test"
suite.NotEqual(newID, "test1")
}
Equal
คือ Assert ที่ใช้สำหรับตรวจสอบ param ต่างๆหรือส่วนที่ ผ่าน mechanism ต่างๆ เพื่อตรวจสอบว่าตรงกับ condition ที่เราคิดไว้หรือไม่ Assert ตัวนี้คือ เท่ากับ
func (suite *PackageTestSuite) TestCreate() {
givenInput := companyin.MakeTestCreateInput()
givenCompany := suite.makeDataTestCompany
suite.validator.On("Validate", givenInput).Once().Return(nil)
suite.repo.On("Create", mock.Anything, givenCompany).Once().Return(givenCompany.ID, nil)
actualID, err := suite.service.Create(suite.ctx, givenInput)
suite.Equal(givenCompany.ID, actualID)
}
NotEmpty
คือ Assert ที่ใช้สำหรับตรวจสอบ param ต่างๆหรือส่วนที่ ผ่าน mechanism ต่างๆ เพื่อตรวจสอบว่าตรงกับ condition ที่เราคิดไว้หรือไม่ Assert ตัวนี้คือ ไม่เท่ากับค่าว่าง
func (suite *PackageTestSuite) TestCreate() {
input := companyin.MakeTestCreateInput()
req, resp, err := suite.makeCreateReq(input)
suite.NoError(err)
newID := "test"
suite.NotEmpty(newID)
}
AssertExpectations
คือ Assert ที่ใช้สำหรับตรวจสอบค่าว่าใน Mock มีการส่งค่ามาถูกต้องหรือไม่
func (suite *PackageTestSuite) TestCreate() {
input := companyin.MakeTestCreateInput()
req, resp, err := suite.makeCreateReq(input)
suite.NoError(err)
newID := "test"
suite.service.On("Create", mock.Anything, input).Return(newID, nil)
suite.router.ServeHTTP(resp, req)
suite.service.AssertExpectations(suite.T())
suite.Equal(http.StatusCreated, resp.Code)
suite.Equal(newID, resp.Header().Get("Content-Location"))
}