Home > database >  How to write an unit test for a handler that invokes a function that interacts with db in Golang usi
How to write an unit test for a handler that invokes a function that interacts with db in Golang usi

Time:09-09

I've been trying to write unit tests for my http handler. The code segment is as below:

func (s *Server) handleCreateTicketOption(w http.ResponseWriter, r *http.Request) {
    var t ticket.Ticket
    body, err := ioutil.ReadAll(r.Body)
    if err != nil {
        http.Error(w, er.ErrInternal.Error(), http.StatusInternalServerError)
        return
    }
    err = json.Unmarshal(body, &t)
    if err != nil {
        http.Error(w, er.ErrInvalidData.Error(), http.StatusBadRequest)
        return
    }

    ticket, err := s.TicketService.CreateTicketOption(r.Context(), t)
    if err != nil {
        http.Error(w, er.ErrInternal.Error(), http.StatusInternalServerError)
        return
    }

    res, err := json.Marshal(ticket)
    if err != nil {
        http.Error(w, er.ErrInternal.Error(), http.StatusInternalServerError)
        return
    }

    log.Printf("%v tickets allocated with name %v\n", t.Allocation, t.Name)
    s.sendResponse(w, res, http.StatusOK)
}

Actual logic that interacts with DB. This code segment is invoked by the handler as you can see in the code above. ticket, err := s.TicketService.CreateTicketOption(r.Context(), t)

func (t *TicketService) CreateTicketOption(ctx context.Context, ticket ticket.Ticket) (*ticket.Ticket, error) {
    tx, err := t.db.dbPool.Begin(ctx)
    if err != nil {
        return nil, er.ErrInternal
    }
    defer tx.Rollback(ctx)

    var id int
    err = tx.QueryRow(ctx, `INSERT INTO ticket (name, description, allocation) VALUES ($1, $2, $3) RETURNING id`, ticket.Name, ticket.Description, ticket.Allocation).Scan(&id)
    if err != nil {
        return nil, er.ErrInternal
    }

    ticket.Id = id

    return &ticket, tx.Commit(ctx)
}

And that is my unit test for the handler.

func TestCreateTicketOptionHandler(t *testing.T) {
    

    caseExpected, _ := json.Marshal(&ticket.Ticket{Id: 1, Name: "baris", Description: "test-desc", Allocation: 10})
    srv := NewServer()
    // expected := [][]byte{
    //  _, _ = json.Marshal(&ticket.Ticket{Id: 1, Name: "baris", Description: "test-desc", Allocation: 20}),
    //  // json.Marshal(&ticket.Ticket{Id: 1, Name: "baris", Description: "test-desc", Allocation: 20})
    // }

    tt := []struct {
        name  string
        entry *ticket.Ticket
        want  []byte
        code  int
    }{
        {
            "valid",
            &ticket.Ticket{Name: "baris", Description: "test-desc", Allocation: 10},
            caseExpected,
            http.StatusOK,
        },
    }

    var buf bytes.Buffer
    for _, tc := range tt {
        t.Run(tc.name, func(t *testing.T) {
            json.NewEncoder(&buf).Encode(tc.entry)
            req, err := http.NewRequest(http.MethodPost, "/ticket_options", &buf)
            log.Println("1")
            if err != nil {
                log.Println("2")
                t.Fatalf("could not create request: %v", err)
            }
            log.Println("3")
            rec := httptest.NewRecorder()

            log.Println("4")
            srv.handleCreateTicketOption(rec, req)
            log.Println("5")
            if rec.Code != tc.code {
                t.Fatalf("got status %d, want %v", rec.Code, tc.code)
            }
            log.Println("6")
            if reflect.DeepEqual(rec.Body.Bytes(), tc.want) {
                log.Println("7")
                t.Fatalf("NAME:%v,  got %v, want %v", tc.name, rec.Body.Bytes(), tc.want)
            }
        })
    }
}

I did research about mocking pgx about most of them were testing the logic part not through the handler. I want to write unit test for both handler and logic itself seperately. However, the unit test I've written for the handler panics as below

github.com/bariis/gowit-case-study/psql.(*TicketService).CreateTicketOption(0xc000061348, {0x1485058, 0xc0000260c0}, {0x0, {0xc000026dd0, 0x5}, {0xc000026dd5, 0x9}, 0xa})
        /Users/barisertas/workspace/gowit-case-study/psql/ticket.go:24  0x125
github.com/bariis/gowit-case-study/http.(*Server).handleCreateTicketOption(0xc000061340, {0x1484bf0, 0xc000153280}, 0xc00018e000)
        /Users/barisertas/workspace/gowit-case-study/http/ticket.go:77  0x10b
github.com/bariis/gowit-case-study/http.TestCreateTicketOptionHandler.func2(0xc000119860)
        /Users/barisertas/workspace/gowit-case-study/http/ticket_test.go:80  0x305

psql/ticket.go:24: tx, err := t.db.dbPool.Begin(ctx)

http/ticket.go:77: ticket, err := s.TicketService.CreateTicketOption(r.Context(), t)

http/ticket_test.go:80: srv.handleCreateTicketOption(rec, req)

How can I mock this type of code?

CodePudding user response:

  1. Create an interface which has the required DB functions
  2. Your DB handler implements this interface. You use the handler in actual execution
  3. Create a mock handler using testify/mock and use this in place of DB handler in test cases

CodePudding user response:

From what I can read, you have the following structure:

type Server struct {
   TicketService ticket.Service
}

type TicketService struct {
    db *sql.Db // ..or similar
}

func (ts *TicketService) CreateTicketOption(...)

The trick to mock this is by ensuring ticket.Service is an interface instead of a struct.

Like this:

type TicketService interface {
  CreateTicketOption(ctx context.Context, ticket ticket.Ticket) (*ticket.Ticket, error) {
}

By doing this, your Server expects a TicketService interface. Then you could do this:

type postgresTicketService struct {
    db *sql.Db 
}

func (pst *postgresTicketService) CreateTicketOption(...)...

Which means that the postgresTicketService satisfies the requirements to be passed as a ticket.Service to the Server.

This also means that you can do this:

type mockTicketService struct {
}

func (mts *mockTicketService) CreateTicketOption(...)...

This way you decouple the Server from the actual implementation, and you could just init the Server with the mockTicketService when testing and postgresTicketService when deploying.

  • Related