본문 바로가기
Programming/Golang

Go언어 - Gin Framework(3강 Article List)

by Wilkyway 2020. 12. 14.
반응형

Article List 표시

이 섹션에서는 index 페이지에 모든 article list를 표시하는 기능을 추가합니다.

1.Router 설정

원문에 따르면 응용 프로그램이 커질 것을 대비하여 별도의 Router파일에서 경로를 정의하는 방식으로 구성하였는데, 무슨 문제인지 제 실습 중에는 routes.go파일에 따로 코드를 분리하니 에러가 발생했습니다. 그래서 route 를 main() 함수 내부에 구성하도록 하겠습니다. 단, route handler 함수만 별도로 분리해 내도록 하겠습니다.(handlers.article.go)

 

main.go파일은 아래와 같이 코딩합니다.

package main

import (
	//"net/http"

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

var router *gin.Engine

func main() {
	router := gin.Default()

	router.LoadHTMLGlob("templates/*")
	
	router.GET("/", showIndexPage)

	router.Run()

}

2. Article Model 디자인

router를 마치기 전에 우선 Model을 구성하도록 합니다. 우리가 구성할 Article 모델은 Id, Title, Content의 세 개의 필드 만 사용하여 구성합니다. 일반적인 어플리케이션이라면 당연히 데이터베이스를 사용하지만, 예제에서는 이를 단순화하기 위해 아래와 같이 하드코딩된 List변수를 사용하도록 하겠습니다. 그리고 모든 Article List을 반환하는 함수가 필요합니다. 이 함수의 이름을 getAllArticles()로 하고 models.article.go 파일에 함께 구성합니다.

 

models.article.go

// models.article.go

package main

type article struct {
  ID      int    json:"id"
  Title   string json:"title"
  Content string json:"content"
}

// For this demo, we're storing the article list in memory
// In a real application, this list will most likely be fetched
// from a database or from static files
var articleList = []article{
  article{ID: 1, Title: "Article 1", Content: "Article 1 body"},
  article{ID: 2, Title: "Article 2", Content: "Article 2 body"},
}

// Return a list of all the articles
func getAllArticles() []article {
  return articleList
}

또한 단위 테스트를 위한 models.article_test.go 파일을 생성하고 내부에 TestGetAllArticles 함수를 작성하겠습니다.

 

models.article_test.go

// models.article_test.go

package main

import "testing"

// Test the function that fetches all articles
func TestGetAllArticles(t *testing.T) {
  alist := getAllArticles()

  // Check that the length of the list of articles returned is the
  // same as the length of the global variable holding the list
  if len(alist) != len(articleList) {
    t.Fail()
  }

  // Check that each member is identical
  for i, v := range alist {
    if v.Content != articleList[i].Content ||
      v.ID != articleList[i].ID ||
      v.Title != articleList[i].Title {

      t.Fail()
      break
    }
  }
}

위 단위 테스트에서는 getAllArticles() 함수를 이용해 모든 Article List를 가져오는 테스트 수행합니다. 이 테스트는 먼저getAllArticles() 함수로 가져온 Article List와 전역 변수 articleList의 요소 개수가 동일한지 확인합니다. 그런 다음 Article List들을 반복하여 각각의 Article이 동일한 지 확인합니다. 이 두 검사 중 하나가 실패하면 테스트는 Fail이 됩니다.

3. View Template 만들기

index.html에서 모든 Article들의 List를 표시해야합니다. Article List는 payload라는 이름의 변수로 전달하도록 하겠습니다. 아래는 모든 article list를 보여주게 됩니다. 모든 Article List에 대해 Title, Content를 표시해주게 됩니다만, 아직 라우터를 완성하지 않았으므로 표시되는 건 없습니다.

 

 

업데이트 된 index.html파일은 아래와 같습니다.

<!--index.html-->

<!--Embed the header.html template at this location-->
{{ template "header.html" .}}

  <!--Loop over the payload variable, which is the list of articles-->
  {{range .payload }}
    <!--Create the link for the article based on its ID-->
    <a href="/article/view/{{.ID}}">
      <!--Display the title of the article -->
      <h2>{{.Title}}</h2>
    </a>
    <!--Display the content of the article-->
    <p>{{.Content}}</p>
  {{end}}

<!--Embed the footer.html template at this location-->
{{ template "footer.html" .}}

(주의) header에는 웹페이지의 타이틀을 표시하는 {{.title}}변수가 있고, index에서 호출하는 {{.Title}}변수는 Article의 타이틀을 의미합니다.

4. 유닛 테스트를 통한 Route Handler 요구사항 정의

Index 라우트 생성 전에 이 route handler의 동작을 미리 예상한 테스트를 정의하겠습니다. 이 테스트에서 확인할 조건은 다음과 같습니다.

  • 핸들러는 HTTP 상태 코드 200으로 응답한다.
  • 반환 된 HTML에는 "Home Page"라는 텍스트가 포함 된 title tag가 포함된다.

 

handlers.article_test.go파일을 생성하고, 내부에 TestShowIndexPageUnauthenticated 라는 테스트 함수를 생성합니다. 그리고 이 함수에서 사용할 helper 함수를 common_test.go파일에 작성합니다.

 

handlers.article_test.go다음과 같습니다.

// handlers.article_test.go

package main

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

// Test that a GET request to the home page returns the home page with
// the HTTP code 200 for an unauthenticated user
func TestShowIndexPageUnauthenticated(t *testing.T) {
  r := getRouter(true)

  r.GET("/", showIndexPage)

  // Create a request to send to the above route
  req, _ := http.NewRequest("GET", "/", nil)

  testHTTPResponse(t, r, req, func(w *httptest.ResponseRecorder) bool {
    // Test that the http status code is 200
    statusOK := w.Code == http.StatusOK

    // Test that the page title is "Home Page"
    // You can carry out a lot more detailed tests using libraries that can
    // parse and process HTML pages
    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

// This function is used for setup before executing the test functions
func TestMain(m *testing.M) {
  //Set Gin to Test Mode
  gin.SetMode(gin.TestMode)

  // Run the other tests
  os.Exit(m.Run())
}

// Helper function to create a router during testing
func getRouter(withTemplates bool) *gin.Engine {
  r := gin.Default()
  if withTemplates {
    r.LoadHTMLGlob("templates/*")
  }
  return r
}

// Helper function to process a request and test its response
func testHTTPResponse(t *testing.T, r *gin.Engine, req *http.Request, f func(w *httptest.ResponseRecorder) bool) {

  // Create a response recorder
  w := httptest.NewRecorder()

  // Create the service and process the above request.
  r.ServeHTTP(w, req)

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

// This function is used to store the main lists into the temporary one
// for testing
func saveLists() {
  tmpArticleList = articleList
}

// This function is used to restore the main lists from the temporary one
func restoreLists() {
  articleList = tmpArticleList
}

 

테스트를 도와주는 helper 함수 몇개를 작성했습니다. 이 함수들은 유사한 기능 테스트를 위한 추가 테스트 코드를 작성할 때 상용구 코드를 줄이는 데에도 도움이됩니다.

 

TestMain함수는 Gin이 테스트 모드를 사용하도록 설정하고 나머지 테스트 함수를 호출합니다. getRouter함수는 기본 애플리케이션과 유사한 방식으로 라우터를 생성/반환합니다. saveLists() 함수는 (original) Article List를 임시 변수에 저장합니다. 임시 변수는 restoreLists() 함수에서 사용되어,  단위 테스트가 실행 된 후 Article List를 테스트 수행 전 초기 상태로 복원하는 역할을 합니다.

 

마지막으로 testHTTPResponse함수는 인자로 전달 된 함수를 실행하여 true/false를 반환하는지 확인합니다.(테스트 성공/실패). 이 함수는 HTTP 요청에 대한 응답을 테스트하는 데 필요한 중복 코드를 방지해줍니다.

 

HTTP 코드와 반환 된 HTML을 확인하기 위해 다음을 수행합니다.

  • 새 router 생성.
  • 메인 앱에서 사용하는 것과 동일한 handler를 사용하는 route 정의( showIndexPage),
  • 이 route에 접근하기 위한 새 request 생성
  • HTTP 코드와 HTML을 테스트하기 위한 응답처리 함수 생성
  • testHTTPResponse() 호출. 단, 새로 생성한 응답처리 함수와 연결.

 

5. Route Handler 생성

handlers.article.go파일에 Article 관련 기능들에 대한 모든 route handler 함수인 showIndexPage를 정의합니다. payload라는 변수를 통해 Article List를 템플릿에 전달하도록 수정하였습니다.

 

handlers.article.go파일

// handlers.article.go

package main

import (
  "net/http"

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

func showIndexPage(c *gin.Context) {
  articles := getAllArticles() // 기존에 정의한 getAllArticles 함수를 이용하여 article list 가져오기

  // Call the HTML method of the Context to render a template
  c.HTML(
    // Set the HTTP status to 200 (OK)
    http.StatusOK,
    // Use the index.html template
    "index.html",
    // Pass the data that the page uses
    gin.H{
      "title":   "Home Page",
      "payload": articles,
    },
  )

}

<실행>

 

 

<앱 구성>

 

 

 

 
 
 
 
 

 

 
 
 
 
 
반응형

댓글