Daaang Amy
open main menu
Part of series: Learning the Backend

Let's Go: Ch 13-14

/ 7 min read

The last 2 chapters :)

Comment directives

Comment directives are actually compiler directives, more commonly known as pragmas1. In Go, they are comments matching the regular expression //(line |extern |export |[a-z0-9]+:[a-z0-9].

In this project, the Go library embed is used to embed files into your program’s binary.Directives must be placed immediately above the variable in which you want to store the embedded files

You can only use the go:embed directive on global variables at package level, not within functions or methods. If you try to use it within a function or method, you’ll get the error ‘go:embed cannot apply to var inside func’ at compile time.

❓ How useful is embedding our html files into our go program?

❗ The html, css and js files will be baked into our binary when the go server is built. This means that our binary will no longer need to rely on files existing on the directory. As in the binary will no longer assume that we have these html files within the directory to run.

Testing

Testing is an important part of every project. I find this part a bit tedious, but the pay off is always worth it. The hard part is figureing out what you need to test.

Table Driven Tests

What is it?

To create a ‘table’ of tests containing inputs and expected outputs, then loop over each one and run a sub test.

Why?

To reduce the amount of code you have to write when testing the same function with multiple inputs and outputs.

A Useful Helper

We can create a new package called assert to create custom assertions for our tests. Especially for common use cases.

func Equal[T comparable](t * testing.T, actual, expected T) {
  t.Helper()
  if actual != expected {
    t.Errorf("got: %v; want %v", actual, expected)
  }
}

Testing HTTP Handlers

Go proveds a useful http test package called net/http/httptest. We can use this to test our handlers and middleware.

So if we want to test a request…

General pattern:

func TestRequest(t *testing.T) {
  rr := httptest.NewRecorder()

  r, err := http.NewRequest(http.MethodGet, "/", nil)
  if err != nil {
      t.Fatal(err)
  }

  // call handler here.
  ping(rr, r)

  // record result
  rs := rr.Result()
  
  // assertions here
  assert.Equal(t, rs.StatusCode, http.StatusOK)

  defer rs.Body.Close()

  // assertions on the body
  body, err := io.ReadAll(rs.Body)
  if err != nil {
      t.Fatal(err)
  }

  bytes.TrimSpace(body)
  assert.Equal(t, string(body), "OK")
}

t.Fatal() is used here. And in general, it should be called when it doesn’t make sense to continue the current test.

Testing Middleware Pattern

func TestYourMiddleware(t *testing.T) {
	// Initialize a new httptest.ResponseRecorder and dummy http.Request.
	rr := httptest.NewRecorder()
	r, err := http.NewRequest(http.MethodGet, "/", nil)
	if err != nil {
		t.Fatal(err)
	} 

	// Create a mock HTTP handler 
	next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("OK"))
	})

	// Pass the mock HTTP handler to our your middleware. 
	// your middleware probably *returns* a http.Handler we can call its 
		// ServeHTTP() method, passing in the http.ResponseRecorder and dummy 
		// http.Request to execute it.
	yourMiddleware(next).ServeHTTP(rr, r)

	// get the results
	rs := rr.Result()

	// All your middleware assertions go here
		// ...

	defer rs.Body.Close()

	body, err := io.ReadAll(rs.Body)
	if err != nil {
		t.Fatal(err)
	}
	bytes.TrimSpace(body)
	assert.Equal(t, string(body), "OK")
}

Useful Test Commands

To Run All Tests in the Current Project

go test ./...

Run only specific tests or skip tests

Use the -run flag to limit testing to sub-tests using regex. To skip a tests, use the -skip flag instead.

go test -v -run="^TestPing$" ./cmd/web/

Running Tests In Parallel

When you have hundreds and thousands of tests, the total run time can take forever. We mark tests with t.Parallel() to run our tests in concurrently. There’s a caveat though. Tests marked with t.Parallel() will only run in parallel with other tests that have been marked with t.Parallel().

There is a maximum number of tests that can be run simultaneously but you can configure this with the flag -parallel.

go test -parallel=4 ./...

Detecting Race Conditions

You can use the -race flag to detect race conditions in your tests. This does not however ensure that your code is free of race conditions.

Something to look out for

Go will cache tests you run. So if you have not made any changes in the package you are testing, the cached test result will be returned.

To avoid this, use the -count flag.

go test -count=1 ./cmd/web

You can also clear the cache with:

go clean -testcache

A Great Example of Struct Emdedding

The book uses struct embedding to write custom test utils for testing http servers. This makes end-to-end testing of handlers a lot easier and less verbose.

type testServer struct {
	*httptest.Server
}

func newTestServer(t *testing.T, h http.Handler) *testServer {
	ts := httptest.NewTLSServer(h)
	return &testServer{ts}
} 

func (ts *testServer) get(t *testing.T, urlPath string) (int, http.Header, string) {
	rs, err := ts.Client().Get(ts.URL + urlPath)
	if err != nil {
		t.Fatal(err)
	}

	defer rs.Body.Close()

	body, err := io.ReadAll(rs.Body)
	if err != nil {
		t.Fatal(err)
	}

	bytes.TrimSpace(body)
	return rs.StatusCode, rs.Header, string(body)
}

Cool thing about structs and interfaces

When you are writing mocks for your tests, sometimes you will run into an issue where you cannot satisfy a type within a struct because of how it was defined. Structs usually use concrete types but you can use an interface in place of a concrete type instead for extensibility and mocking. What this means is that, as long as the object statisfies the interface, it is considered the same “type” as the type that is required to initialize the struct.

This is a confusing way to explain it lol.

Example

Like in this project (snippetbox, the project used in this book), when you are creating a server, you’ll probably have a struct called application. The application struct will include all the data and functionality that is needed to run a web server.

// Example from the project
type application struct {
	errorLog       *log.Logger
	infoLog        *log.Logger
	snippets       *models.SnippetModel
	users          *models.UserModel
	templateCache  map[string]*template.Template
	formDecoder    *form.Decoder
	sessionManager *scs.SessionManager
}

Let’s say you want to test handlers for the snippets model. You’re going to need to write a mock for it. But the problem is, the mock model will not satisfy the type *models.SnippetModel even IF it does have all the same methods. And there will be an error running the test.

// Snippet Model
type SnippetModel struct {}

func (m *SnippetModel) Insert(title string, content string, expires int) (int, error) {
	// ...code here
}

func (m *SnippetModel) Get(id int) (*Snippet, error) {
	// ...code here
}

func (m *SnippetModel) Latest() ([]*Snippet, error) {
	// ...code here
}

// Mock Snippet Model
type MockSnippetModel struct{}

func (m *MockSnippetModel) Insert(title string, content string, expires int) (int, error) {
	// ...code here
}

func (m *MockSnippetModel) Get(id int) (*models.Snippet, error) {
	// ...code here
}

func (m *MockSnippetModel) Latest() ([]*models.Snippet, error) {
	// ...code here
}

Go will always treat these as different types. This is where the magic happens using interfaces.

Interfaces acts like a blueprint. Any struct that satisfies the interface will be considered as the “same” type.

So if we create SnippetModelInterface and then change the type in the application struct from *models.SnippetModel to models.SnippetModelInterface. Both the SnippetModel and the MockSnippetModel will satisfy the SnippetModelInterface, allowing you to successfully mock the SnippetModel and run the test.

type application struct {
	// ...code here
	snippet  models.SnippetModelInterface
	// ...
}

To test or not to test

One thing I’ve always struggled with was “what do you test?“. This book does not go in depth on testing but what is not included in the book further emphasizes what you don’t need to test for.

The book goes over the basics of mocking, unit testing and E2E testing. Which makes sense. It however does not test small things like database transactions. Because there’s no point. A good database manager would already have it’s own tests and it is redundant to also test it yourself.

Don’t test for every line of code. Only test for what matters. Functionality and behavior.

Conclusion

This was a great book to go through! Some chapters might be theory heavy and it would be worth it to go back to really understand everything. I also recommend going through the additional exercises at the end. They really help to solidify the concepts you learn throughout the project.

I will also be going through Let's Go Further to learn more about APIs.

Useful links:

Relevant Posts

Footnotes

  1. Go’s Hidden Pragmas