OnlineNoteZ part 4— Router, handlers and testing them

Adam Toth
8 min readMar 19, 2023
Source: google images

In part 3 we’ve discovered how the service layer is working and how we test it. Now we are going to talk about the router (Chi — in part 1 I’m justifying my tool choices) and handler functions + their tests.

chiRouter.go

This file contains everything that’s related to our router setup. The RegisterChiMiddlewares function takes care of setting up all the middleware we need — the eagle-eyed reader can notice how I’m not using the httplog library defined in Chi. That’s because I forked it, and modified it to my liking (TL;DR: now it uses the global logger I defined instead of having to create a new logger,customize it, and pass it in by value).

The other functions are pretty self explanatory (notice how I’m using the r.Route() function to actually group all the routes together which require authentication, and do the r.Use(middleware) only once — it’s a great feature of Chi).

The NewChiRouter factory function takes care of instantiating the Chi mux struct with the supplied configuration. Notice how the first argument is s NoteService which is an interface defined in server.go:

....
type NoteService interface {
CreateNote(ctx context.Context, title string, username string, text string) (uuid.UUID, error)
GetAllNotesFromUser(ctx context.Context, username string) ([]db.Note, error)
DeleteNote(ctx context.Context, id uuid.UUID) (uuid.UUID, error)
UpdateNote(ctx context.Context, reqID uuid.UUID, title string, text string, isTextEmpty bool) (uuid.UUID, error)
RegisterUser(ctx context.Context, args *db.RegisterUserParams) (string, error)
GetUser(ctx context.Context, username string) (db.User, error)
}

type Router interface {
Get(pattern string, handlerFn http.HandlerFunc)
Delete(pattern string, handlerFn http.HandlerFunc)
Post(pattern string, handlerFn http.HandlerFunc)
Put(pattern string, handlerFn http.HandlerFunc)
ServeHTTP(w http.ResponseWriter, r *http.Request)
}
.....

You might have wondered in part 3, where do I actually store the abstraction of the service layer? I read a great quote on reddit which became one of my favourite quotes of all time:

“There’s only 1 thing that is more disgusting then a two-week old dead body that’s been pulled out of the sea — and that is a Java developer trying to write Go”

(This quote has nothing against Java developers in general, but if you come over from languages like Java, C#, TS, etc. be prepared for the fact that Go is hugely different from what you are used to. Do NOT try to apply your language’s best practices here — instead learn how idiomaticish Go should be written.)

You might have expected the NoteService interface to be on the top of the note.go file, together with the noteService struct and its methods. Well, this is most of the time a huge antipattern! We don’t want to tell our clients (in this case the server) up front how to do abstraction, it should be left up to them to decide how they want to define their interfaces, and use the abstractions they actually need — not what we THINK they’ll need. (There are cases though even in the standard library where interfaces are defined up front, because there are occurrences where it actually helps the client.). Also one thing to note with all of this: A big interface means weak abstraction!

The same case can be said for returning interfaces from functions we don’t do that. Our functions should rely on abstractions wherever possible in the form of accepting interfaces as function parameters— and they should return concrete types — mostly structs — not interfaces.

Accept interfaces, return structs !

Now after this quick detour, let’s take a look at the handlers.

note.go

user.go

You should notice a few things here. I’m always defining my handlers as functions that return functions that match the http.Handler signature. It’s because this way I can actually pass in parameters to my handlers — if I didn’t do this, and only did e.g func LoginUser(w http.ResponseWriter, r *http.Request) I wouldn’t be able to pass things in as function arguments, I’d have to construct the dependencies inside the function — which is far from ideal (Another approach would be to have the handlers as methods on a server struct for example, that way we could use the server struct’s fields).
I’m also using the good old SetupHandler function (mentioned in part 2) which takes care of context timeout, cancellation and setting up the logger.

I really like using anonymous structs in my handlers (e.g line 125 in note.go), they seem the perfect fit for storing the requests.

As I mentioned earlier, IMO a handler function should

  1. Handle the client request (decode, validate, etc.)
  2. Interact with the service layer
  3. Return the result to the client

That’s pretty much what I’m trying to do in every handler —sometimes a little bit more, depending on what the handler needs to do, like creating an access token.

For validation I’m using go-validator. I have a models folder which contains the http models and the constraints on them (as a side note here — I think it’s a good practice to separate your HTTP models from the DB models, how you handle and model your HTTP requests should have nothing to do with the DB models, and vice versa — not to mention how a change in one layer could screw the other):

package models

import (
"time"

"github.com/google/uuid"
)

type User struct {
Username string `json:"username" validate:"required,min=5,max=30,alphanum"`
Password string `json:"password" validate:"required,min=5"`
Email string `json:"email" validate:"email,required"`
}

type Note struct {
ID uuid.UUID `json:"id"`
Title string `json:"title" validate:"required,min=4"`
User string `json:"user" validate:"required"`
Text string `json:"text"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}

Our test files are quite large, and I’m not going to paste in everything here. We are going to take a look at 1 test from notes_test.go and user_test.go.

note_test.go TestGetAllNotesFromUser function:

func TestGetAllNotesFromUser(t *testing.T) {

const username = "testuser1"

testCases := []struct {
name string
addQuery func(t *testing.T, r *http.Request)
mockSvcCall func(svcmock *mocksvc.MockNoteService)
checkResponse func(t *testing.T, rec *httptest.ResponseRecorder)
}{
{
name: "gettings notes from user OK",

addQuery: func(t *testing.T, r *http.Request) {
q := r.URL.Query()
q.Add("username", username)
r.URL.RawQuery = q.Encode()
},

mockSvcCall: func(mocksvc *mocksvc.MockNoteService) {
mocksvc.EXPECT().GetAllNotesFromUser(gomock.Any(), username).Times(1).Return([]db.Note{}, nil)
},

checkResponse: func(t *testing.T, rec *httptest.ResponseRecorder) {
require.Equal(t, http.StatusOK, rec.Code)
},
},
{
name: "returns bad request - missing url param",

addQuery: func(t *testing.T, r *http.Request) {
},

mockSvcCall: func(mocksvc *mocksvc.MockNoteService) {
},

checkResponse: func(t *testing.T, rec *httptest.ResponseRecorder) {
require.Equal(t, http.StatusBadRequest, rec.Code)
},
},
{
name: "returns internal server error - db error",

addQuery: func(t *testing.T, r *http.Request) {
q := r.URL.Query()
q.Add("username", username)
r.URL.RawQuery = q.Encode()
},

mockSvcCall: func(mocksvc *mocksvc.MockNoteService) {
mocksvc.EXPECT().GetAllNotesFromUser(gomock.Any(), username).Times(1).Return(nil, note.ErrDBInternal)
},

checkResponse: func(t *testing.T, rec *httptest.ResponseRecorder) {
require.Equal(t, http.StatusInternalServerError, rec.Code)
},
},
}

for c := range testCases {
tc := testCases[c]

t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
mocksvc := mocksvc.NewMockNoteService(ctrl)

rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/notes", nil)

tc.addQuery(t, req)
tc.mockSvcCall(mocksvc)

handler := GetAllNotesFromUser(mocksvc)
handler(rec, req)
tc.checkResponse(t, rec)
})
}
}

I think I pretty much explained the structure behind my table driven tests in part 3, so please read that before diving deep into this code. Staying true to the basic principal of testing: we feed the input, and tell the test what we expect from it to happen — not caring about the implementation details.

In the happy case, we add a query to the request struct — like we would do when we send the request through the frontend, where we actually use the user context to fetch the current user. Then, we tell our test that the mocked note service (which again was easy to mock because we have the NoteService interface defined in server.go) should return no errors and a slice of Notes. Then, we see whether the response code comes out as 200 or not.

As you can see in the for loop, I’m using an http test recorder, and a test request. httptest is a really great library that makes is easy to test handler functions and server interactions without having to stand up an HTTP server (or you have the option to create a testserver as well if needed). The NewRecorder function returns an http.ResponseRecorder which implements the http.ResponseWriter interface, so you can pass it in as a response writer to your handler along with the httptest.NewRequest.

After we tell the test that we expect the mocked service call to happen, we call the handler function with the test recorder and test request. I’ve seen a lot of confusion around handler functions, but you should not be confused — this is just a function like any other, that gets invoked in a goroutine by your server when the given HTTP endpoint is being hit.
After all of this, we see if the output of the test matches our expectations. I tested for 2 other edge cases, they are pretty self explanatory.

user_test.go TestRegisterUser function:

I picked this example, because it uses a custom mock matcher. Not going to go into details about the test function structure and what it does — you should be able to understand most of it by now. So why do we need a custom matcher?

When users register to our application, they need to provide their password, which is going to be hashed (along with a salt) and stored like that in the DB (see hashedPw below):

  hashedPw, err := password.Hash(req.Password)
...
uname, err := s.RegisterUser(ctx, &db.RegisterUserParams{
Username: req.Username,
Password: hashedPw,
Email: req.Email,
})
...

This actually means, that no two same password hashes will be found in the DB, even if the passwords are the same! Let’s use our colossal brain again, and think. If we are trying to mock the interaction with the servic e and through that the DB, and we try to register a test user — we still need to hash the password, but how do we check the hash value if it changes every single time the test is run?

That’s the trick — we don’t.

Instead, we should check whether the hash that we supply to the mocked svc call as an argument is an actual hash of the supplied test password (“password1" in this case)! That’s where our custom type comes into play. Since the service function expects the db.RegisterUserParams struct as it’s argument — and we don’t wanna change that — we need to come up with a type which matches the RegisterUserParams struct so that we can supply it to the mock service call as an argument, but has some additional things to it.

type regUserMatcher db.RegisterUserParams

Go’s type aliasing is a really powerful feature — and this example shows. For a custom matcher we need to implement two methods on the new struct — Matches() and String().

func (m *regUserMatcher) Matches(x interface{}) bool {

reflectedValue := reflect.ValueOf(x).Elem()
if m.Username != reflectedValue.FieldByName("Username").String() {
return false
}

if m.Email != reflectedValue.FieldByName("Email").String() {
return false
}

err := password.Validate(reflectedValue.FieldByName("Password").String(), m.Password)
if err != nil {
return false
}

return true
}

func (m *regUserMatcher) String() string {
return fmt.Sprintf("Username: %s, Email: %s", m.Username, m.Email)
}

We use the reflection package here to get the actual value of the type we are matching against — which is db.RegisterUserParams. With this method, we tell our test: if our new struct’s (regUserMatcher) username and email fields are the same as the matched type’s (db.RegisterUserParams) AND the matched type’s password can be hashed into our given hash (which is regUserMatcher’s password field), the two structs are matching.

So now, when we go ahead and call the mock service function:

   mockSvcCall: func(mocksvc *mocksvc.MockNoteService, u *models.User) {
mocksvc.EXPECT().RegisterUser(gomock.Any(), ---->&regUserMatcher{
Username: u.Username,
Password: u.Password,
Email: u.Email,
}).Times(1).Return(u.Username, nil)
},

we can use our custom type instead of db.RegisterUserParams without modifying the function signature of RegisterUser, and make sure that our tests pass — because this way we are not checking against the exact hash value, but whether the password can be of that hash that we supply as an argument.

So this concludes part 4. In part 5, we’ll do a deep dive into the authentication used in OnlineNoteZ, and take a look at the current Makefile and Dockerfiles. Stay tuned!

--

--