본문 바로가기
Golang

Golang과 함께하는 테스트 여정 - 단위 테스트

by Limm_jk 2023. 5. 5.

큰 규모의 개발에서 테스트는 상당히 중요합니다. 어딘가의 코드 한 부분을 변경했는데, 예상치 못한 다른 곳에서 깨지는 경험을 해보면 테스트가 얼마나 중요한지 왜 없어서는 안 되는지 느끼리라 믿어 의심치 않습니다.

그렇기에 많은 언어 환경에서 테스트를 잘 할 수 있도록 라이브러리를 많이 제공하는 편인데요. 그중 사내에서 Golang 신규 프로젝트를 세팅하고, 이로 인하여 편리하고 좋은 테스트 환경을 구축하기 위하여 고민했던 것들을 두 편으로 나눠서 정리해보려 합니다. 그중, 오늘은 단위 테스트와 환경 세팅에 대하여 이야기해보려 합니다.

 

단위 테스트

테스트 환경을 세팅을 시작하며 어떤 환경이 먼저 세팅되어야 할지 고민했고, 가장 먼저 e2e 회귀 테스트를 보장하기 위한 통합테스트 환경을 먼저 구축하려 했습니다. 하지만, DI Framework가 지속적으로 변경되고 있는 상황이어서 알맞은 테스트 코어 환경을 구축하는 것이 큰 의미가 없는 시점이라는 생각이 들었고, 무엇보다 Golang에 대한 이해도가 그렇게 높지 않은 상황에서 외부환경이 개입함에도 안정적인 통합 테스트 환경을 구축하는 것이 쉽지 않을 것이라는 생각이 들었습니다. 그래서 먼저 간단하게 단위 테스트를 진행할 수 있는 환경을 구축하고, 이를 기반으로 점진적으로 테스트 환경을 늘려나가고자 하였습니다.

 

테스트 라이브러리

이를 위하여 가장 먼저 고민해야 했던 것은 테스트 라이브러리와 모킹이었습니다. 개인적으로 테스트 또한 직관적으로 잘 읽혀야 좋은 코드라고 생각을 하고, 예쁜 테스트 함수들은 이를 잘 도와준다고 생각합니다. 그렇기 때문에, 직관적인 네이밍으로 테스트의 이해도를 늘려줄 수 있는 좋은 라이브러리를 찾고자 하였고, 추가적으로 mocking도 지원이 되면 더할 나위 없이 좋으리라 생각했습니다.

 

이와 같은 관점에서 저희는 golang standard library 대신 testify라는 library를 사용하기로 했습니다.

https://github.com/stretchr/testify

 

GitHub - stretchr/testify: A toolkit with common assertions and mocks that plays nicely with the standard library

A toolkit with common assertions and mocks that plays nicely with the standard library - GitHub - stretchr/testify: A toolkit with common assertions and mocks that plays nicely with the standard li...

github.com

 

이유는 assert 패키지에서 제공하는 다양한 테스트 함수들로 인한 코드 가독성 향상, 그리고 읽기 좋은 오류 메세지를 큰 이유로 꼽았습니다. 부가적으로 test suite으로 테스트를 묶어서 사용하고, 테스트 이전, 이후에 입력할 코드들을 공통화할 수 있는 점도 좋은 점이라고 생각했습니다. 

 

모킹 라이브러리

단위 테스트를 위하여는 하나 더, 넘어야 할 산이 있습니다. 바로 mocking인데요. unit이 잘 동작하는지 검증하기 위해서는 해당 unit을 제외한 부분의 참여를 통제해야 할 필요가 있습니다. 이를 위하여 의존하는 객체의 동작을 mocking하여 통제할 수 있습니다.

이렇게 하기 위해서 우리는 generate library의 힘을 빌리기로 했습니다. 하나하나 손으로 만들어주는 것은 꽤나 고통스러운 일이리라 여겨졌거든요.

 

조사 결과 두가지 라이브러리가 많이 사용되고 있음을 알 수 있었는데요. 바로 golang 공식 organization에서 제공하는 go-mocktestify에서 제공하는 mock을 이용하여 generate해주는 mockery가 있었습니다. 이 두 가지 중 선택하기 위하여 직접 사용해 보면서 장단점을 정리해 봤습니다.

 

go-mock

https://github.com/golang/mock

 

GitHub - golang/mock: GoMock is a mocking framework for the Go programming language.

GoMock is a mocking framework for the Go programming language. - GitHub - golang/mock: GoMock is a mocking framework for the Go programming language.

github.com

장점

  • 강력한 Expectation 기능 제공. 함수 호출 순서도 검증 가능. 파라미터 검증에 Anything, MatchedBy 만 제공하는 testify와 달리 Any, Eq, Nil, Not, Matcher를 제공함. (Not 정도가 의미가 있을 것 같긴 함)
  • 기본적으로 Type safe 한 expectation을 제공해 좀 더 간결하고 단순함

단점

  • CLI 가 복잡함. 모킹 할 인터페이스, 패키지 등을 일일이 지정해줘야 함. Imbeded interface 사용 시 반드시 reflect 모드로 mock을 생성해야 함.
  • 매 테스트마다 Controller를 사용해줘야 하는 번거로움
  • 에러 메시지가 단순하고 친절하지 않음

 

mockery

https://github.com/vektra/mockery

 

GitHub - vektra/mockery: A mock code autogenerator for Golang

A mock code autogenerator for Golang. Contribute to vektra/mockery development by creating an account on GitHub.

github.com

장점

  • CLI 명령어가 훨씬 간단함 - directory based

go-mock을 사용하면 어떤 디렉토리에 mock 파일을 생성할지 규칙을 정하고 Makefile을 작성하고 해야 하지만,

mockery --all --keeptree를 사용하면 일관적으로 /mocks 아래에 대상 코드와 같은 패키지 구조를 만들어줌

  • Embeded Interface 가 존재해도 --all 옵션을 통해 mock 생성 가능
  • 에러 메시지가 더 친절하고 많은 정보를 제공해 줌
  • assertion + mock을 testify 하나로 사용 가능

단점

  • String으로 함수와 타입을 참조해야 하는 불편 → mockery 2.10.0 이상을 사용하면 해결 가능.
  • Expectation 이 Gomock 보다 약간 기능이 적음. 동작을 함수로 모킹 할 때, 리턴 타입 하나마다 함수 하나씩 넣어줘야 함. gomock 은 DoAndReturn 하나로 끝 → mockery 2.20.0 이상을 사용하면 해결 가능.

 

위와 같은 리서치 결과 결론적으로는 mockery를 사용하게 되었습니다. 둘 다 다양한 장단점이 존재하지만, 저는 CLI의 간편함과 지속적인 메인테이닝과 릴리즈에 높은 평가를 했습니다.

golang의 오픈소스 생태계는 아직 열심히 성장하고 있습니다. 그런 만큼 한 분야를 확연하게 잡고 있는 오픈소스도 거의 없다시피 하고, 그런 오픈소스 또한 어느 순간 메인테이닝이 뚝 끊기곤 합니다. 그런 것을 고려하면 후발주자였던 mockery가 지속적인 개선을 통하여 작년 1월(22년 1월)에는 expectation(type safe)를 지원한 점이 상당히 멋지다고 생각했습니다. 또한, 이렇게 지속적으로 발전해 나간다면 언젠가 많은 문제를 해결한 오픈 소스가 되어있으리라 믿어 의심치 않았습니다. 이 예상대로 올해 2월(23년 2월)에는 2.20.0을 릴리즈 하면서 향상된 리턴 함수 모킹을 지원하기 시작했습니다. 저희 또한 지속적으로 기여하여 좋은 오픈 소스가 되는 것에 기여할 수 있으면 좋을 것 같네요.

 

테스트 환경 세팅

테스트 환경을 어떻게 잘 세팅할지도 고민이었습니다.

 

가장 먼저 테스트 자동화를 위한 makefile 세팅이었습니다. 이 때, 원하는 디렉토리는 빼고 테스트를 실행하는 조건을 해결하기 위하여 고민했었는데요. 그래서 go test옵션에 --exclude와 같은 옵션이 있겠구나! 해서 찾아봤는데, 없어서 상당히 당황했었습니다. 그래서 저는 대신 grep -v 를 이용하여 exclude와 유사하게 만들었습니다.

 

unitTest:
	GO_ENV=$(TEST_STAGE) go test `go list ./... | grep -Ev '/generated/|swagger/'`

go list를 가져온 이후 grep을 이용해 원하는 디렉토리를 제거하는 방법으로 해결했습니다. 이 때, 다양한 디렉토리 조건을 grep 조건으로 지정하고자 하여 E flag를 추가로 붙여주었고, 이를 통하여 옵션에서 |를 통하여 여러 조건을 넣어줄 수 있었습니다.

 

다음으로는 config에 대한 고민이었는데요. 저희는 개발 환경(dev, production 등등)을 환경 변수로 입력받고, 이 환경에 따라서 분기 처리를 해주는 로직이 다소 존재합니다. 물론 환경 변수로 test를 잘 넣어주면 상관은 없겠지만, makefile로만 테스트를 실행하진 않기 때문에, 어떤 환경에서든 개발 환경을 잘 지정할 방법이 필요했습니다. 이 문제는 리서치하면서 테스트는 실행되고 있는 프로그램의 확장자가 .test로 끝난다는 조건을 발견하면서 해결되었는데요. 코드로는 다음과 같이 표현했습니다.

 

stage := viper.GetString("GO_ENV")
if stage == "" {
    // test 환경인지 확인하기 위하여 현재 실행되고 있는 프로그램 경로(os.Args[0])의 suffix가 .test로 끝나는지 확인함.
    if strings.HasSuffix(os.Args[0], ".test") {
        stage = StageTest
    } else {
        stage = defaultStage
    }
}

GO_ENV에서 개발 환경이 무엇인지 가져오고, 따로 지정된 것이 없다면 테스트 환경인지 확인하는 절차를 추가로 넣었습니다. os.Args를 통하여 가져오는 값들은 각각 고정적으로 의미를 가지고 있기 때문에 다음과 같이 표현할 수 있었습니다. 이를 통해서 goland를 통하여 테스트할 때 또한 test환경으로 잘 받아서 테스트할 수 있었습니다.

마무리

이를 이용하여 어떻게 테스트를 짰는지도 함께 다루려다가 이미 이 부분은 세상에 좋은 글이 많은 것 같아 제가 테스트를 작성하면서 도움을 많이 받은 글을 남기고 넘어가 보려 합니다.

https://medium.com/nerd-for-tech/writing-unit-tests-in-golang-part3-test-suite-6cca903be9ab

 

Writing unit tests in Golang Part3: Test suite

As you may know, a program usually grows and evolves over time. And as time advances, you may add new features, some edge cases handling…

medium.com

 

이 글을 읽는 여러분은 어떻게 테스트하고 계신가요? 항상 뭔가 더 좋은 방법이 있지 않을까 하는 고민이 있습니다. 혹시 제안 주시고 싶은 부분이 있다면 편하게 말씀해 주세요! 작은 조언이라도 너무 감사하게 듣겠습니다.

그럼 이만 글을 줄이고, 저는 2부 통합테스트 편으로 돌아오겠습니다. 감사합니다 :)

 

댓글