본문 바로가기
개인 프로젝트/coin

[Project coin] 단위 테스트(3) - sync.Once가 포함된 코드

by _royJang 2022. 8. 19.
반응형

Project coin 경과

니코 선생님의 coin 클론코딩이 거의 막바지이다. 강의가 몇개 남지 않았지만 테스트 코드를 싹싹 핥아 먹으며 공부하기 위해 강의를 보기 전 테스트 코드를 만들고 강의를 보고 있다.

문제 발생

스스로 테스트 코드를 만들며 아무리 생각해도 이상한 현상을 발견했다.

 

테스트하려는 코드

func Blockchain() *blockchain {
    once.Do(func() {
        b = &blockchain{
            Height: 0,
        }
        checkpoint := 블록체인_불러오기()
        if checkpoint == nil {
            //새로운 블록 추가
        } else {
            //기존 블록체인 변수 b에 할당
        }
    })
    return b
}

 

테스트 코드

func TestBlockchain(t *testing.T) {
    t.Run("불러올 블록체인 없을 때", func(t *testing.T) {
        dbStorage = fakeDB{
            fakeLoadChain: func() []byte {
                return nil
            },
        }
        bc := Blockchain()
        if bc.Height != 1 {
            t.Error("Height가 1인 블록체인이 만들어 져야합니다.")
        }
    })
    t.Run("불러올 블록체인 있을 때", func(t *testing.T) {
        dbStorage = fakeDB{
            fakeLoadChain: func() []byte {
                b := &blockchain{
                    Height: 2,
                }
                return utils.ToBytes(b)
            },
        }
        bc := Blockchain()
        if bc.Height != 2 {
            t.Errorf("Height가 %d인 블록체인이 불려져야 하지만, Height가 %d인 블록체인이 불려집니다.", 2, bc.Height)
        }
        if reflect.TypeOf(bc) != reflect.TypeOf(&blockchain{}) {
            t.Error("Blockchain()이 반환해야 하는 타입이 옳지 않습니다.")
        }
    })
}

 

현상

testing.T가 가진 t.Run을 통해 sub test를 만들어 테스트를 하는 모습이다.
나는 JetBrain의 goLand를 사용하고 있으며 이 Idle의 기능 중 sub test를 하나씩 실행할 수 있는 기능이 있다. 이 기능을 사용하여 sub test를 하나씩 실행하면 결과는 PASS. 하지만 TestBlockchain 전체를 실행하면 FAIL이 뜬다..?

=== RUN TestBlockchain
=== RUN TestBlockchain/불러올_블록체인_없을_때

=== RUN TestBlockchain/불러올_블록체인_있을_때
    chain_test.go:53: Height가 2인 블록체인이 불려져야 하지만, Height가 1인 블록체인이 불려집니다.
--- FAIL: TestBlockchain (0.00s)
    --- PASS: TestBlockchain/불러올_블록체인_없을_때 (0.00s)
    --- FAIL: TestBlockchain/불러올_블록체인_있을_때 (0.00s)
FAIL

 

원인

강의를 보자마자 '아! 왜 기존 코드를 제대로 살피지 않았을까!' 라는 생각이 바로 들었다. 원인은 once.Do() 함수 때문이었다. 이 함수의 기능은 2021.08.26 - [Study/Design Pattern] - [Design Pattern] 싱글톤 패턴(Singleton Pattern) with Golang 에 기입해 두었었다. 글이 너무 구리다. 옛날에 포스팅한 내용들은 어떻게 해야하는것일까..

그렇다. 결국 이들은 blockchain의 코드를 공유하기에 테스트 해야하는 함수를 잘 살피고 이해할 필요가 있다. (너무 당연한 얘기이지만 아직 테스트 코드에 미숙하기에 너무 테스트 코드만 집중해 버렸다.)

 

해결 방법

해결 방법은 굉장히 간단했다.

func TestBlockchain(t *testing.T) {
   t.Run("불러올 블록체인 없을 때", func(t *testing.T) {
      dbStorage = fakeDB{
         fakeLoadChain: func() []byte {
            return nil
         },
      }
      bc := Blockchain()
      if bc.Height != 1 {
         t.Error("Height가 1인 블록체인이 만들어 져야합니다.")
      }
   })
   t.Run("불러올 블록체인 있을 때", func(t *testing.T) {
      once = sync.Once{} // 수정 부분
      dbStorage = fakeDB{
         fakeLoadChain: func() []byte {
            b := &blockchain{
               Height: 2,
            }
            return utils.ToBytes(b)
         },
      }
      bc := Blockchain()
      if bc.Height != 2 {
         t.Errorf("Height가 %d인 블록체인이 불려져야 하지만, Height가 %d인 블록체인이 불려집니다.", 2, bc.Height)
      }
      if reflect.TypeOf(bc) != reflect.TypeOf(&blockchain{}) {
         t.Error("Blockchain()이 반환해야 하는 타입이 옳지 않습니다.")
      }
   })
}

once를 다시 초기화시켜 마치 처음 불려지는 것 처럼 속이는 것이다.

 

수정 후 테스트

=== RUN TestBlockchain
=== RUN TestBlockchain/불러올_블록체인_없을_때
 
=== RUN TestBlockchain/불러올_블록체인_있을_때
--- PASS: TestBlockchain (0.00s)
    --- PASS: TestBlockchain/불러올_블록체인_없을_때 (0.00s)
    --- PASS: TestBlockchain/불러올_블록체인_있을_때 (0.00s)
PASS

 

 

느낀 점

테스트 코드를 작성할 때에 조심해야 할 부분을 배울 수 있었다. 테스트 코드는 기존의 함수를 테스트 하는 것이다. 테스트를 위한 코드이다. 테스트 코드가 중심이 되어선 안된다.

반응형