회사에서 go를 다루게 되었다. go의 철학은 java의 철학과 비슷한듯 하며 다르다. go의 interface 의 특징을 이용해서 단위 테스트 코드를 작성한 경험을 포스팅하며 go의 철학을 조금이나마 이해해보자
단위 테스트를 하기 위해서는 테스트 대상이 아닌 녀석들을 테스트 더블로 대체하곤 한다. 이번에도 ResponseWriter
라는 인터페이스를 Mock으로 대체할 생각이다. 자바에서는 테스트 더블 객체를 생성하기 위해서 다형성을 이용한다. 인터페이스의 인스턴스라면 구현 객체를 새로 정의하고 클래스면 상속 객체를 새로 정의한다. Go에서도 인터페이스가 존재한다. Go에서는 인터페이스를 어떻게 재정의 해줘야할까?
자바에서는 인터페이스와 구현 클래스 간의 관계를 implements
키워드를 사용해서 명시적으로 표현했다. Go에서는 그렇지 않다. 키워드 같은 건 없고 그냥 구조체가 인터페이스의 함수들을 냅다 구현하면 된다. 예시 코드로 살펴보자
type I interface {
M()
}
type T struct {
S string
}
func (t T) M() {
fmt.Println(t.S)
}
func main() {
var i I = T{"hello"}
i.M()
}
인터페이스 I
는 그냥 존재할 뿐이다. 그리고 I
는 모르지만 어디선가 I
의 구현체가 사용된다. 자바와 비슷하지만 다르다. 관계가 없으니 의존성이 없고 유연하면서 테스트하기 좋아진다.
이제 테스트를 작성해보자.
웹 서버에 특정 유저가 가지고 있는 디바이스들 중 서버와 연결된 녀석들을 모두 조회하는 API를 테스트하고 싶다. 그 중에서도 특정 유저가 가지고 있는 연결된 디바이스를 필터링하는 로직
을 테스트하고 싶다. 코드로 살펴보자
func Devices(conn *Connection, rw ResponseWriter, req Request) {
result := filter(req.Body()) // 테스트 대상
// 아래 세줄은 Mock 대상
rw.Write(result);
rw.Header().Set("Content-Type", "application/json");
rw.WriterHeader(http.StatusOK)
}
ResponseWriter
로직들은 테스트 대상이 아니다. ResponseWriter
의 로직에 의해서 테스트가 실패하는 일이 없어야한다.
Mocking하기 위해서 ResponseWriter
의 명세를 살펴보자
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
}
한가지 알아둬야할 점은 Header()
→ WriteHeader()
→ Write()
순서로 호출 되어야 한다는 점이다.
그래서 아래와 같이 Mocking 했다.
type ResponseWriterMock struct {
body []byte
isCalledWrite bool
isCalledWriteHeader bool
}
func (rw *ResponseWriterMock) Header() http.Header {
if rw.isCalledWrite || rw.isCalledWriteHeader {
panic("Header() must called before called Wrtie(), WriteHeader()")
}
return http.Header{}
}
func (rw *ResponseWriterMock) Write(body []byte) (int, error) {
rw.isCalledWrite = true
rw.body = body
return len(rw.body), nil
}
func (rw *ResponseWriterMock) WriteHeader(int) {
rw.isCalledWriteHeader = true
}
func (rw *ResponseWriterMock) GetBody() []byte {
return rw.body
}
Header()
메서드는 반드시 Write()
WriteHeader()
메서드 호출 이전에 호출되어야 한다. 그 외에는 로직을 포함하고 있지 않도록 작성되었다.
이제 Mocking이 완료되었으니 테스트 코드를 작성해보자.
총 두 가지를 테스트해야한다.
Header()
메서드를 가장 먼저 호출하도록 작성이 되었는지 먼저, Mock 구조체 테스트이다.
func TestResponseWriterMock_MethodCallOrder(t *testing.T) {
// given
rw : ResponseWriteMock
expected := "Header() must called before called Write(), WriteHeader()"
// when
when := func() {
rw.Write([]byte(""))
rw.Header()
}
// then
assert.PanicsWithValue(t, expected, when)
}
when 에서 Write()
호출 후에 Header()
를 호출하도록 했다. 반드시 panic
이 발생해야 하고 테스트는 통과했다.
다음으로 필터링 로직 테스트이다.
func TestDevices_FilterByClient(t *testing.T) {
// given
conn := NewConnection()
conn.RegisterConnection(ClientId: "test")
conn.RegisterConnection(ClientId: "test2")
conn.RegisterConnection(ClientId: "test3")
rw := ResponseWriterMock{}
req := NewRequest(http.MethodPost, "test url", ClientIds: []string{"test", "test2"})
// when
Devices(manager, rw, req)
// then
actual := rw.getBody()
expected := 2
assert.Len(t, then, expected)
}
자바의 Mockito 같은 녀석이 생각나지 않을정도로 편했다. 생산성은 그랬다. 그런데 go가 가지고 있는 철학을 관통하지는 못했다. accept interface, return struct
라는 문구를 이해할 수 있을 때 까지 많이 연습하고 코딩해야할 것 같다. 지금은 장점이 뭔지 단점이 뭔지 설명하는 것이 어렵다고 느껴진다. 그저 활용만 하고 있는 수준이라서 글도 잘 안써졌다.