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
은 백업) - 위 순서를 거치면
r
이w
가 쓴 데이터를 읽기 때문에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