반응형
이전 포스팅에서 go test를 이용하여 unit test를 모두 독립적으로 할 수 있다는 것을 알았다. 하지만 코드에 db 및 api 요청이 있을 시에는 쉽게 unit test를 할 수 없다.
https://namsaenga.tistory.com/59
1. Unit Test
파일 생성 및 삭제, db 접속을 함께 테스트하는 것은 unit test가 아니다. unit test를 하려면 독립적인 코드로 만들어야 한다.
예를 들어, 고유의 주소와 private key를 가지고 있는 wallet 객체에 서명을 하는 함수인 Sign(payload string, w *wallet) string 함수가 있다고 하자. 이 Sign 함수를 테스트하기 위해 Wallet() 함수를 호출하면 파일 시스템을 사용할 것이다.
<wallet.go>
- persistKey 함수는 os.WriteFile(fileName, bytes, 0644) 함수로 파일 시스템에 접근한다.
- restoreKey 함수는 os.ReadFile(fileName) 함수로 파일 시스템에 접근한다.
- 결국, Wallet 함수는 파일 시스템을 사용한다.
func persistKey(key *ecdsa.PrivateKey) {
bytes, err := x509.MarshalECPrivateKey(key)
utils.HandleErr(err)
err = os.WriteFile(fileName, bytes, 0644)
utils.HandleErr(err)
}
func restoreKey() (key *ecdsa.PrivateKey) {
keyAsBytes, err := os.ReadFile(fileName)
utils.HandleErr(err)
key, err = x509.ParseECPrivateKey(keyAsBytes)
utils.HandleErr(err)
}
func Wallet() *wallet {
if w == nil {
w = &wallet{}
if hasWalletFile() {
w.privateKey = restoreKey()
} else {
key := createPrivKey()
persistKey(key)
w.privateKey = key
}
w.Address = AFromK(w.privateKey)
}
return w
}
wallet_test.go에서 파일 시스템을 사용하지 않고 테스트할 수 있도록 코드를 작성한다.
<wallet_test.go>
- Wallet 함수 대신 사용할 makeTestWallet 함수를 만든다.
- 파라미터로 전달할 인수 값들을 임의로 정의한다.
const (
testKey string = "3077020101042019c4ed17f7ccbdb6f317623e4741b6cb6dddb6caff39eba47e9565e31faa1a87a00a06082a8648ce3d030107a1440342000421e3fc0ba79e68e63559940a52f34425c449cde202f065e7b80bb7fab5fd4a11f85be4b261a1ee2916d2082083d8bbc55b14c4948269a34e0162964749d3a5a9"
testPayload string = "00fad70befde16d2a9630e7d00cc0c463c42700769f14440ee12920b02fc8630"
testSig string = "47ae5f68c41eeeea6e66d941040504a3ed3c5c356a8c0003e08972d7e20fcf06f4a747f0d9d705f3b96cf866833ea26fb666f190193fb29aee8160bfe65068b5"
)
func makeTestWallet() *wallet {
w := &wallet{}
b, _ := hex.DecodeString(testKey)
key, _ := x509.ParseECPrivateKey(b)
w.privateKey = key
w.Address = aFromK(key)
return w
}
func TestSign(t *testing.T) {
s := Sign(testPayload, makeTestWallet())
_, err := hex.DecodeString(s)
if err != nil {
t.Errorf("Sign() should return a hex encoded string, got %s", s)
}
}
2. Interfaces 기반 테스트(코드 리팩터링 및 사이드 이펙트 제거)
기존의 소스코드에서 interfaces 패턴을 기반으로 테스트하기 쉽고 다른 함수에 덜 의존적인 코드로 만들 수 있다.
<wallet.go 수정>
- persistKey 함수 안에 os.WriteFile 함수를 files.writeFile 함수로 수정한다.
- restoreKey 함수 안에 os.ReadFile 함수를 files.redfile 함수로 수정한다.
- Wallet() 함수 안에 hasWalletFile 함수를 files.hasWalletFile 함수로 수정한다.
- 파일 시스템과 대화 하고 있던 함수를 분리된 struct로 격리시켜서 여러 함수를 원하는 대로 컨트롤할 수 있다.
type fileLayer interface { // 인터페이스 선언
hasWalletFile() bool
writeFile(filename string, data []byte, perm fs.FileMode) error
readFile(filename string) ([]byte, error)
}
type layer struct{}
func (layer) writeFile(filename string, data []byte, perm fs.FileMode) error { // 인터페이스 정의
return ioutil.WriteFile(fileName, data, perm)
}
func (layer) readFile(filename string) ([]byte, error) { // 인터페이스 정의
return ioutil.ReadFile(fileName)
}
var files fileLayer = layer{} // 함수들을 struct로 격리
func (layer) hasWalletFile() bool { // 인터페이스 정의
_, err := os.Stat(fileName)
return !os.IsNotExist(err)
}
type wallet struct {
privateKey *ecdsa.PrivateKey
Address string
}
func createPrivKey() *ecdsa.PrivateKey {
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
utils.HandleErr(err)
return privKey
}
func persistKey(key *ecdsa.PrivateKey) {
bytes, err := x509.MarshalECPrivateKey(key) // the byte isn't need to be a string based HEX
utils.HandleErr(err)
err = files.writeFile(fileName, bytes, 0644)
utils.HandleErr(err)
}
func restoreKey() (key *ecdsa.PrivateKey) {
keyAsBytes, err := files.readFile(fileName)
utils.HandleErr(err)
key, err = x509.ParseECPrivateKey(keyAsBytes)
utils.HandleErr(err)
return
}
func Wallet() *wallet {
if w == nil {
w = &wallet{}
if files.hasWalletFile() {
w.privateKey = restoreKey()
} else {
key := createPrivKey()
persistKey(key)
w.privateKey = key
}
w.Address = aFromK(w.privateKey)
}
return w
}
3. Wallet() 함수 테스팅
- Wallet에서 files.hasWalletFile 함수의 리턴 값이 true 그리고 false 일 때를 모두 테스트한다.
- wallet.go에서 readFile 함수는 파일 시스템에 접근하지만, wallet_test.go에서 readFile은 그냥 TestWallet으로 byte값을 리턴해주고 있을 뿐이다.
- 여기에서 fakeLayer가 layer와 다른 점은 fakerLayer는 struct가 비어있지 않다는 점이다.
- TestWallet 함수의 두 번째 t.Run에서 w = nil을 작성한 이유는 첫 번째 t.Run에서 Wallet 함수를 실행시키면, w = &wallet{}으로 w값이 이미 null이 아니기 때문이다. 이러한 부분을 고려하여 테스팅해야 한다.
type fakeLayer struct {
fakeHasWalletFile func() bool
}
func (f fakeLayer) hasWalletFile() bool {
return f.fakeHasWalletFile()
}
func (fakeLayer) writeFile(filename string, data []byte, perm fs.FileMode) error {
return nil
}
func (fakeLayer) readFile(filename string) ([]byte, error) {
//return utils.ToBytes(makeTestWallet().privateKey), nil
return x509.MarshalECPrivateKey(makeTestWallet().privateKey)
}
func TestWallet(t *testing.T) {
t.Run("New Wallet is created", func(t *testing.T) {
files = fakeLayer{ // 여기에서 기존에 files가 layer{}를 가지고 있었는데, fakeLayer{}로 overwrite 해주었다.
fakeHasWalletFile: func() bool {
t.Log("I have been called")
return false
},
}
tw := Wallet()
if reflect.TypeOf(tw) != reflect.TypeOf(&wallet{}) {
t.Error("New Wallet should return a new wallet instance")
}
})
t.Run("Wallet is restored", func(t *testing.T) {
files = fakeLayer{
fakeHasWalletFile: func() bool {
t.Log("I have been called")
return true
},
}
w = nil
tw := Wallet()
if reflect.TypeOf(tw) != reflect.TypeOf(&wallet{}) {
t.Error("New Wallet should return a new wallet instance")
}
})
}
반응형
'프로그래밍 > GO' 카테고리의 다른 글
[golang] 조건문 있는 함수를 조건문에서 호출하는 유닛 테스트 (0) | 2022.06.13 |
---|---|
[golang] 조건문 및 루프가 있는 함수 유닛 테스트, nested function (0) | 2022.06.09 |
[golang] Adapter Pattern 기반 Unit Test(2) (0) | 2022.06.08 |
[golang] Adapter Pattern 기반 Unit Test(1) (0) | 2022.06.08 |
godoc 설치 및 사용법 / go 테스트 및 커버리지 확인 (0) | 2022.05.30 |
댓글