Go에서 Stdin에 대한 테스트 코드 작성하기

테스트 코드 작성 중 우리가 예측 가능한 함수 인자 값은 쉽게 체크가 가능하지만, 시스템으로 부터 넘어오는 데이터는 막상 작성하려고 하면 어떻게 해야할지 고민이 되기 시작합니다.

오늘은 그 중 하나인 Stdin에 대한 테스트 코드 작성 이야기를 하려고 합니다.

Pipe trick

Stdin은 테스트 코드상에서 os.Pipe()와 간단한 트릭을 사용해 통제할 수 있습니다.

os.Pipe()

먼저 os.Pipe() 는 아래와 같은 리턴을 가집니다. 그리고 설명을 읽어보면 첫번째 리턴인 r과 두번째 리턴인 w가 서로 연결된 File 오브젝트라고 합니다.

func Pipe() (r *File, w *File, err error)

Pipe returns a connected pair of Files; reads from r return bytes written to w. It returns the files and an error, if any.

Flow

다음 아래 순서로 로직을 구성하면 마치 Stdin으로 데이터를 받은 것 처럼 꾸밀 수 잇습니다.

  • os.Pipe()로 연결된 File 오브젝트를 쌍으로 생성 (r=read, w=write)
  • w 에 파일을 씀
  • os.Stdin을 r 로 교체 (이 때 기존 os.Stdin은 백업)
  • 위 순서를 거치면 rw가 쓴 데이터를 읽기 때문에 os.Stdin으로 데이터가 들어감
  • 이후 다시 os.Stdin을 원복

코드로 보면 아래와 같습니다.

input := []byte("HAHWUL")
r, w, err := os.Pipe()
if err != nil {
    t.Fatal(err)
}
// os.Pipe()로 File 쌍 생성

_, err = w.Write(input)
if err != nil {
    t.Error(err)
}
w.Close()
// w에 원하는 데이터를 씀

stdin := os.Stdin
defer func() { os.Stdin = stdin }()
os.Stdin = r
// r은 w가 쓴 데이터를 읽음
// 결국 os.Stdin으로 w로 쓰여진 데이터가 들어감

Full code

package gee

import (
	"os"
	"testing"

	model "github.com/hahwul/gee/pkg/model"
)

func TestGee(t *testing.T) {
	type args struct {
		options model.Options
	}
	tests := []struct {
		name string
		args args
	}{
		//... 생략 ...
		{
			name: "test distribute (nofile)",
			args: args{
				options: model.Options{
					Append:     false,
					Distribute: true,
				},
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			input := []byte("HAHWUL")
			r, w, err := os.Pipe()
			if err != nil {
				t.Fatal(err)
			}

			_, err = w.Write(input)
			if err != nil {
				t.Error(err)
			}
			w.Close()
			stdin := os.Stdin
			defer func() { os.Stdin = stdin }()
			os.Stdin = r
			Gee(tt.args.options)
		})
	}
}

Conclusion

덕분에 Stdin으로 방치했던 구간의 테스트 코드를 개선해서 16%나 커버리지를 올렸습니다 😍

References

  • https://pkg.go.dev/os#Pipe