Upgrade to Pro — share decks privately, control downloads, hide ads and more …

How We Built Testable HTTP API Server

How We Built Testable HTTP API Server

GoCon JP 2017/03/25

Akira Chiku

March 25, 2017
Tweet

More Decks by Akira Chiku

Other Decks in Programming

Transcript


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

    View full-size slide

  2. NF

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

    View full-size slide

  3. BHFOEB

    Ø  CBDLHSPVOE QSPEVDUTJ[FUFBN

    Ø  QSPKFDUCBTJDTTUSVDUVSF
    Ø  UFTUTXJUINPEFMQBDLBHF UFTUJOHXJUI3%#.4

    Ø  UFTUTXJUITFSWJDFQBDLBHF UFTUJOHXJUIFYUFSOBM"1*

    Ø  UFTUTXJUIIUUQIBOEMFST
    Ø  MPDBMJOUFHSBUJPOUFTUTXJUIFYUFSOBMTFSWJDFT BOE
    XPSLFST

    View full-size slide


  4. CBDLHSPVOE
    QSPEVDUTJ[FUFBN

    View full-size slide

  5. CBDLHSPVOE QSPEVDU


    Ø  QSPEVDU
    Ø  "QQCBTFEJOTUBOU BOESFMPBEBCMFWJSUVBMQMBTUJD
    7*4"QSFQBJEDBSEGPSFWFSZPOF
    Ø  J04 "OESPJE
    Ø  IUUQTWBOEMFKQ

    View full-size slide

  6. CBDLHSPVOE TJ[F


    Ø  TJ[F BTPG.BS

    Ø  PGFOEQPJOUT JOUFSOBM"1*

    Ø  PGFYUFSOBMTFSWJDFT
    Ø  OPUJODMVEJOH4FOUSZ/FX3FMJD4UBUVT1BHFJP

    Ø  PGUBCMFT 1PTUHSF42-

    Ø  PGMJOFT (PDPEFXJUIPVUWFOEPS

    Ø  XJUIHFOFSBUFEDPEF
    Ø  XJUIPVUHFOFSBUFEDPEF

    View full-size slide

  7. CBDLHSPVOE UFBN


    NFNCFS EFTJHO GSPOUFOE CBDLFOE
    *OGSB
    "84

    JEFZVUB
    NPRBEB
    ZZPTIJLJ
    BDIJLV
    "TPG'FC

    View full-size slide


  8. QSPKFDUCBTJDTTUSVDUVSF

    View full-size slide

  9. QSPKFDUCBTJDT

    Ø  EFQFOEFODZNBOBHFNFOU
    Ø  IUUQTHJUIVCDPNNBUUOHPN
    Ø  VSMSPVUJOH
    Ø  IUUQTHJUIVCDPNHPSJMMBNVY
    Ø  +40/)ZQFS4DIFNB
    Ø  IUUQTHJUIVCDPNJOUFSBHFOUQSNE
    Ø  EBUBCBTFTDIFNBNBOBHFNFOUTDIFNBNJHSBUJPO
    Ø  IUUQTHJUIVCDPN[[[FFLTRMBMDIFNZ
    Ø  IUUQTHJUIVCDPN[[[FFLBMFNCJD
    Ø  UBCMFSPXEBUBHBUFXBZTUSVDUTGVODUJPOTHFOFSBUJPOVTJOH
    1PTUHSF42-UBCMF
    Ø  IUUQTHJUIVCDPNBDIJLVEHX

    View full-size slide

  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

    View full-size slide

  11. QSPKFDUTUSVDUVSF

    WBOEMFTFSWFS
    WBOEMFXPSLFS
    WBOEMFDMJ
    WBOEMF TFSWJDF NPEFM
    TFSWJDF
    TFSWJDF
    &YUFSOBM
    "1*
    DMJFOU

    View full-size slide


  12. UFTUTXJUINPEFMQBDLBHF

    View full-size slide

  13. QSPKFDUTUSVDUVSF

    WBOEMFTFSWFS
    WBOEMFXPSLFS
    WBOEMFDMJ
    WBOEMF TFSWJDF NPEFM
    TFSWJDF
    TFSWJDF
    &YUFSOBM
    "1*
    DMJFOU

    View full-size slide

  14. 42-"MDIFNZ"MFNCJDEHX

    Ø  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

    View full-size slide

  15. 42-"MDIFNZ"MFNCJDEHX

    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

    View full-size slide

  16. 42-"MDIFNZ"MFNCJDEHX

    // 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

    View full-size slide

  17. TFUVQUFBSEPXO

    Ø  %FOF5FTU$SFBUF4DIFNB 5FTU%SPQ4DIFNB BOE
    5FTU$SFBUF5BCMFTJONPEFMUFTUJOHHP
    Ø  .BLFUIFTFGVODUJPOT1VCMJD5FTU"1*
    Ø  IUUQTTQFBLFSEFDLDPNNJUDIFMMIBEWBODFEUFTUJOH
    XJUIHP TMJEF
    Ø  8FDBOVTFUIFTFGVODUJPOTGSPNPUIFSQBDLBHFTXIJDIBSF
    EFQFOEVQPOEBUBCBTFUPTFUVQUFBSEPXO
    Ø  6TF5FTU.BJO NUFTUJOH.
    JONBJO@UFTUHP
    Ø  $SFBUFUFTUTDIFNB DSFBUFEBUBCBTFPCKFDUT SVOUFTUT BOE
    ESPQUFTUTDIFNBXJUIDBTDBEFPQUJPO
    Ø  8FVTFˎBMFNCJDVQHSBEFIFBETRMˏDPNNBOEUPDSFBUFBMM
    VQUPEBUFEBUBCBTFPCKFDUT

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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")
    }

    View full-size slide

  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)
    }

    View full-size slide

  22. QSFQBSJOHUFTUEBUB

    Ø  8IFODSFBUJOHUFTUEBUB
    Ø  )BTUPIBWFNFBOJOHGVMEFGBVMUWBMVFT BOECFFYQMJDJU
    XIFOVTJOHOPOEFGBVMUWBMVFT
    Ø  8FNFSHFUXP(PTUSVDUTUPDSFBUFUFTUEBUB
    Ø  TUSVDUTSFQSFTFOUJOH3%#.4UBCMFTBSFHFOFSBUFECZEXH
    BMMTUSVDUTIBWFUCM$SFBUF UY
    NFUIPEUPJOTFSUSFDPSEUP
    UBCMF

    Ø  0OFTUSVDUIBTBMMUIFEFGBVMUWBMVFT BOEUIFPUIFSIBTUFTU
    TQFDJDWBMVFT5IFMBUFSPWFSXSJUFTUIFGPSNFS
    Ø  IUUQTHJUIVCDPNBDIJLVPWX

    View full-size slide

  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.%[email protected]",
    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
    }

    View full-size slide


  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

    View full-size slide


  25. UFTUTXJUITFSWJDFQBDLBHF

    View full-size slide

  26. QSPKFDUTUSVDUVSF

    WBOEMFTFSWFS
    WBOEMFXPSLFS
    WBOEMFDMJ
    WBOEMF TFSWJDF NPEFM
    TFSWJDF
    TFSWJDF
    &YUFSOBM
    "1*
    DMJFOU

    View full-size slide

  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 SIUUQ3FRVFTU
    JOTJEF
    UIFUFTUGVODUJPO BOEVTFIUUQUFTUQBDLBHFUPTQJOVQJO
    NFNPSZUFTUTFSWFSUPUFTUBHBJOTU

    View full-size slide


  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

    View full-size slide

  29. "1*DMJFOUQBDLBHF

    Ø  *UTFFNTPLBUUIFSTUHMBODF CVU
    Ø  "1*TFSWJDFVTVBMMZIBTNVMUJQMFFOEQPJOUT BOETPNF
    QSPDFTTFTOFFEUPVTFUIFNGPSPOFUBTL
    Ø  *UJTOPUGVOUPXSJUFGVOD IUUQ3FTQPOTF8SJUFS
    IUUQ3FRVFTU
    GPSFWFSZUFTU
    Ø  8FDBOXSJUFEFGBVMUCFIBWJPSTJO"1*DMJFOUQBDLBHF BOENBLF
    UIFNQVCMJDTPUIBUPUIFSQBDLBHFTEFQFOEPO"1*DMJFOU
    QBDLBHFDBOVTFUIFNGPSUFTUT

    View full-size slide

  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)
    }
    }

    View full-size slide

  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

    View full-size slide

  32. "1*DMJFOUQBDLBHF

    Ø  8FIBEUPXSJUFDMJFOUQBDLBHFT TPFYUSBDUFEFTTFODFPGUIF
    QSFWJPVTQBHFUPBMJCSBSZ
    Ø  IUUQTHJUIVCDPNBDIJLVUFTUTWS

    View full-size slide

  33. QSPKFDUTUSVDUVSF

    WBOEMFTFSWFS
    WBOEMFXPSLFS
    WBOEMFDMJ
    WBOEMF TFSWJDF NPEFM
    TFSWJDF
    TFSWJDF
    &YUFSOBM
    "1*
    DMJFOU

    View full-size slide

  34. VTFNPEFMQVCMJDUFTU"1*

    Ø  6TF5FTU$SFBUF4DIFNB 5FTU%SPQ4DIFNB BOE
    5FTU$SFBUF5BCMFTJONPEFMUFTUJOHHPUPTFUVQUFBSEPXO
    TDIFNB
    Ø  6TF5FTU$SFBUF5PEP%BUBJONPEFMUFTUJOH@UPEPHPUPDSFBUF
    UFTUEBUB

    View full-size slide


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

    View full-size slide


  36. UFTUTXJUIIUUQIBOEMFST

    View full-size slide

  37. QSPKFDUTUSVDUVSF

    WBOEMFTFSWFS
    WBOEMFXPSLFS
    WBOEMFDMJ
    WBOEMF TFSWJDF NPEFM
    TFSWJDF
    TFSWJDF
    &YUFSOBM
    "1*
    DMJFOU

    View full-size slide

  38. BQQHMPCBMWBMVFT

    Ø  $SFBUF"QQTUSVDU BOEMFUJUIBWFBMMBQQMJDBUJPOHMPCBMWBMVFT
    Ø  %#JOUFSGBDF "QQ$POH "1*$MJFOUT
    Ø  5IJTNBLFTJUFBTZUPTFUVQEVNNZWBMVFT PSNPDLTJODF
    UIFTFBSFOPUQBDLBHFHMPCBM
    Ø  #FBXBSFUIBUUIFTFWBMVFTBSFBDDFTTFEDPODVSSFOUMZ
    Ø  TRM%# BOEIUUQ$MJFOUBSFDPODVSSFOUTBGB
    Ø  8FDBOBEENFUIPETUP"QQTUSVDUXIJDIIBOEMF)551SFRVFTU
    SFTQPOTF

    View full-size slide

  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

    View full-size slide

  40. IUUQSFRVFTUSFTQPOTF

    Ø  "QQ)BOEMFS4FSWF)551 XIUUQ3FTQPOTF8SJUFS S
    IUUQ3FRVFTU

    Ø  IUUQ)BOEMFSDPNQBUJCMFNFUIPE
    Ø  UIJTJTXSBQQFSBSPVOEBDUVBMQSPDFTT
    Ø  FSSPSMPHHJOH SFTQPOTFTUSVDUUPKTPOFODPEF )551SFMBUFE
    UBTL FHSFEJSFDU
    JGOFDFTTBSZ
    Ø  "QQ)FBMUIDIFDL XIUUQ3FTQPOTF8SJUFS SIUUQ3FRVFTU
    JOU
    JOUFSGBDF\^ FSSPS

    Ø  5IJTJTOPUIUUQ)BOEMFSDPNQBUJCMFJUTFMG
    Ø  IPXZPVQSPDFTTSFRVFTU BOEDSFBUFSFTQPOTF
    Ø  UIJTSFUVSOWBMVFTNBLFTJUFBTZUPBTTFSUJOUFTUT

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  44. IUUQSFRVFTUSFTQPOTF

    Ø  4FUVQ"QQGPSIBOEMFSUFTUT
    Ø  $SBUF"QQ$POHGPSUFTU
    Ø  $SFBUFTJOHMFUSBOTBDUJPOCZVTJOHNPEFM
    5FTU4FUVQ%# UFTUJOH5
    NPEFM%#FSUPTFUJUUP"QQ%#
    Ø  4FUNJEEMFXBSFHFOFSBUFEEBUB FHVTFSJE
    UPDPOUFYU$POUFYU

    View full-size slide

  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

    View full-size slide

  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)
    }

    View full-size slide


  47. MPDBMJOUFHSBUJPOUFTUT
    FYQFSJNFOUBM

    View full-size slide

  48. QSPKFDUTUSVDUVSF

    WBOEMFTFSWFS
    WBOEMFXPSLFS
    WBOEMFDMJ
    WBOEMF TFSWJDF NPEFM
    TFSWJDF
    TFSWJDF
    &YUFSOBM
    "1*
    DMJFOU

    View full-size slide

  49. NPDL"1*TFSWFS

    Ø  5PBDUVBMMZSFRVFTU BOESFDFJWFSFTQPOTFGSPNHPUPEPJUTFSWFS
    XFBMTPIBWFUPIBWFNPDL"1*TFSWFS FTUDTFSWFS&TUJNBUF
    5JNFUP$PNQMFUF"1*TFSWJDF
    SVOOJOHUPQSPTFTSFRVFTUT
    Ø  4JODFXFEFOFE.VYJO"1*DMJFOUQBDLBHF XFDBOFBTJMZTUBSU
    )551TFSWFSJOTJEFDNEFTUDTFSWFSNBJOHP

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

  52. UNVYQXCT

    Ø  4UBSUHPUPEPTFSWFS BOEFTUDTFSWFSJOPOFUNVYTFTTJPOVTJOH
    UNVYQ
    Ø  IUUQTHJUIVCDPNUPOZUNVYQ
    Ø  IUUQTHJUIVCDPNBDIJLVXCT
    Ø  "VUPSFMPBETFSWFSTPODPEFNPEJDBUJPOVTJOHXCT
    Ø  "GUFSTUBUJOHCPUITFSWFST XFDBOKVTUDVSMUPRVJDLDIFDLJGJUˏT
    XPSLJOH

    View full-size slide

  53. UNVYQXCT

    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

    View full-size slide

  54. QZUFTUSFRVFTUT

    Ø  *UˏTEJDVMUUPTIBSFBMMDVSMDPNNBOETGPSFBDIFOEQPJOU BOE
    UFTUDBTF
    Ø  3BUIFSXSJUJOHTIFMMTDSJQUXJUICVODIPGDVSMDPNNBOET XF
    BSFVTJOHQZUFTUSFRVFTUT XFMPWF1ZUIPO
    BOENBJOUBJOJOH
    UIPTFTDSJQUTJOUIFSFQP

    View full-size slide

  55. QZUFTUSFRVFTUT

    # -*- 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

    View full-size slide


  56. 3FGFSFODFT
    Ø  "EESFGFSFODFTMBUFS˘

    View full-size slide