How We Built Testable HTTP API Server

How We Built Testable HTTP API Server

GoCon JP 2017/03/25

36e72b299b441378e41b6c445296b959?s=128

Akira Chiku

March 25, 2017
Tweet

Transcript

  1.  )PX8F#VJMU (P5FTUBCMF )551"1*4FSWFS (P$PO+1.BS  "LJSB$IJLV

  2. NF  /BNF"LJSB$IJLV 5XJUUFS!@BDIJLV (JU)VC!BDIJLV (PPHMF4FBSDI"LJSB$IJLV'JSF

  3. BHFOEB  Ø  CBDLHSPVOE QSPEVDUTJ[FUFBN  Ø  QSPKFDUCBTJDTTUSVDUVSF Ø  UFTUTXJUINPEFMQBDLBHF

    UFTUJOHXJUI3%#.4  Ø  UFTUTXJUITFSWJDFQBDLBHF UFTUJOHXJUIFYUFSOBM"1*  Ø  UFTUTXJUIIUUQIBOEMFST Ø  MPDBMJOUFHSBUJPOUFTUTXJUIFYUFSOBMTFSWJDFT BOE XPSLFST
  4.  CBDLHSPVOE QSPEVDUTJ[FUFBN 

  5. CBDLHSPVOE QSPEVDU   Ø  QSPEVDU Ø  "QQCBTFEJOTUBOU BOESFMPBEBCMFWJSUVBMQMBTUJD 7*4"QSFQBJEDBSEGPSFWFSZPOF

    Ø  J04 "OESPJE Ø  IUUQTWBOEMFKQ
  6. CBDLHSPVOE TJ[F   Ø  TJ[F BTPG.BS   Ø 

    PGFOEQPJOUT JOUFSOBM"1*  Ø  PGFYUFSOBMTFSWJDFT Ø  OPUJODMVEJOH4FOUSZ/FX3FMJD4UBUVT1BHFJP  Ø  PGUBCMFT 1PTUHSF42-  Ø  PGMJOFT (PDPEFXJUIPVUWFOEPS  Ø  XJUIHFOFSBUFEDPEF  Ø  XJUIPVUHFOFSBUFEDPEF 
  7. CBDLHSPVOE UFBN   NFNCFS EFTJHO GSPOUFOE CBDLFOE *OGSB "84

     JEFZVUB NPRBEB ZZPTIJLJ BDIJLV "TPG'FC 
  8.  QSPKFDUCBTJDTTUSVDUVSF

  9. QSPKFDUCBTJDT  Ø  EFQFOEFODZNBOBHFNFOU Ø  IUUQTHJUIVCDPNNBUUOHPN Ø  VSMSPVUJOH Ø  IUUQTHJUIVCDPNHPSJMMBNVY

    Ø  +40/)ZQFS4DIFNB Ø  IUUQTHJUIVCDPNJOUFSBHFOUQSNE Ø  EBUBCBTFTDIFNBNBOBHFNFOUTDIFNBNJHSBUJPO Ø  IUUQTHJUIVCDPN[[[FFLTRMBMDIFNZ Ø  IUUQTHJUIVCDPN[[[FFLBMFNCJD Ø  UBCMFSPXEBUBHBUFXBZTUSVDUTGVODUJPOTHFOFSBUJPOVTJOH 1PTUHSF42-UBCMF Ø  IUUQTHJUIVCDPNBDIJLVEHX
  10. QSPKFDUTUSVDUVSF  WBOEMFBQJ NPEFM  JOUFSBDUJPOXJUISECNT  EBUBNPEFMJOUIFTFSWJDF  OPUKVTUSECNTUBCMFSFQSFTFOUBUJPOT

    TFSWJDF  EBUBNPEFMTJOUFSBDUXJUIFBDIPUIFS  BMTP FYUFSOBM"1*TBSFDBMMFEJOTFSWJDFT JBQJ  SFRVFTUSFTQPOTFTUSVDUT  SFRVFTUWBMJEBUJPOGVODUJPOT  IBOEMFST KPCT TFSWFS DPOG FUD  FYTSWDMJFOU FYTSWDMJFOU DNE  WBOEMFTFSWFS  WBOEMFXPSLFS  WBOEMFDMJ  FYTSWNPDLTFSWFS  FYTSWNPDLTFSWFS   ANPEFMAQLHGPS3%#.4 SFMBUFEJOUFSBDUJPOT ATFSWJDFAQLHGPSNPEFMT BOEFYU"1*JOUFSBDUJPOT AWBOEMFAQLHGPSIUUQ IBOEMFSTIUUQTFSWFS XPSLFSTBTZODKPCT 6TFADNEAUPDSFBUF EJFSFOUUZQFTPGCJOBSJFT
  11. QSPKFDUTUSVDUVSF  WBOEMFTFSWFS WBOEMFXPSLFS WBOEMFDMJ WBOEMF TFSWJDF NPEFM TFSWJDF TFSWJDF

    &YUFSOBM "1* DMJFOU
  12.  UFTUTXJUINPEFMQBDLBHF

  13. QSPKFDUTUSVDUVSF  WBOEMFTFSWFS WBOEMFXPSLFS WBOEMFDMJ WBOEMF TFSWJDF NPEFM TFSWJDF TFSWJDF

    &YUFSOBM "1* DMJFOU
  14. 42-"MDIFNZ "MFNCJD EHX  Ø  42-"MDIFNZUPEFOF1PTUHSF42-UBCMFTJOEFYFTDPOTUSBJOUT Ø  "MFNCJDUPHFOFSBUFNJHSBUJPOTDSJQUT Ø  *UBMTPNBOBHFTVQHSBEFEPXOHSBEFEBUBCBTFPCKFDUT

    Ø  EHX HJUIVCDPNBDIJLVEHX  Ø  *UHFOFSBUFT(PTUSVDUT BOETJNQMF5BCMF3PX%BUB(BUFXBZ GVODUJPOTGSPN1PTUHSF42-UBCMFNFUBEBUB Ø  8IZ  Ø  #BDLFOEUFBN NPRBEB BOE* IBT1ZUIPOCBDLHSPVOE BOEVTFE UP42-"MDIFNZ Ø  8FBSFDPNGPSUBCMFXJUI42- Ø  (P03.03.JTIMJCSBSJFTTFFNBCJUTIBLZBUUIBUUJNF .BZ    Ø  4JNJMBSBQQSPBDI Ø  42-#PJMFS HJUIVCDPNWBUUMFTRMCPJMFS  Ø  YP HJUIVCDPNLORYP 
  15. 42-"MDIFNZ "MFNCJD EHX  class Todo(Base): __tablename__ = 'todo' uuid

    = Column(UUID, primary_key=True, server_default=text('uuid_generate_v1mc()')) user_id = Column(UUID, ForeignKey('todo_user.uuid'), nullable=False) name = Column(TEXT, nullable=False) duration = Column(BIGINT, nullable=False) started_at = Column(DateTime(timezone=True), nullable=False) is_completed = Column(Boolean, nullable=False) user = relationship('TodoUser', backref=backref('todos')) IUUQTHJUIVCDPNBDIJLVHPUPEPJUCMPCNBTUFSECTDIFNBNPEFMTUPEPQZ
  16. 42-"MDIFNZ "MFNCJD EHX  // Todo represents gotodoit_api.todo type Todo

    struct { UUID string // uuid UserID string // user_id Name string // name Duration int64 // duration StartedAt time.Time // started_at IsCompleted bool // is_completed } // Create inserts the Todo to the database. func (r *Todo) Create(db Queryer) error { err := db.QueryRow( `INSERT INTO todo (user_id, name, duration, started_at, is_completed) VALUES ($1, $2, $3, $ 4, $5) RETURNING uuid`, &r.UserID, &r.Name, &r.Duration, &r.StartedAt, &r.IsCompleted).Scan(&r.UUID) if err != nil { return errors.Wrap(err, "failed to insert todo") } return nil } // GetTodoByPk select the Todo from the database. func GetTodoByPk(db Queryer, pk0 string) (*Todo, error) { var r Todo err := db.QueryRow( `SELECT uuid, user_id, name, duration, started_at, is_completed FROM todo WHERE uuid = $1`, pk0).Scan(&r.UUID, &r.UserID, &r.Name, &r.Duration, &r.StartedAt, &r.IsCompleted) if err != nil { return nil, errors.Wrap(err, "failed to select todo") } return &r, nil } IUUQTHJUIVCDPNBDIJLVHPUPEPJUCMPCNBTUFSNPEFMUBCMFHP
  17. TFUVQUFBSEPXO  Ø  %FOF5FTU$SFBUF4DIFNB 5FTU%SPQ4DIFNB BOE 5FTU$SFBUF5BCMFTJONPEFMUFTUJOHHP Ø  .BLFUIFTFGVODUJPOT1VCMJD5FTU"1* Ø 

    IUUQTTQFBLFSEFDLDPNNJUDIFMMIBEWBODFEUFTUJOH XJUIHP TMJEF Ø  8FDBOVTFUIFTFGVODUJPOTGSPNPUIFSQBDLBHFTXIJDIBSF EFQFOEVQPOEBUBCBTFUPTFUVQUFBSEPXO Ø  6TF5FTU.BJO N UFTUJOH. JONBJO@UFTUHP Ø  $SFBUFUFTUTDIFNB DSFBUFEBUBCBTFPCKFDUT SVOUFTUT BOE ESPQUFTUTDIFNBXJUIDBTDBEFPQUJPO Ø  8FVTFˎBMFNCJDVQHSBEFIFBETRMˏDPNNBOEUPDSFBUFBMM VQUPEBUFEBUBCBTFPCKFDUT
  18. TFUVQUFBSEPXO  func TestMain(m *testing.M) { flag.Parse() … testSchema :=

    "gotodoit_api_test" testUser := "gotodoit_api_test" TestDropSchema(dbSetupCfg, testSchema) if err := TestCreateSchema(dbSetupCfg, testSchema, testUser); err != nil { log.Println(err) os.Exit(1) } if err := TestCreateTables(tblSetupCfg, ".."); err != nil { log.Println(err) os.Exit(1) } code := m.Run() if err := TestDropSchema(dbSetupCfg, testSchema); err != nil { log.Println(err) os.Exit(1) } os.Exit(code) } IUUQTHJUIVCDPNBDIJLVHPUPEPJUCMPCNBTUFSNPEFMNBJO@UFTUHP
  19. TJOHMFUSBOTBDUJPO42-ESJWFS  Ø  (PPEUFTUTBSFSFQFBUBCMF BOEJOEFQFOEFOUPGFBDIPUIFSFWFO UIFZIBWFUPJOUFSBDUXJUIEBUBCBTF Ø  /PUTVDJFOUGPSHPPEUFTUT CVUOFDFTTBSZ Ø 

    4JOHMF5SBOTBDUJPO XIBU  Ø  8IFOUIFDPOOFDUJPOJTPQFOFE JUTUBSUTBUSBOTBDUJPOBOE BMMPQFSBUJPOTQFSGPSNFEPOUIJTTRM%#XJMMCFXJUIJOUIBU USBOTBDUJPO Ø  IUUQTHJUIVCDPN%"5"%0(HPUYEC GPS.Z42-  Ø  IUUQTHJUIVCDPNBDIJLVQHUYEC GPS1PTUHSF42-  Ø  8FDBOUFTUSPMMCBDLJOUFTUUBSHFUGVODUJPOVTJOHQHUYEC TJODFJUˏTVTJOHTBWFQPJOUGFBUVSFGPSDPOO#FHJO BOE SPMMCBDLUPTBWFQPJOUXIFOUY3PMMCBDL JTDBMMFE
  20. TJOHMFUSBOTBDUJPO42-ESJWFS  // TestSetupTx create tx and cleanup func for

    test func TestSetupTx(t *testing.T) (Txer, func()) { db, err := sql.Open("txdb", "dummy") if err != nil { t.Fatal(err) } tx, err := db.Begin() if err != nil { t.Fatal(err) } cleanup := func() { tx.Rollback() db.Close() } return tx, cleanup } IUUQTHJUIVCDPNBDIJLVHPUPEPJUCMPCNBTUFSNPEFMUFTUJOHHP func init() { txdb.Register("txdb", "postgres", "postgres://gotodoit_api_test@localhost:5432/gotodoit?sslmode =disable") }
  21. TJOHMFUSBOTBDUJPO42-ESJWFS  IUUQTHJUIVCDPNBDIJLVHPUPEPJUCMPCNBTUFSNPEFMVTFS@UFTUHP func TestUser_GetUserByAccessToken(t *testing.T) { tx, clean :=

    TestSetupTx(t) defer clean() u := TestCreateUserData(t, tx, &TodoUser{}) at := TestCreateAccessTokenData(t, tx, u) u, found, err := GetUserByAccessToken(tx, at.Token) if err != nil { t.Fatal(err) } if !found { t.Error("must be found") } t.Logf("%v", u) }
  22. QSFQBSJOHUFTUEBUB  Ø  8IFODSFBUJOHUFTUEBUB Ø  )BTUPIBWFNFBOJOHGVMEFGBVMUWBMVFT BOECFFYQMJDJU XIFOVTJOHOPOEFGBVMUWBMVFT Ø  8FNFSHFUXP(PTUSVDUTUPDSFBUFUFTUEBUB

    Ø  TUSVDUTSFQSFTFOUJOH3%#.4UBCMFTBSFHFOFSBUFECZEXH BMMTUSVDUTIBWFUCM$SFBUF UY NFUIPEUPJOTFSUSFDPSEUP UBCMF  Ø  0OFTUSVDUIBTBMMUIFEFGBVMUWBMVFT BOEUIFPUIFSIBTUFTU TQFDJDWBMVFT5IFMBUFSPWFSXSJUFTUIFGPSNFS Ø  IUUQTHJUIVCDPNBDIJLVPWX
  23. QSFQBSJOHUFTUEBUB  IUUQTHJUIVCDPNBDIJLVHPUPEPJUCMPCNBTUFSNPEFMUFTUJOH@UPEPHP // TestCreateUserData create user test data func

    TestCreateUserData(t *testing.T, tx Queryer, u *TodoUser) *TodoUser { uDefault := TodoUser{ Email: fmt.Sprintf( "akira.chiku.%s@gmail.com", hseq.Get("todo_user.email")), Username: fmt.Sprintf( "user%s", hseq.Get("todo_user.username")), Status: "active", } var target TodoUser if err := ovw.MergeOverwrite(uDefault, u, &target); err != nil { t.Fatal(err) } if err := target.Create(tx); err != nil { t.Fatal(err) } return &target }
  24.  IUUQTHJUIVCDPNBDIJLVHPUPEPJUCMPCNBTUFSNPEFMUPEP@UFTUHP func TestTodo_GetTodosByUserID(t *testing.T) { tx, clean := TestSetupTx(t)

    defer clean() u := TestCreateUserData(t, tx, &TodoUser{}) notCompleted := TestCreateTodoData(t, tx, u, &Todo{ Name: "buy milk", IsCompleted: false, }) completed := TestCreateTodoData(t, tx, u, &Todo{ Name: "done todo", IsCompleted: true, }) QSFQBSJOHUFTUEBUB
  25.  UFTUTXJUITFSWJDFQBDLBHF

  26. QSPKFDUTUSVDUVSF  WBOEMFTFSWFS WBOEMFXPSLFS WBOEMFDMJ WBOEMF TFSWJDF NPEFM TFSWJDF TFSWJDF

    &YUFSOBM "1* DMJFOU
  27. "1*DMJFOUQBDLBHF  Ø  5IFCFTUBSUJDMFPOIPXUPXSJUF(P"1*DMJFOUQBDLBHF Ø  IUUQEFFFFUDPNXSJUJOHHPBQJDMJFOU Ø  JUˏTJO+BQBOFTF CVU(PPHMFUSBOTMBUPSXJMMEPUIFKPCJ 

    Ø  4ZNNFUSJD5FTUJOH PS(PMEFO'JMF XPSLTSFBMMZOJDFMZXIFO SFTQPOTFJTTUBUJD Ø  IUUQTTQFBLFSEFDLDPNNJUDIFMMIBEWBODFEUFTUJOH XJUIHP TMJEF Ø  IUUQTCMPHHPQIFSBDBEFNZDPNBEWFOUTZNNFUSJD BQJUFTUJOHJOHP Ø  *GSFTQPOTFIBTUPJODMVEFSFRVFTUWBMVFTUPGVMMZUFTU BOEJU BMTPSFRVJSFTUPUFTU)551TUBUVTDPEFIFBEFST *XPVMETVHHFTU UPDSFBUFGVOD XIUUQ3FTQPOTF8SJUFS S IUUQ3FRVFTU JOTJEF UIFUFTUGVODUJPO BOEVTFIUUQUFTUQBDLBHFUPTQJOVQJO NFNPSZUFTUTFSWFSUPUFTUBHBJOTU
  28.  func TestClient_EstimateTimeToCompleteSimple(t *testing.T) { req := &Task{ Name: "CS

    101 week one homework", Difficulty: 2, } f := func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(ETC{ StatusCode: SuccessStatusCode, ConfidenceLevel: 10 * req.Difficulty, Time: 1000, }) return } ts := httptest.NewServer(http.HandlerFunc(f)) defer ts.Close() client := NewClient(TestNewConfig(ts.URL), &http.Client{}, nil) ctx := context.Background() res, err := client.EstimateTimeToComplete(ctx, req) if err != nil { t.Fatal(err) } if res.StatusCode != SuccessStatusCode { t.Errorf("want %d got %d", SuccessStatusCode, res.StatusCode) } } IUUQTHJUIVCDPNBDIJLVHPUPEPJUCMPCNBTUFSFTUDDMJFOU@UFTUHP
  29. "1*DMJFOUQBDLBHF  Ø  *UTFFNTPLBUUIFSTUHMBODF CVU Ø  "1*TFSWJDFVTVBMMZIBTNVMUJQMFFOEQPJOUT BOETPNF QSPDFTTFTOFFEUPVTFUIFNGPSPOFUBTL Ø 

    *UJTOPUGVOUPXSJUFGVOD IUUQ3FTQPOTF8SJUFS  IUUQ3FRVFTU GPSFWFSZUFTU Ø  8FDBOXSJUFEFGBVMUCFIBWJPSTJO"1*DMJFOUQBDLBHF BOENBLF UIFNQVCMJDTPUIBUPUIFSQBDLBHFTEFQFOEPO"1*DMJFOU QBDLBHFDBOVTFUIFNGPSUFTUT
  30. "1*DMJFOUQBDLBHF  IUUQTHJUIVCDPNBDIJLVHPUPEPJUCMPCNBTUFSFTUDDMJFOU@UFTUHP func TestClient_EstimateTimeToCompleteDefault(t *testing.T) { ts := httptest.NewServer(TestNewMux(DefaultHandlerMap))

    defer ts.Close() client := NewClient(TestNewConfig(ts.URL), &http.Client{}, nil) ctx := context.Background() req := &Task{ Name: "CS 101 week one homework", Difficulty: 2, } res, err := client.EstimateTimeToComplete(ctx, req) if err != nil { t.Fatal(err) } if res.StatusCode != SuccessStatusCode { t.Errorf("want %d got %d", SuccessStatusCode, res.StatusCode) } }
  31. "1*DMJFOUQBDLBHF  func estimateHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json")

    w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(ETC{ StatusCode: SuccessStatusCode, ConfidenceLevel: 10, Time: 1000, }) return } // DefaultHandlerMap default url and handler map var DefaultHandlerMap = map[string]http.Handler{ "/api/v1/estimate-time": http.HandlerFunc(estimateHandler), } // TestNewMux creates mux for test/dev server func TestNewMux(hm map[string]http.Handler) *http.ServeMux { mux := http.NewServeMux() for url, handler := range hm { mux.Handle(url, handler) } return mux } IUUQTHJUIVCDPNBDIJLVHPUPEPJUCMPCNBTUFSFTUDUFTUJOHHP
  32. "1*DMJFOUQBDLBHF  Ø  8FIBEUPXSJUFDMJFOUQBDLBHFT TPFYUSBDUFEFTTFODFPGUIF QSFWJPVTQBHFUPBMJCSBSZ Ø  IUUQTHJUIVCDPNBDIJLVUFTUTWS

  33. QSPKFDUTUSVDUVSF  WBOEMFTFSWFS WBOEMFXPSLFS WBOEMFDMJ WBOEMF TFSWJDF NPEFM TFSWJDF TFSWJDF

    &YUFSOBM "1* DMJFOU
  34. VTFNPEFMQVCMJDUFTU"1*  Ø  6TF5FTU$SFBUF4DIFNB 5FTU%SPQ4DIFNB BOE 5FTU$SFBUF5BCMFTJONPEFMUFTUJOHHPUPTFUVQUFBSEPXO TDIFNB Ø  6TF5FTU$SFBUF5PEP%BUBJONPEFMUFTUJOH@UPEPHPUPDSFBUF

    UFTUEBUB
  35.  VTFDMJFOUQLHQVCMJDUFTU"1* Ø  6TF5FTU/FX.VYBOE%FGBVMU)BOEMFSEFOFEJO"1*DMJFOU QBDLBHFXJUIIUUQUFTU/FX4FSWFSUPDSFBUFUFTUTFSWFS Ø  0SZPVDBOEFOFZPVSPXOIBOEMFSTJOTJEFTFSWJDFQBDLBHF UFTUT

  36.  UFTUTXJUIIUUQIBOEMFST

  37. QSPKFDUTUSVDUVSF  WBOEMFTFSWFS WBOEMFXPSLFS WBOEMFDMJ WBOEMF TFSWJDF NPEFM TFSWJDF TFSWJDF

    &YUFSOBM "1* DMJFOU
  38. BQQHMPCBMWBMVFT  Ø  $SFBUF"QQTUSVDU BOEMFUJUIBWFBMMBQQMJDBUJPOHMPCBMWBMVFT Ø  %#JOUFSGBDF "QQ$POH "1*$MJFOUT Ø 

    5IJTNBLFTJUFBTZUPTFUVQEVNNZWBMVFT PSNPDLTJODF UIFTFBSFOPUQBDLBHFHMPCBM Ø  #FBXBSFUIBUUIFTFWBMVFTBSFBDDFTTFEDPODVSSFOUMZ Ø  TRM%# BOEIUUQ$MJFOUBSFDPODVSSFOUTBGB Ø  8FDBOBEENFUIPETUP"QQTUSVDUXIJDIIBOEMF)551SFRVFTU SFTQPOTF
  39. BQQHMPCBMWBMVFT  // App application type App struct { DB

    model.DBer Config *Config EstcClient *estc.Client } IUUQTHJUIVCDPNBDIJLVHPUPEPJUCMPCNBTUFSBQQHP // Healthcheck healthcheck func (app *App) Healthcheck( w http.ResponseWriter, r *http.Request) (int, interface{}, error) { m, err := service.Healthcheck(app.DB) if err != nil { c, e := iapi.NewInternalServerError() return c, e, errors.Wrap(err, "service.Healthcheck failed") } res := iapi.HealthcheckSelfResponse{ Message: m, } return http.StatusOK, res, nil } IUUQTHJUIVCDPNBDIJLVHPUPEPJUCMPCNBTUFSIBOEMFSHP
  40. IUUQSFRVFTUSFTQPOTF  Ø  "QQ)BOEMFS4FSWF)551 XIUUQ3FTQPOTF8SJUFS S IUUQ3FRVFTU  Ø  IUUQ)BOEMFSDPNQBUJCMFNFUIPE

    Ø  UIJTJTXSBQQFSBSPVOEBDUVBMQSPDFTT Ø  FSSPSMPHHJOH SFTQPOTFTUSVDUUPKTPOFODPEF )551SFMBUFE UBTL FHSFEJSFDU JGOFDFTTBSZ Ø  "QQ)FBMUIDIFDL XIUUQ3FTQPOTF8SJUFS S IUUQ3FRVFTU  JOU  JOUFSGBDF\^ FSSPS  Ø  5IJTJTOPUIUUQ)BOEMFSDPNQBUJCMFJUTFMG Ø  IPXZPVQSPDFTTSFRVFTU BOEDSFBUFSFTQPOTF Ø  UIJTSFUVSOWBMVFTNBLFTJUFBTZUPBTTFSUJOUFTUT
  41. IUUQSFRVFTUSFTQPOTF  // AppHandler internal type AppHandler struct { h

    func(w http.ResponseWriter, r *http.Request) (int, interface{}, error) } // ServeHTTP serve func (ah AppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { logger := xlog.FromRequest(r) encoder := json.NewEncoder(w) reqInfo := xlog.F{"http_request": r} statusCode, res, err := ah.h(w, r) if err != nil { logger.Error(err, reqInfo) w.WriteHeader(statusCode) encoder.Encode(res) return } w.WriteHeader(statusCode) encoder.Encode(res) return } IUUQTHJUIVCDPNBDIJLVHPUPEPJUCMPCNBTUFSBQQHP
  42. IUUQSFRVFTUSFTQPOTF  // GetUserDetail get user func (app *App) GetUserDetail(

    w http.ResponseWriter, r *http.Request) (int, interface{}, error) { auth := getAuthData(r.Context()) u, err := service.GetUserByID(app.DB, auth.User.UUID) if err != nil { c, e := iapi.NewInternalServerError() return c, e, errors.Wrap(err, "service.GetUserByID failed") } res := iapi.UserSelfResponse{ ID: u.UUID, Username: u.Username, Email: u.Email, } return http.StatusOK, res, nil } IUUQTHJUIVCDPNBDIJLVHPUPEPJUCMPCNBTUFSBQQHP
  43. IUUQSFRVFTUSFTQPOTF  apiChain := jsonChain.Append( apiAuthMiddleware(app), ) router := mux.NewRouter()

    r := router.PathPrefix("/v1").Subrouter() // user r.Methods("POST").Path("/users").Handler( apiChain.Then(AppHandler{h: app.CreateUser})) r.Methods("POST").Path("/login").Handler( apiChain.Then(AppHandler{h: app.LoginUser})) r.Methods("GET").Path("/users/me").Handler( apiChain.Then(AppHandler{h: app.GetUserDetail})) IUUQTHJUIVCDPNBDIJLVHPUPEPJUCMPCNBTUFSTFSWFSHP
  44. IUUQSFRVFTUSFTQPOTF  Ø  4FUVQ"QQGPSIBOEMFSUFTUT Ø  $SBUF"QQ$POHGPSUFTU Ø  $SFBUFTJOHMFUSBOTBDUJPOCZVTJOHNPEFM 5FTU4FUVQ%# UFTUJOH5

    NPEFM%#FSUPTFUJUUP"QQ%# Ø  4FUNJEEMFXBSFHFOFSBUFEEBUB FHVTFSJE UPDPOUFYU$POUFYU
  45. IUUQSFRVFTUSFTQPOTF  func testSetupApp(t *testing.T) (*App, model.DBer, context.Context, func() )

    { config, err := NewConfig("./conf/test.toml") if err != nil { t.Fatal(err) } db, cleanup := model.TestSetupDB(t) app := &App{ Config: config, DB: db, } ctx := context.Background() return app, db, ctx, func() { cleanup() } } IUUQTHJUIVCDPNBDIJLVHPUPEPJUCMPCNBTUFSNBJO@UFTUHP
  46. IUUQSFRVFTUSFTQPOTF  IUUQTHJUIVCDPNBDIJLVHPUPEPJUCMPCNBTUFSIBOEMFS@UFTUHP func TestGetTodos(t *testing.T) { app, tx, ctx,

    cleanup := testSetupApp(t) defer cleanup() ... ts := httptest.NewServer(estc.TestNewMux(estc.DefaultHandlerMap)) defer ts.Close() app.EstcClient = estc.NewClient( estc.TestNewConfig(ts.URL), &http.Client{}, nil) req := testCreateRequest(t, "GET", "/v1/todos", nil).WithContext( context.WithValue( ctx, ctxKeyAuth, AuthModel{Token: "dummy", User: u}), ) wr := httptest.NewRecorder() status, res, err := app.GetTodos(wr, req) if err != nil { t.Fatal(err) }
  47.  MPDBMJOUFHSBUJPOUFTUT FYQFSJNFOUBM 

  48. QSPKFDUTUSVDUVSF  WBOEMFTFSWFS WBOEMFXPSLFS WBOEMFDMJ WBOEMF TFSWJDF NPEFM TFSWJDF TFSWJDF

    &YUFSOBM "1* DMJFOU
  49. NPDL"1*TFSWFS  Ø  5PBDUVBMMZSFRVFTU BOESFDFJWFSFTQPOTFGSPNHPUPEPJUTFSWFS  XFBMTPIBWFUPIBWFNPDL"1*TFSWFS FTUDTFSWFS&TUJNBUF 5JNFUP$PNQMFUF"1*TFSWJDF SVOOJOHUPQSPTFTSFRVFTUT

    Ø  4JODFXFEFOFE.VYJO"1*DMJFOUQBDLBHF XFDBOFBTJMZTUBSU )551TFSWFSJOTJEFDNEFTUDTFSWFSNBJOHP
  50. NPDL"1*TFSWFS  func main() { port := flag.String("p", "", "port

    number") flag.Parse() if *port != "" { s := estc.TestNewServer(estc.DefaultHandlerMap, *port) if err := s.ListenAndServe(); err != nil { log.Fatal(err) } } else { log.Fatal("service port number is not specified") } } IUUQTHJUIVCDPNBDIJLVHPUPEPJUCMPCNBTUFSDNEFTUDTFSWFSNBJOHP
  51. NPDL"1*TFSWFS  func estimateHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json")

    w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(ETC{ StatusCode: SuccessStatusCode, ConfidenceLevel: 10, Time: 1000, }) return } // DefaultHandlerMap default url and handler map var DefaultHandlerMap = map[string]http.Handler{ "/api/v1/estimate-time": http.HandlerFunc(estimateHandler), } // TestNewMux creates mux for test/dev server func TestNewMux(hm map[string]http.Handler) *http.ServeMux { mux := http.NewServeMux() for url, handler := range hm { mux.Handle(url, handler) } return mux } IUUQTHJUIVCDPNBDIJLVHPUPEPJUCMPCNBTUFSFTUDUFTUJOHHP
  52. UNVYQ XCT  Ø  4UBSUHPUPEPTFSWFS BOEFTUDTFSWFSJOPOFUNVYTFTTJPOVTJOH UNVYQ Ø  IUUQTHJUIVCDPNUPOZUNVYQ Ø 

    IUUQTHJUIVCDPNBDIJLVXCT Ø  "VUPSFMPBETFSWFSTPODPEFNPEJDBUJPOVTJOHXCT Ø  "GUFSTUBUJOHCPUITFSWFST XFDBOKVTUDVSMUPRVJDLDIFDLJGJUˏT XPSLJOH
  53. UNVYQ XCT  session_name: gotodoit-dev-pane windows: - window_name: gotodoit-dev-services layout:

    tiled shell_command_before: - cd $GOPATH/src/github.com/achiku/gotodoit panes: - cd ./cmd/gotodo-server && gom exec wbs -c wbs.toml - cd ./cmd/estc-server && gom exec wbs -c wbs.toml IUUQTHJUIVCDPNBDIJLVHPUPEPJUCMPCNBTUFSDNEUVNYTFTTJPOZNM
  54. QZUFTU SFRVFTUT  Ø  *UˏTEJDVMUUPTIBSFBMMDVSMDPNNBOETGPSFBDIFOEQPJOU BOE UFTUDBTF Ø  3BUIFSXSJUJOHTIFMMTDSJQUXJUICVODIPGDVSMDPNNBOET XF

    BSFVTJOHQZUFTU SFRVFTUT XFMPWF1ZUIPO BOENBJOUBJOJOH UIPTFTDSJQUTJOUIFSFQP
  55. QZUFTU SFRVFTUT  # -*- coding: utf-8 -*- import json

    import requests def test_todos(base_header, host): res = requests.get( host+'/v1/todos', headers=base_header) j = res.json() print(json.dumps(j, indent=2)) assert res.status_code == 200 IUUQTHJUIVCDPNBDIJLVHPUPEPJUCMPCNBTUFSECTDIFNBUFTUTUFTU@UPEPQZ
  56.  3FGFSFODFT Ø  "EESFGFSFODFTMBUFS˘