본문 바로가기
프로그래밍/GO

[golang] Unit Test 및 Interfaces 기반 테스트

by 남생 namsaeng 2022. 6. 2.
반응형

이전 포스팅에서 go test를 이용하여 unit test를 모두 독립적으로 할 수 있다는 것을 알았다. 하지만 코드에 db 및 api 요청이 있을 시에는 쉽게 unit test를 할 수 없다.


https://namsaenga.tistory.com/59

godoc 설치 및 사용 절차 / godoc 이용한 테스트 및 Coverage 확인

리팩토링이 필요한 코드 리뷰 및 테스트 가능한 코드 작성을 위하여 godoc을 설치한다. 1. Visual Studio Code 터미널에서 아래와 같은 환경변수를 설정한다. [namsaenga@localhost ~]$ export GOPATH="/home/nams..

namsaenga.tistory.com


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")
		}
	})
}
반응형

댓글