0

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?

tassador
  • 19
  • 3
  • 1
    Would you mind spinning up a Postgres instance using Docker for testing purposes? I've generated mocks to test db-functions, but its never as reliable as performing actual requests against an actual database. – Mani Sep 08 '22 at 09:19
  • I did create Postgres instance. But how this could help me to solve the issue I've had? I didn't understand. – tassador Sep 08 '22 at 10:56
  • Im sorry, I did not read the question properly! You're wondering how to mock it, right? I can write an example for you. – Mani Sep 09 '22 at 06:15

2 Answers2

0
  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
Suyash Medhavi
  • 1,135
  • 6
  • 18
0

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 TicketService 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.

Mani
  • 1,597
  • 15
  • 19
  • I don't understand, where did you get `ticket.Service`? This post doesn't even have this type. – Jessica Mar 20 '23 at 23:56
  • 1
    This guy suggested you create a `ticket` package that defined a `Service` interface. I think this guy misspelled `Service` with `TicketService`. – Anh Nhat Tran Mar 21 '23 at 08:18
  • @Jessica It was a minor typo. It should've been TicketService. – Mani Mar 22 '23 at 07:40