본문 바로가기
golang

[golang] go언어 mockup을 통한 테스트 코드 작성법(go mock)

by devjh 2022. 4. 11.
반응형

이번 게시글에서는 mockup을 통한 테스트 코드 작성법에 대해서 정리합니다.

1. 테스트종류

프로그래밍의 테스트방식은 상태기반 테스트와 행위기반 테스트로 나뉩니다.

 

(1). 상태기반테스트

db등의 상태를 직접 변경하고 정상적으로 변경됐는지 조회하는 테스트 방식을 말하며 통합테스트라고 불립니다.

 

(2). 행위기반 테스트

내 코드의 행위만 검증하는 테스트로 유닛테스트라고 불립니다.

내 코드의 행위만 검증해야하므로 외부에 의존한 모듈들은 mockup하여 테스트합니다.

실제 모듈을 만들어 사용하기에 시간, 비용 등의 Cost가 너무 높다면,

가짜객체를 만들어 가짜객체가 원하는행위를 하도록 정의한 후 타 컴포넌트에 의존하지 않는 순수한 나의 코드만 테스트할 수 있습니다.

가짜객체를 만드는 행위를 mockup이라고 합니다.

 

2. golang에서 mockup의 방식

golang에서는 크게 세가지 방식을 사용하여 mockup을 합니다.

  • 객체지향 설계를 통해 di, ioc를 사용하는 방식(mockup한 struct를 주입하여 테스트)
  • 라이브러리에서 제공하는 mock client를 사용
  • 기존의 라이브러리를 호출할때 추상을 호출하도록 변경(테스트할때 mockup한 결과물을 추상에 주입)

세가지 방식 모두 구현이 아닌 추상(인터페이스)에 의존한 상태에서 추상을 mock객체로 채워 준 후 제어한다는 공통점을 가집니다.

각각의 방식에 대해 예제를 정리합니다.

 

3. 객체지향 설계를 통해 di, ioc를 사용하여 mock 컴포넌트를 주입

첫번째 방식은 스프링의 방식과 유사합니다.

스프링은 스스로 di ioc를 해주고 내부적으로 프록시, 어댑터 패턴을 사용하였기에 @Mock @Mockbean 등의 어노테이션만 붙여주면 쉽게 의존을 제어할 수 있지만

golang은 객체지향 언어가 아닙니다. 

직접 struct와 interface를 이용하여 추상에 의존하도록 레이어를 구축해준후 mock구조체를 직접 주입하여 사용할 수 있습니다.

 

[golang] golang echo framework와 layered architecture를 활용한 백엔드 api 서버 구축하기

이번에 golang으로 백엔드 api 서버를 구축하게 되어 관련내용 및 느낀점을 정리합니다. 1. 프레임워크 선택하기 여러 프레임워크를 살짝 맛본결과 echo가 좋다고 판단하여 echo를 선택하게 되었습니

frozenpond.tistory.com

 

4. 라이브러리에서 제공하는 mock client를 사용

이전 게시글에 redis-mock 사용법을 정리해놓은 예제로 대체합니다.

미리 구축된 mock client를 사용하는 방식입니다.

 

[golang] go-redis, redis-mock 사용법 및 예제(suite 사용법)

이번 게시글에서는 go-redis 패키지를 사용하는 컴포넌트 구축 방법을 정리하고 유닛테스트(행위기반 테스트)와, 통합테스트(상태기반 테스트)방식의 테스트 코드 작성법(suite, mock사용)을 예제로

frozenpond.tistory.com

 

5. 기존의 라이브러리를 호출할때 추상을 호출하도록 변경(테스트할때 mockup한 결과물을 추상에 주입)

프로젝트가 di ioc를 사용하지 않았으며, library에서 mock client를 제공하지 않을때 사용하는 방식입니다.

자바의 pojo 같은 느낌입니다.

이전게시글인 go-aws-sdk-v2의 copyobject메서드를 테스트하는 예제입니다. 

mock 구현체를 구현해주는 라이브러리와, assertion을 제공하는 라이브러리는

mockery와 testify를 사용하였습니다.

 

소스코드는 아래의 github에서 확인할 수 있습니다.

https://github.com/jaeho310/unit-test-tut

 

GitHub - jaeho310/unit-test-tut

Contribute to jaeho310/unit-test-tut development by creating an account on GitHub.

github.com

 

(1). 추상에 의존하도록 변경

package s3_service

import (
   "context"
   "log"
   "net/url"

   "github.com/aws/aws-sdk-go-v2/config"
   "github.com/aws/aws-sdk-go-v2/service/s3"
)

var s3Client S3Client

type S3Client interface {
   CopyObject(ctx context.Context, params *s3.CopyObjectInput, optFns ...func(*s3.Options)) (*s3.CopyObjectOutput, error)
}

func init() {
   log.Println("load config")
   cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithSharedConfigProfile("your-profile"))
   if err != nil {
      log.Fatal(err)
   }
   log.Println("create s3 client")
   SetS3Client(s3.NewFromConfig(cfg))
}

func SetS3Client(client S3Client) {
   s3Client = client
}

func CopyS3Object() error {
   sourceBucket := url.PathEscape("my-foo-test2")                              // 복사한 오브젝트가 들어갈 버킷
   destinationBucket := url.PathEscape("my-foo-test1" + "/" + "my-object.txt") // 복사할 원본의 위치. 버킷/디렉토리/파일.확장자 풀네임으로 입력
   objectName := "my-copied-object"                                            // 복사한 오브젝트의 새로운 이름

   input := &s3.CopyObjectInput{
      Bucket:     &sourceBucket,
      CopySource: &destinationBucket,
      Key:        &objectName,
      // StorageClass: , copyobject는 storage타입 등 go-sdk에서 직접 제공하지 않는 설정을 바꿀때 사용할 수 있습니다.
   }
   _, err := s3Client.CopyObject(context.TODO(), input)
   if err != nil {
      return err
   }
   log.Println("Copied " + objectName + " from " + sourceBucket + " to " + destinationBucket)
   return nil
}

S3client라는 인터페이스를 만들어주고 package변수로 가져간 후

해당 인터페이스를 set하는 메서드를 만들어줍니다.

이제 해당 라이브러리를 직접 호출하지 않고, interface를 호출하므로 ioc가 적용된 상황이며

SetS3Client 메서드에서 추상에 의존한 아규먼트 패싱을 하여 di를 하는 상황입니다.

(위의 코드는 약간의 오류가 있는것 같지만 aws의 copyobject example 코드입니다 sourceBucket과 destinationBucket은 제가 달아놓은 주석을 기반으로 사용해야 올바르게 동작 합니다)

 

 

(2).  테스트 관련 모듈 다운로드

$ go get github.com/vektra/mockery/v2/...
$ go get github.com/stretchr/testify

$ mockery --all --keeptree

mockery와 testify를 다운로드 받고

$ mockery --all --keeptree

를 입력해서 mockup된 test모듈을 생성합니다.(해당 명령어를 입력하면 모든 인터페이스의 mock구현체가 생성되며 해당 구현체의 실행계획을 개발자가 관리할 수 있습니다.)

 

(3). 테스트코드 작성

package s3_service

import (
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/mock"
	"testing"
	mocks "unit-test-tut/mocks/s3-service"
)

func TestCopyObject(t *testing.T) {
	// given
	mockClient := &mocks.S3Client{}
	SetS3Client(mockClient)
	mockClient.On("CopyObject", mock.Anything, mock.Anything).Return(nil, nil)

	// when
	err := CopyS3Object()

	// then
	assert.Equal(t, err, nil)
}

client를 셋해주는 메서드를 사용하여 mockery로 생성한 mockClient를 set해줍니다.

이제 given 에서 mockClient의 실행계획을 정의하고 

when 과 then 영역에서 실행후 assert 체크를 해줍니다.(예제에서는 CopyObject메서드에 임의의 아규먼트가 두개 들어가면 nil, nil을 리턴하도록 실행계획을 제어해놨습니다.)

반응형

댓글