메뉴

문서정보

목차

MSA환경에서의 Go 웹 애플리케이션 테스트 경험

소개

개발 환경

MSA

cut -f 3 -d, list.txt | awk '{print $1}' | myscript.sh | sort | uniq | gnuplot

왜 ?

좋은 MSA 모델을 만들어보자.

API로 커뮤니케이션 하자

swagger

RESTful Web Service를 위한 디자인, 빌드, 문서화를 도와주는 툴
swagger: '2.0'
info:
  version: 0.1.0
  title: Simple To Do List API
securityDefinitions:
  key:
    type: apiKey
    in: header
    name: x-todolist-token
security:
  - key: []
consumes:
  - application/io.swagger.examples.todo-list.v1+json
produces:
  - application/io.swagger.examples.todo-list.v1+json
schemes:
  - http
  - https
x-schemes:
  - unix
paths:
  /:
    get:
      tags: ["todos"]
      operationId: find
      parameters:
        - name: limit
          in: formData
          type: integer
          format: int32
          required: true
          allowEmptyValue: true
        - name: "X-Rate-Limit"
          in: header
          type: integer
          format: int32
          required: true
        - name: tags
          in: formData
          type: array
          collectionFormat: multi
          allowEmptyValue: true
          items:
            type: integer
            format: int32
          required: true
      responses:
        '200':
          description: OK
          schema:
            type: array
            items:
              $ref: "#/definitions/item"
        default:
          description: error
          schema:
            $ref: "#/definitions/error"
    post:
      tags: ["todos"]
      operationId: addOne
      parameters:
        - name: body
          in: body
          schema:
            $ref: "#/definitions/item"
      responses:
        '201':
          description: Created
          schema:
            $ref: "#/definitions/item"
        default:
          description: error
          schema:
            $ref: "#/definitions/error"
  /{id}:
    parameters:
      - type: string
        name: id
        in: path
        required: true
    put:
      tags: ["todos"]
      operationId: updateOne
      parameters:
        - name: body
          in: body
          schema:
            $ref: "#/definitions/item"
      responses:
        '200':
          description: OK
          schema:
            $ref: "#/definitions/item"
        default:
          description: error
          schema:
            $ref: "#/definitions/error"
    delete:
      tags: ["todos"]
      operationId: destroyOne
      responses:
        '204':
          description: Deleted
        default:
          description: error
          schema:
            $ref: "#/definitions/error"
definitions:
  item:
    type: object
    required:
      - description
    properties:
      id:
        type: integer
        format: int64
        readOnly: true
      description:
        type: string
        minLength: 1
      completed:
        type: boolean
  error:
    type: object
    required:
      - message
    properties:
      code:
        type: integer
        format: int64
      message:
        type: string

Swagger 문서 관리

문서로 부터 코드 생성

그냥 문서를 분리

REST API 테스트

테스트를 하는 이유

첫번째 안 : http client

두번째 안 : net/http/httptest

 --/---- main.go
     |
     +-- handler --+--- handler_middleware.go
                   |
                   +--- handler_todo.go
                   |
                   +--- handler_user.go
                   |
                   +--- handler_test.go

// handler 초기화
type Handler struct {
   DB      *sql.DB
   Redis   *redis.Client
   Logger  *logging.Logger
   router  *mux.Router     // gorilla.mux
}

func (h *handler) Init() error {
    // DB, Redis, Logger 설정..
}

package handler
import (
    "net/http/httptest"
    "testing"
    "github.com/stretchr/testify/assert"
)

var (
    testServer string
)

func Test_Init(t *testing.T) {
    /*
    handler := Handler{
      Redis:  redisCli,
      DB:     mysqlCli, 
    }
    */
    handler.Init() 
    server := httptest.NewServer(handlers.CombinedLoggingHandler(logfile, http.DefaultServeMux))
    testServer = server.URL  // ex. http://localhost:8193
}

func Test_Todo(t *testing.T) {
   r, b, err := DoGet(testServer+"/todo/ping", Header, Body)
   assert.Nil(t, err)
   assert.Equal(t, http.StatusOK, res.StatusCode)
   json.Unmarshal(b, &response)
   assert.Equal(t, "pong", response.Text)
}

백엔드 연동 테스트

func Test_Init(t *testing.T) {
    db, err := sql.Open("mysql", "root:gkwlak@tcp(semina:3306)/semina")
    if err != nil {
        panic(err)
    }

    _, err = db.Exec("DELETE FROM todo")
    if err != nil {
        panic(err)
    }
    _, err = db.Exec("DELETE FROM user")
    if err != nil {
        panic(err)
    }
    _, err = db.Exec("DELETE FROM job")
    if err != nil {
        panic(err)
    }

    _, err = db.Exec("DELETE FROM schedule")
    if err != nil {
        panic(err)
    }

    testHeader = []map[string]string{
        {
            "X-SERVICE-Name":     "testService-01",
            "X-Service-Base-Version": "1.0",
            "X-Service-Version":      "1.2",
            "X-TOKEN":            "validtoken",
            "Content-Type":       "text/plain",
        },
        {
            "X-SERVICE-Name":     "testService-01",
            "X-Service-Base-Version": "1.0",
            "X-Service-Version":      "1.2",
            "X-TOKEN":            "validtoken",
            "Content-Type":       "text/plain",
        },
        {
            "X-SERVICE-Name":     "testService-01",
            "X-Service-Base-Version": "1.1",
            "X-Service-Version":      "1.2",
            "X-TOKEN":            "invalidtoken",
            "Content-Type":       "application/json",
        },
        {
            "X-SERVICE-Name":     "testService-02",
            "X-Service-Base-Version": "1.1",
            "X-Service-Version":      "1.3",
            "Content-Type":       "application/json
        },
        {
            "X-Service-Base-Version": "1.1",
            "X-Service-Version":      "1.3",
            "Content-Type":       "application/json
        },

외부 API 서버

 외부 API 서버 연동

package myauth
type Handler struct {
}

func (h *Handler) Init() *mux.Router {
    h.router = mux.NewRouter()
    h.router.HandleFunc("/oauth2.0/token", h.TokenValidation).Methods("GET")
    h.router.HandleFunc("/user/agreement", h.TermsGet).Methods("GET")
    h.router.HandleFunc("/user/agreement", h.TermsUpdate).Methods("PUT")
    h.router.HandleFunc("/user/agreement", h.TermsDelete).Methods("DELETE")
    h.router.HandleFunc("/ping", h.Ping).Methods("GET")
    return h.router
}


func (h *Handler) TokenValidation(w http.ResponseWriter, r *http.Request) {
    token := r.Header.Get("Authorization")
    if len(token) < 10 {
        SendMessage(w, http.StatusNonAuthoritativeInfo, "FAIL")
        return
    }
    switch token {
    case "Bearer validToken-user01":
        profile := Profile{
            ResponseEmail:         "yundream@gmail.com",
            ResponseNickname:      "yundream",
            ResponseName:          "sangbae.yun",
            ResponseGender:        "M",
            ResponseAge:           "40",
            ResponseProfile_image: "http://www.example.com/test.png",
            ResponseId:            "903890",
            ResponseBirthday:      "19740208",
        }
        response := struct {
            ResultCode string  `json:"resultcode"`
            Message    string  `json:"message"`
            Response   Profile `json:"response"`
        }{"00", "success", profile}
        SendMessage(w, http.StatusOK, response)
    case "Bearer validToken-user02":
        profile := Profile{
            ResponseEmail:         "red3018@gmail.com",
            ResponseNickname:      "red3018",
            ResponseName:          "BoBs",
            ResponseGender:        "M",
            ResponseAge:           "42",
            ResponseProfile_image: "http://www.example.com/test.png",
            ResponseId:            "18501287",
            ResponseBirthday:      "19800512",
        }
        response := struct {
            ResultCode string  `json:"resultcode"`
            Message    string  `json:"message"`
            Response   Profile `json:"response"`
        }{"00", "success", profile}
        SendMessage(w, http.StatusOK, response)
    case "Bearer invalidToken":
        }
        response := struct {
            ResultCode string  `json:"resultcode"`
            Message    string  `json:"message"`
            Response   Profile `json:"response"`
        }{"80", "fail", profile}
        SendMessage(w, http.StatusOK, response)
    default:
        SendMessage(w, http.StatusNonAuthoritativeInfo, "FAIL")
    }
}

 --/---- main.go
     |
     +-- handler --+--- handler_middleware.go
     |             |
     |             +--- handler_todo.go
     |             |
     |             +--- handler_user.go
     |             |
     |             +--- handler_test.go
     |
     +-- myauth --+--- myauth.go

packate handler
import "myauth"

authHandler := myauth.Handler{}  
r := authHandler.Init()
authServer := httptest.NewServer(handlers.CombinedLoggingHandler(logfile, r))

마무리