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

[golang] Adapter Pattern 기반 Unit Test(2)

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

이전 포스팅에서 소스코드를 Adapter Pattern 기반으로 리팩터링 하여 Unit Test를 수월하게 만들어 주었다.

 

https://namsaenga.tistory.com/68

 

[Golang] Adapter Pattern 기반 Unit Test(1)

Adapter Pattern 기반의 Unit Test를 하기 위해서는 기본적인 Unit Test를 왜 해야 하는지 이해해야 한다. 이전 포스팅에 관련된 설명이 있다. https://namsaenga.tistory.com/62 [godoc] Unit Test 및 Interfaces..

namsaenga.tistory.com

 

 


 

 

1. 어댑터 패턴 기반 유닛 테스트(예제: 블록체인 클론코딩)

 

블록 테스트 과정에서 블록 생성이 가장 먼저 호출될 것이기 때문에, block.go의  createBlock()가 먼저 테스트될 것이다.  createBlock() 함수 안에서는 채굴이 이루어지며, 블록 생성에 필요한 트랜잭션을 위하여 가짜 mempool을 만들어 주어야 한다. 또한, mempool안에 사용하는 데이터베이스에 대해 가짜 데이터베이스 어댑터를 만들어준다.

 

 

 

<blockchain/blockchain.go>

func FindBlock(hash string) (*Block, error) {
	blockBytes := dbStorage.FindBlock(hash)
	if blockBytes == nil {
		return nil, ErrNotFound
	}
	block := &Block{}
	block.restore(blockBytes)
	return block, nil
}

func Blockchain() *blockchain {
	once.Do(func() {
		b = &blockchain{
			Height: 0,
		}
		checkpoint := dbStorage.LoadChain()
		if checkpoint == nil {
			b.AddBlock()
		} else {
			b.restore(checkpoint)
		}
	})
	return b
}

func (b *blockchain) AddBlock() *Block {

	block := createBlock(b.NewestHash, b.Height+1, getDifficulty(b))
	b.NewestHash = block.Hash
	b.Height = block.Height
	b.CurrentDifficulty = block.Difficulty
	persistBlockchain(b)
	return block
}

func createBlock(prevHash string, height, diff int) *Block {
	block := &Block{
		Hash:       "",
		PrevHash:   prevHash,
		Height:     height,
		Difficulty: diff,
		Nonce:      0,
	}
	// 채굴을 끝내고 해시를 찾고 전부 끝낸 다음에 트랜잭션들을 Block에 넣음
	block.mine()
	block.Transactions = Mempool().TxToConfirm()
	persistBlock(block)
	return block
}

func persistBlock(b *Block) {
	dbStorage.SaveBlock(b.Hash, utils.ToBytes(b))
}

func persistBlockchain(b *blockchain) {
	dbStorage.SaveChain(utils.ToBytes(b))
}

 

 

 

2. 테스트 파일 생성 및 구성

blockchain.go를 테스트하기 위해서는 blockchain_test.go 파일을 생성하고, 테스트할 함수명 앞에 Test를 붙여서 구현한다. 예를 들어, blockchain.go의 FindBlock 함수를 테스트하기 위하여 blockchain_test.go에서 func TestFindBlock(t *testing.T)로 시그니쳐를 맞춰야 한다. 이전 포스팅에서 Adapter Pattern 기반으로 리팩터링 한 코드 상, FindBlock도 함께 테스트하기 위해 CreateBlock과 함께 작성하였다.

 

 

<blockchain/blockchain_test.go>

  • 각 Test 함수는 독립적이며, 개별 Test 함수 내에 여러 개의 서브 테스트를 동시(Concurrently)에 진행하러면 t.Run 함수를 구현한다.
  • func TestCreateBlock(t *testing.T) : Mempool이 적어도 하나 이상 있어야 하기 때문에 createBlock 함수 실행 전에 미리 fake Mempool을 만들어 주고 createBlock 함수를 테스트한다.  createBlock 함수 내에서 실행되는 persistBlock 함수는 fakeDBAdapter의 SaveBlock을 사용해 아무 일이 일어나지 않기 때문에 유닛 테스트를 순조롭게 진행할 수 있다. 기존 DBAdapter의 SaveBlock이었다면 실제 데이터베이스에 접속했을 것이다.
  • func TestFindBlock(t *testing.T) : TestFindBlock 테스트 함수는 테스트 대상인 FindBlock 함수 내에 조건문이 참(dbStorage.FindBlock(hash) == nil) 일 때와 거짓(dbStorage.FindBlock(hash) != nil) 일 때 모두 체크해야 한다. 따라서 참과 거짓 두 경우 모두를 테스트할 수 있게 fakeFindBlock 함수에 대한 정의를 각각 해준다.

 

package blockchain

import (
	"reflect"
	"testing"
)

type fakeDBAdapter struct {
	fakeFindBlock func() []byte
    fakeLoadChain func() []byte
}

func (f fakeDBAdapter) FindBlock(hash string) []byte {
	return f.fakeFindBlock()
}

func (fakeDBAdapter) SaveBlock(hash string, data []byte) {

}
func (fakeDBAdapter) SaveChain(data []byte) {

}

func (f fakeDB) LoadChain() []byte {
	return f.fakeLoadChain()
}

func TestCreateBlock(t *testing.T) {
	dbStorage = fakeDBAdapter{} // blockchain.go에 있는 "var dbStorage storage = db.DBAdapter{}"를 fakeDBAdapter{}로 바꿈.
	Mempool().Txs["test"] = &Tx{}
	b := createBlock("x", 1, 1)
	if reflect.TypeOf(b) != reflect.TypeOf(&Block{}) {
		t.Error("createBlock() should return an instance of a block")
	}
}

func TestFindBlock(t *testing.T) {
	t.Run("Block not found", func(t *testing.T) {
		dbStorage = fakeDBAdapter{
			fakeFindBlock: func() []byte {
				return nil
			},
		}
		_, err := FindBlock("xx")
		if err == nil {
			t.Error("The block should not be found.")
		}
	})
	t.Run("Block is found", func(t *testing.T) {
		dbStorage = fakeDBAdapter{
			fakeFindBlock: func() []byte {
				b := &Block{
					Height: 1,
				}
				return utils.ToBytes(b)
			},
		}
		block, _ := FindBlock("xx")
		if block.Height != 1 {
			t.Error("Block shoould be found.")
		}
	})
}

func TestBlockchain(t *testing.T) {
	t.Run("Should create blockchain", func(t *testing.T) {
		dbStorage = fakeDBAdapter{
			fakeLoadChain: func() []byte {
				return nil
			},
		}
		bc := Blockchain()
		if bc.Height != 1 {
			t.Error("Blockchain() should create a blockchain")
		}
	})
	t.Run("Should restore blockchain", func(t *testing.T) {
		once = *new(sync.Once)
		dbStorage = fakeDBAdapter{
			fakeLoadChain: func() []byte {
				bc := &blockchain{Height: 2, NewestHash: "xxx", CurrentDifficulty: 1}
				return utils.ToBytes(bc)
			},
		}
		bc := Blockchain()
		if bc.Height != 2 {
			t.Errorf("Blockchain() should restore a blockchain with a height of %d, got %d", 2, bc.Height)
		}
	})
}

 

 

 

 

3. fake database 및 fake function을 이용한 unit test 다이어그램

fake database and fake function
fake database and fake function for unit test

 

반응형

댓글