Go에서 자동으로 테스트 코드 생성하기(with gotests)

저는 테스트 코드를 많이 작성하진 않습니다. 아무래도 본업이 보안 엔지니어링이다 보니 현업 개발자처럼 꼼꼼하게 테스트 코드를 작성하고 조금 더 나아가 TDD(Test-Driven Development)를 신경쓰면서 개발할 수 있는 여유가 있는 것도 아니구요.

다만 그래도.. 올해부터 조금 더 신경쓰고 적용해보려고 노력중이였습니다. 아무튼 개발 단계부터 테스트를 고려하면서 만들게 되면 자연스럽게 테스트 코드 또한 따라오게 되겠지만, 이미 만들어진 프로젝트에 테스트 코드를 추가하는 경우 생각보다 번거롭고 귀찮은 작업으로 느껴집니다.

오늘은 golang에서 테스트 코드를 빠르고 쉽게 작성할 수 있도록 테스트 코드를 어느정도 자동으로 생성해주는 도구인 gotests에 대해 정리해보고 글로 남겨봅니다.

gotests

gotests는 테스트 코드를 자동으로 생성해주는 도구이자 IDE 플러그인입니다. go get으로 쉽게 설치가 가능하며, 공식 문서에 있는 go get 명령의 경우 GOMODULE 처리가 표기 안되어 있어서, 간혹 GO11MODULE 환경변수를 세팅하지 않은 분들은 설치에 에러가 발생할 수 있습니다. 아래 명령처럼 GOMODULE 설정 후 go get 해주시면 잘 설치됩니다.

GO111MODULE=on go get -u github.com/cweill/gotests/...
gotests

flag를 살펴보면 이렇습니다.

flags description
-all 모든 function과 method에 대해 test 코드를 생성합니다.
-excl 정규표현식을 이용하여 해당 룰에 un-match된 function과 method의 test 코드를 생성합니다.
-exported exported function과 exported method 만 test 코드를 생성합니다.
(외부 참조 가능 여부는 대문자로 시작하는 함수겠죠 😁)
-i 에러 메시지에서 test 코드에 사용된 input 값을 출력하여 test 코드를 생성합니다.
-only 정규표현식을 이용하여 해당 룰에 match된 function과 method의 test 코드를 생성합니다.
-nosubtests subtest 코드를 생성하지 않도록 설정합니다. >= Go 1.7
-parallel parallel subtest 코드를 생성합니다. >= Go 1.7
-w 기본적으로 gotests는 stdout으로 출력하는데, 이 내용을 지정한 파일로도 출력합니다.
-template 커스텀한 테스트 코드 생성을 위한 플래그입니다.
-template_dir
-template_params
-template_params_file

Generate test code

제 jwt-hack의 코드를 가지고 한번 생성해보겠습니다.

https://github.com/hahwul/jwt-hack/blob/master/pkg/jwt/jwt.go

package jwtInterface

import (
	"fmt"

	"github.com/golang-jwt/jwt"
)

// JWTencode is JWT interface for encode
func JWTencode(claims map[string]interface{}, secret, alg string) string {
	var key = []byte(secret)
	var jwtClaims = jwt.MapClaims(claims)

	algorithm := jwt.GetSigningMethod(alg)

	token := jwt.NewWithClaims(algorithm, jwtClaims)
	// Sign and get the complete encoded token as a string using the secret
	tokenString, err := token.SignedString(key)
	if err != nil {
		fmt.Println(err)
		return ""
	}
	return tokenString
}

// JWTdecode is JWT interface for decode
func JWTdecode(tokenString string) *jwt.Token {
	// signature := false

	// Parse the token
	var token *jwt.Token
	var err error
	parser := new(jwt.Parser)
	// Figure out correct claims type
	token, _, err = parser.ParseUnverified(tokenString, jwt.MapClaims{})
	//token, _, err = parser.ParseUnverified(tokenString, &jwt.StandardClaims{})

	if err != nil {
		fmt.Errorf("[%v] Invalid token", err)
		return nil
	}
	return token
}

// JWTdecodeWithVerify is interface for decode with verify
func JWTdecodeWithVerify(tokenString, secret string) (bool, *jwt.Token) {
	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		//Make sure that the token method conform to "SigningMethodHMAC"
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return false, nil
		}
		return []byte(secret), nil
	})
	if err != nil {
		return false, nil
	}
	return true, token
}

gotests로 테스트 코드를 생성해보면..

gotests -all jwt.go

이렇게 화면에 stdout을 통해 생성된 테스트 코드가 출력됩니다. 저는 보통 assert 이용해서 쭉 나열하는 식으로 테스트 코드를 작성했었는데, 확실히 조금 더 구조화된 테스트 코드 포맷을 제공해주는 것 같네요 :D

package jwtInterface

import (
	"reflect"
	"testing"

	"github.com/golang-jwt/jwt"
)

func TestJWTencode(t *testing.T) {
	type args struct {
		claims map[string]interface{}
		secret string
		alg    string
	}
	tests := []struct {
		name string
		args args
		want string
	}{
		// TODO: Add test cases.
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := JWTencode(tt.args.claims, tt.args.secret, tt.args.alg); got != tt.want {
				t.Errorf("JWTencode() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestJWTdecode(t *testing.T) {
	type args struct {
		tokenString string
	}
	tests := []struct {
		name string
		args args
		want *jwt.Token
	}{
		// TODO: Add test cases.
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := JWTdecode(tt.args.tokenString); !reflect.DeepEqual(got, tt.want) {
				t.Errorf("JWTdecode() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestJWTdecodeWithVerify(t *testing.T) {
	type args struct {
		tokenString string
		secret      string
	}
	tests := []struct {
		name  string
		args  args
		want  bool
		want1 *jwt.Token
	}{
		// TODO: Add test cases.
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, got1 := JWTdecodeWithVerify(tt.args.tokenString, tt.args.secret)
			if got != tt.want {
				t.Errorf("JWTdecodeWithVerify() got = %v, want %v", got, tt.want)
			}
			if !reflect.DeepEqual(got1, tt.want1) {
				t.Errorf("JWTdecodeWithVerify() got1 = %v, want %v", got1, tt.want1)
			}
		})
	}
}

Using on Atom

gotests는 일반적으로 사용되는 IDE들은 대다수 지원하기 때문에 당연히 Atom 또한 지원 대상 중 하나입니다. 아래 atom pkg로 접근하면 바로 설치해보실 수 있습니다.

https://atom.io/packages/gotests

Install 누르면 Atom으로 이동되어 설치를 진행합니다.

이후에 다른 Atom plugin처럼 상단 메뉴의 Package에서 gotests 항목을 보면 generate 라는 메뉴가 생기는 걸 볼 수 있습니다. 파일을 연 상태에서 해당 버튼을 누르거나 단축키를 눌러주면 바로 테스트 코드를 생성하여 파일로 만들어줍니다.

코드 위에서 package > gotests > generate 를 누르면..

이렇게 테스트 코드 파일이 생성됩니다.

References