프로그래밍/Go

(golang) Embedding Interface 활용

seungkyua@gmail.com 2024. 3. 18. 11:47
반응형

인터페이스가 다른 인터페이서를 가지는 임베딩 방식을 사용하여 인터페이스를 선언할 수 있다. 예를 들어 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

소스는 아래의 링크에서 다운 받을 수 있다.

https://github.com/seungkyua/go-test.git

반응형