반응형

Go의 인기 Web Framework 'Gin' 


해당 튜토리얼은 'Building Go Web Applications and Microservices Using Gin'의 내용을 번역하며 개인적으로 정리한 것입니다.


이번 시간에는 기사 리스트의 뼈대를 만들고 테스트해보도록 하겠습니다.

 

1. 뷰 템플릿 작성

 

메인 페이지에 기사(Article) 리스트가 표시될 예정이므로 새로 템플릿을 만들 필요는 없습니다. 하지만 현재의 내용을 기사 리스트로 대체하려면 index.html 템플릿에 변경이 필요합니다.

 

이 수정사항을 적용하기 위해서 기사 리스트가 payload라는 변수의 템플릿에 전달된다고 해봅시다. 이러한 가정하에서 다음의 snippet은 모든 기사의 리스트들을 표시해야 합니다.

 

  {{range .payload }}
    <!--ID를 기준으로 기사에 대한 링크를 만듭니다-->
    <a href="/article/view/{{.ID}}">
      <!--기사의 제목을 표시-->
      <h2>{{.Title}}</h2>
    </a>
    <!--기사의 컨텐츠를 표시-->
    <p>{{.Content}}</p>
  {{end}}

 

이 snippet은 payload 변수의 모든 항목을 반복하고, 각 기사의 제목과 컨텐츠를 표시합니다. 위의 snippet도 각 기사로 연결됩니다. 아직 개별 기사들을 표시하기 위한 라우트 핸들러를 정의하지 않았기 때문에 이 링크는 예상대로 작동되지 않을 것입니다.

 

업데이트 된 index.html 파일은 다음의 코드를 포함하여야 합니다.

 

<!--index.html-->

<!-- header.html 템플릿을 이 위치에 포함시킵니다. -->
{{ template "header.html" .}}

  <!--Loop over the `payload` variable, which is the list of articles-->
  {{range .payload }}
    <!-- ID를 기준으로 기사에 대한 링크를 만듭니다. -->
    <a href="/article/view/{{.ID}}">
      <!-- 기사의 제목을 표시 -->
      <h2>{{.Title}}</h2>
    </a>
    <!-- 기사의 컨텐츠를 표시 -->
    <p>{{.Content}}</p>
  {{end}}

<!-- footer.html 템플릿을 이 위치에 포함시킵니다 -->
{{ template "footer.html" .}}

 

2. 유닛 테스트를 포함한 라우터 핸들러 요구사항 지정

 

index 경로에 대한 핸들러를 만들기 전에 이 라우터 핸들러의 예상 동작을 정의하는 테스트를 먼저 만들어보도록 합시다. 이 테스트는 다음의 조건들을 확인해볼 것입니다

 

  1.  핸들러는 HTTP status 200을 응답합니다
  2.  반환된 HTML에는 'Home page' 텍스트가 포함된 제목 태그를 포함합니다. 

테스트를 위한 코드는 handlers.article_test.go 파일의 TestShowIndexPageUnauthentified 함수에 저장됩니다. 우리는 이 함수가 사용하는 헬퍼 기능을 common_test.go 파일에 두도록 할 것입니다.

 

handler.article_test.go의 내용은 다음과 같습니다

 

//------------------- handlers.article_test.go -----------------//

package main

import (
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

// ----- 홈페이지에 대한 GET 요청이 인증되지 않은 사용자에 대해 HTTP 200 코드가 있는 홈페이지를 반환하는지 테스트합니다 ----- //
func TestShowIndexPageUnauthenticated(t *testing.T) {
	r := getRouter(true)

	r.GET("/", showIndexPage)

	// ---- 위의 라우터로 보낼 요청을 생성합니다 ----- //
	req, _ := http.NewRequest("GET", "/", nil)

	testHTTPResponse(t, r, req, func(w *httptest.ResponseRecorder) bool {
		// ----- HTTP status 코드가 200인지 확인합니다 ----- //
		statusOK := w.Code == http.StatusOK

		// ------ 페이지의 제목이 'Home Page' 인지 테스트합니다 ------- //
		// --- HTML 페이지를 파싱하고 처리할 수 있는 라이브러리를 사용하여 훨씬 디테일한 테스트를 수행합니다 --- //
		p, err := ioutil.ReadAll(w.Body)
		pageOK := err == nil && strings.Index(string(p), "<title>Home Page</title>") > 0

		return statusOK && pageOK
	})
}

 

common_test.go 의 내용은 다음과 같습니다.

 

package main

import (
	"net/http"
	"net/http/httptest"
	"os"
	"testing"

	"github.com/gin-gonic/gin"
)

var tmpArticleList []article

// ------- 이 함수는 테스트 함수를 실행하기 전, 설정에 사용합니다 ----- //
func TestMain(m *testing.M) {
	// ----- Gin을 테스트 모드로 설정합니다 ----- //
	gin.SetMode(gin.TestMode)

	// ------ 다른 테스트를 실행합니다 ------ //
	os.Exit(m.Run())
}

// ---- 테스트 중 라우터를 만드는 헬퍼 함수 ---- //
func getRouter(withTemplates bool) *gin.Engine {
	r := gin.Default()
	if withTemplates {
		r.LoadHTMLGlob("templates/*")
	}
	return r
}

// ----- 요청을 처리하고 응답을 테스트하는 헬퍼 함수 ------- //
func testHTTPResponse(t *testing.T, r *gin.Engine, req *http.Request, f func(w *httptest.ResponseRecorder) bool) {

	// ---- 응답 레코더 생성 ---- //
	w := httptest.NewRecorder()

	// ------ 서비스를 생성하고 위의 요청을 처리합니다 ----- //
	r.ServeHTTP(w, req)

	if !f(w) {
		t.Fail()
	}
}

// ---- 이 함수는 테스트를 위해 메인 리스트를 임시 리스트에 저장하는데 사용됩니다 ----- //
func saveLists() {
	tmpArticleList = articleList
}

// ------ 이 함수는 임시 리스트에서 메인 리스트를 복구하는데 사용됩니다 ------ //
func restoreLists() {
	articleList = tmpArticleList
}

 

우리는 이 테스트를 구현하기 위해 몇가지의 헬프 함수를 작성했습니다. 이 함수들은 유사한 기능을 테스트하기 위해 추가 테스트를 작성할 때, 상용 코드를 줄이는 데 도움을 줄 것입니다.

 

TestMain 함수는 Gin이 테스트 모드를 사용하도록 설정하고, 나머지 테스트 함수를 호출합니다. getRouter 함수는 메인 어플리케이션과 유사한 방법으로 라우터를 만들고 반환합니다. saveLists() 함수는 원본 기사 목록을 임시 변수에 저장합니다. 이 임시 변수는 restoreLists() 함수에 의해 유닛 테스트가 실행된 후 기사 리스트를 초기 상태로 복원하는데 사용됩니다.

 

최종적으로, testHTTPResponse 함수는 전달된 함수를 실행하여 테스트의 성공 여부를 나타내는 boolean true 값을 반환하는지 확인합니다. 이 함수는 HTTP 요청의 응답을 테스트하는 데 필요한 코드가 중복이 되지 않도록 하는데 도움을 줄 것입니다.

 

HTTP 코드와 반환된 HTML을 확인하려면 다음의 몇가지를 수행합니다.

 

  1.  새 라우터를 생성합니다.
  2. 기본 App이 사용하는 것과 동일한 핸들러를 사용하도록 경로를 정의합니다. (ShowIndexPage)
  3. 이 라우터에 접근할 수 있는 새 요청을 생성합니다.
  4. HTTP 코드와 HTML을 테스트하기 위해 응답을 처리하는 함수를 만듭니다.
  5. testHTTPResponse() 함수를 호출하여 이 테스트를 완료합니다

 

3. 라우터 핸들러 만들기

 

우리는 handler.article.go 파일에 기사와 관련된 기능에 대한 모든 라우터 핸들러를 생성할 것입니다. 인덱스 페이지 핸들러 showIndexPage 는 다음의 작업을 수행하도록 합니다.

 

3-1. 문서 목록을 가져옵니다.

 

이 작업은 이전에 정의된 getAllArticles 함수를 사용하여 수행될 수 있습니다.

 

articles := getAllArticles()

 

 

3-2. 기사 리스트를 전달하는 index.html 템플릿을 렌더링합니다.

 

이 작업은 아래의 코드를 사용합니다.

 

c.HTML(
    // ----- HTTP Status를 200(OK)로 설정합니다 ----- //
    http.StatusOK,
    // ------ index.html 템플릿을 사용합니다 ------ //
    "index.html",
    // ---- 페이지에서 사용하는 데이터를 전달합니다 ---- //
    gin.H{
        "title":   "Home Page",
        "payload": articles,
    },
)

 

이전 단계의 버전과의 유일한 차이점은 payload라는 변수가 템플릿에 접근할 기사 리스트를 전달한다는 것입니다.

 

handlers.article.go 는 다음의 코드를 포함하여야 합니다.

 

// handlers.article.go

package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

func showIndexPage(c *gin.Context) {
	articles := getAllArticles()

	// ---- Context의 HTML 메소드를 호출하여 템플릿을 렌더링합니다 ---- //
	c.HTML(
		// ----- HTTP Status를 200(OK)로 설정합니다 ------ //
		http.StatusOK,
		// ----- index.html 템플릿을 사용합니다 ------ //
		"index.html",
		// ---- 페이지에서 사용하는 데이터를 전달합니다 ----- //
		gin.H{
			"title":   "Home Page",
			"payload": articles,
		},
	)

}

 

이제 어플리케이션을 실행하고 브라우저에서 https://localhost:8080 에 접속하면 다음과 같은 화면을 볼 수 있습니다.

 

기사 제목과 내용이 보입니다!

 

현재까지의 소스 tree는 다음과 같습니다.

 


다음 시간에는 메인 페이지가 아닌 기사 하나하나의 링크에 맞는 경로 설정을 해보도록 할 예정입니다. 이번 포스팅에서는 많은 내용의 코딩이 필요하므로 실수하지 않도록 꼼꼼하게 체크해주는 것도 필요합니다! 

반응형

+ Recent posts