Go 에서 단위 테스트하기

노력을 즐기는 사람·2022년 6월 12일
1
post-thumbnail

시작하기 전

회사에서 go를 다루게 되었다. go의 철학은 java의 철학과 비슷한듯 하며 다르다. go의 interface 의 특징을 이용해서 단위 테스트 코드를 작성한 경험을 포스팅하며 go의 철학을 조금이나마 이해해보자

Go는 어떻게 Mocking 할까?

단위 테스트를 하기 위해서는 테스트 대상이 아닌 녀석들을 테스트 더블로 대체하곤 한다. 이번에도 ResponseWriter 라는 인터페이스를 Mock으로 대체할 생각이다. 자바에서는 테스트 더블 객체를 생성하기 위해서 다형성을 이용한다. 인터페이스의 인스턴스라면 구현 객체를 새로 정의하고 클래스면 상속 객체를 새로 정의한다. Go에서도 인터페이스가 존재한다. 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 의 로직에 의해서 테스트가 실패하는 일이 없어야한다.

ResponseWriter

Mocking하기 위해서 ResponseWriter 의 명세를 살펴보자

type ResponseWriter interface {
	Header() Header
	Write([]byte) (int, error)
	WriteHeader(statusCode int)
}

한가지 알아둬야할 점은 Header()WriteHeader()Write() 순서로 호출 되어야 한다는 점이다.

ResponseWriter Mock

그래서 아래와 같이 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() 메서드 호출 이전에 호출되어야 한다. 그 외에는 로직을 포함하고 있지 않도록 작성되었다.

Test

이제 Mocking이 완료되었으니 테스트 코드를 작성해보자.

총 두 가지를 테스트해야한다.

  1. Mock 구조체가 Header() 메서드를 가장 먼저 호출하도록 작성이 되었는지
  2. 필터링 로직이 제대로 동작하는지

먼저, 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 라는 문구를 이해할 수 있을 때 까지 많이 연습하고 코딩해야할 것 같다. 지금은 장점이 뭔지 단점이 뭔지 설명하는 것이 어렵다고 느껴진다. 그저 활용만 하고 있는 수준이라서 글도 잘 안써졌다.

profile
노력하는 자는 즐기는 자를 이길 수 없다

0개의 댓글