Test를 위한 함수 추상화
log.Panic이라는 함수는 log 모듈에 의존적인 함수이다. 이를 직접적으로 테스트하기는 힘들다. 그렇기에 우리는 추상화라는 선택지를 고려할 수 있다.
기존 코드
func HandleErr(err error) {
if err != nil {
log.Panic(err)
}
}
변경 코드
var logFn = log.Panic
func HandleErr(err error) {
if err != nil {
logFn(err)
}
}
우리가 궁금한 것은 err!=nil 일 때 logFn이 작동하는가 이다. 그렇기에 log.Panic 함수를 logFn 이라는 변수에 담아 추상화 과정을 거쳐 이 logFn이 잘 작동하는가를 검사한다.
TestCode
func TestHandleErr(t *testing.T) {
oldLogFn := logFn
defer func() {
logFn = oldLogFn
}()
called := false
logFn = func(v ...interface{}) {
called = true
}
err := errors.New("test")
HandleErr(err)
if !called {
t.Error("HandleError should call fn")
}
}
테스트 코드는 기본적으로 테스트하고자하는 코드와 같은 package에서 이뤄진다. 그렇기에 테스트 코드에서 logFn을 변경한다면 실제 프로젝트에서 사용될 logFn이라는 함수가 Test를 위한 logFn으로 변경 될 가능성이 있기에 oldLogFn을 통해 기존 함수를 저장 후 코드가 종료되면 원상태로 되돌려 놓는다.
결과
하지만 이렇게 억지로 테스트코드를 우겨넣는것 역시 좋지 않다는 글을 어디서 본 듯 하다.
TODO : 찾으면 그에 대한 정보도 추가
+++)
데이비드 하이네마이어 핸슨은 테스트로 인한 손상이라고 표현했다.
또한 이렇게 런타임 환경에서 테스트 데이터를 입력하고 원래로 돌리는 것을 몽키패치라고 한다는 것을 알았다.
그렇다면 또 어댑터 패턴은 무엇인가.
Interface를 사용하여 테스트하기 유리한 코드 작성
기존 코드
func hasWalletFile(name string) bool {
_, err := os.Stat(name)
return !os.IsNotExist(err)
}
간단히 코드 설명을 덧붙이면 os.Stat은 파일의 정보를 읽어오는 함수이다. name이라는 이름을 가진 file이 있으면 그 파일의 이름을 불러온다.
여기서 생각을 해야 한다. 내가 이 os 모듈이 가진 Stat()이라는 함수를 Test할 필요가 있을까? 굳이?
사실 굳이 해야 하나? 라는 생각보다는 하면 안된다는 생각이 맞다. 왜냐하면 내가 가진 hasWalletFile()이라는 함수에 대한 단위 테스트를 진행하는데 os가 가진 Stat()이라는 함수가 끼어드는 것이니까. 만약 os.Stat()함수가 수정이 되어 기존과 달라진다면 os.Stat()에 의존하고 있는 hasWalletFile() 함수 또한 문제가 생길 것이다. 이렇게 의존성을 가진 함수는 단위 테스트를 진행하기가 어렵다.
위와 같은 경우 Interface를 사용함으로써 문제를 해결할 수 있다. 어댑터 패턴을 사용하는 것인데 코드 테스트에 유리할 뿐 아니라 함수 자체를 추상화 하여 더 좋은 코드를 작성할 수 있다.
수정 코드
type ioLayer interface {
hasWalletFile(name string) bool
writeFile(name string, data []byte, perm fs.FileMode) error
readFile(name string) ([]byte, error)
}
type filLayer struct{}
var files ioLayer = filLayer{}
func (filLayer) readFile(name string) ([]byte, error) {
return os.ReadFile(name)
}
func (filLayer) writeFile(name string, data []byte, perm fs.FileMode) error {
return os.WriteFile(name, data, perm)
}
func (filLayer) hasWalletFile(name string) bool {
_, err := os.Stat(name)
return !os.IsNotExist(err)
}
어댑터 패턴을 사용할 수 있도록 Interface와 그 구조를 변경한 모습이다.
이제 나는 ioLayer interface를 통해 다양한 io를 직접적인 프로젝트 수정을 거치지 않고 단순히 interface를 작성하는 것 만으로 원하는 형태의 다양한 io를 적용할 수 있다.(usbLayer, blutoothLayer 등)
테스트 하고자 하는 코드
func Wallet() *wallet {
if w == nil {
w = &wallet{}
if files.hasWalletFile(fileName) {
w.privateKey = restoreKey()
} else {
key := createPrivateKey()
persistKey(key)
w.privateKey = key
}
w.Address = aFromK(w.privateKey)
}
return w
}
테스트 코드
type fakeLayer struct {
fakeHasWalletFile func() bool
}
func (fakeLayer) readFile(name string) ([]byte, error) {
return x509.MarshalECPrivateKey(makeTestWallet().privateKey)
}
func (fakeLayer) writeFile(name string, data []byte, perm fs.FileMode) error {
return nil
}
func (f fakeLayer) hasWalletFile(name string) bool {
return f.fakeHasWalletFile()
}
func TestWallet(t *testing.T) {
t.Run("새로운 지갑이 생성될 때", func(t *testing.T) {
files = fakeLayer{
fakeHasWalletFile: func() bool { return false },
}
tw := Wallet()
if reflect.TypeOf(tw) != reflect.TypeOf(&wallet{}) {
t.Error("New wallet should return a new wallet instance")
}
})
t.Run("존재하는 지갑을 가져올 때", func(t *testing.T) {
files = fakeLayer{
fakeHasWalletFile: func() bool {
return true
},
}
w = nil
tw := Wallet()
if reflect.TypeOf(tw) != reflect.TypeOf(&wallet{}) {
t.Error("New wallet should return a new wallet instance")
}
})
}
여기서 보면 알 수 있듯 ioLayer interface를 구현한 fakeLayer는 ioLayer가 가진 함수를 했다 치고 로 퉁친다. 이제 기능을 하는 함수 뿐 아니라 Test코드 역시 수정할 필요가 없어진 모습이다.
'개인 프로젝트 > coin' 카테고리의 다른 글
[Project coin] 단위 테스트(3) - sync.Once가 포함된 코드 (0) | 2022.08.19 |
---|---|
[Project Coin] 모니터링 시스템 whatap (0) | 2022.08.15 |
[Project Coin] 단위 테스트(1) _ Unit Test란? & Table Test (0) | 2022.08.10 |