(golang) Embedding Interface 활용
인터페이스가 다른 인터페이서를 가지는 임베딩 방식을 사용하여 인터페이스를 선언할 수 있다. 예를 들어 io.ReadCloser 인터페이스는 io.Reader 와 io.Closer 인터페이스를 가지고 있다.
type Reader interface {
Read(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
type ReadCloser interface {
Reader
Closer
}
이 경우 ReadCloser 인터페이스는 아래와 동일 효과를 갖는다.
type ReadCloser interface {
Read(p []byte) (n int, err error)
Close() error
}
그런데 인터페이스를 struct 타입 안에 임베딩할 수 도 있다.
이렇게 struct 타입 안에 인터페이스를 넣는 이유는 보통 Stub 으로 유닛 테스트 코드를 만들기 쉽기 때문이다.
아래와 같이 Calculator
라는 스트럭트가 있다고 하자.
type Calculator struct {
Resolver MathResolver
}
여기에는 MathResolver
인터페이스 타입의 필드를 가지고 있다.
type MathResolver interface {
Resolve(expression string) (float64, error)
}
이렇게 인터페이스를 만들어 놓으면 MathResolver
를 Stub 으로 구현하여 테스트 코드를 만들 수 있다.
Calculator
는 계산 표현식을 가지고 실제 계산하여 결과 값을 리턴하는 Process
라는 메소스도 가진다.
func (c Calculator) Process(r io.Reader) (float64, error) {
expression, err := readOneLine(r)
if err != nil {
return 0, err
}
if len(expression) == 0 {
return 0, errors.New("no expression to read")
}
answer, err := c.Resolver.Resolve(expression)
return answer, err
}
readOneLine
함수는 계산 표현식을 한 줄만 읽어들이는 함수이며, 이렇게 읽어들인 함수를 MathResolver
타입의 Resolve
함수에 아규먼트로 넘겨서 결과를 받아오는 구조이다.
이제 Proecess
메소드에 대한 테스트 코드를 만들어 보자.
첫번째로 테스트 코드로 MathResolverStub
스트럭트와 Resolve
메소드를 간단히 구현한다.
type MathResolverStub struct{}
func (mr MathResolverStub) Resolve(expr string) (float64, error) {
switch expr {
case "2 + 4 * 10":
return 42, nil
case "( 2 + 4 ) * 10":
return 60, nil
case "( 2 + 4 * 10":
return 0, fmt.Errorf("invalid expression: %s", expr)
}
return 0, nil
}
다음으로 이 스텁을 사용한 테스트 코드를 작성한다.
func TestCalculatorProcess(t *testing.T) {
c := embed.Calculator{Resolver: MathResolverStub{}}
in := strings.NewReader(`2 + 4 * 10
( 2 + 4 ) * 10
( 2 + 4 * 10`)
data := []float64{42, 60, 0}
expectedErr := errors.New("invalid expression: ( 2 + 4 * 10")
for _, d := range data {
result, err := c.Process(in)
if err != nil {
if err.Error() != expectedErr.Error() {
t.Errorf("want (%v) got (%v)", expectedErr, err)
}
}
if result != d {
t.Errorf("Expected result %f, got %f", d, result)
}
}
}
MathResolverStub
를 가지는 Calculator
를 생성하여 Calculator 의 Process
메소드를 테스트할 수 있는 코드를 쉽게 작성할 수 있다.
테스트 코드의 전체 작성은 다음과 같다.
$ mkdir -p interface/embed
$ vi interface/embed/calculate.go
package embed
import (
"errors"
"io"
)
type Calculator struct {
Resolver MathResolver
}
type MathResolver interface {
Resolve(expression string) (float64, error)
}
func (c Calculator) Process(r io.Reader) (float64, error) {
expression, err := readOneLine(r)
if err != nil {
return 0, err
}
if len(expression) == 0 {
return 0, errors.New("no expression to read")
}
answer, err := c.Resolver.Resolve(expression)
return answer, err
}
func readOneLine(r io.Reader) (string, error) {
var out []byte
b := make([]byte, 1)
for {
_, err := r.Read(b)
if err != nil {
if err == io.EOF {
return string(out), nil
}
}
if b[0] == '\n' {
break
}
out = append(out, b[0])
}
return string(out), nil
}
$ vi interface/embed/calculate_test.go
package embed_test
import (
"errors"
"fmt"
"strings"
"testing"
"github.com/seungkyua/go-test/interface/embed"
)
type MathResolverStub struct{}
func (mr MathResolverStub) Resolve(expr string) (float64, error) {
switch expr {
case "2 + 4 * 10":
return 42, nil
case "( 2 + 4 ) * 10":
return 60, nil
case "( 2 + 4 * 10":
return 0, fmt.Errorf("invalid expression: %s", expr)
}
return 0, nil
}
func TestCalculatorProcess(t *testing.T) {
c := embed.Calculator{Resolver: MathResolverStub{}}
in := strings.NewReader(`2 + 4 * 10
( 2 + 4 ) * 10
( 2 + 4 * 10`)
data := []float64{42, 60, 0}
expectedErr := errors.New("invalid expression: ( 2 + 4 * 10")
for _, d := range data {
result, err := c.Process(in)
if err != nil {
if err.Error() != expectedErr.Error() {
t.Errorf("want (%v) got (%v)", expectedErr, err)
}
}
if result != d {
t.Errorf("Expected result %f, got %f", d, result)
}
}
}
실행을 위한 세팅 명령어는 다음과 같다.
$ go mod init github.com/seungkyua/go-test
$ go mod tidy
$ go mod vendor
$ go work init
$ go work use .
go work 는 비지니스 모듈 패키지를 아직 github 에 커밋하지 않은 상태에서 로컬의 최신 패키지 참조를 위해서 필요하다.
전체 소스 트리는 다음과 같다.
$ tree .
.
├── README.md
├── go.mod
├── go.work
└── interface
└── embed
├── calculate.go
└── calculate_test.go
다음은 테스트 실행 결과이다.
$ go test interface/embed/calculate_test.go
ok command-line-arguments 0.390s
소스는 아래의 링크에서 다운 받을 수 있다.