Let's Go: Ch 13-14
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:
- magic of go comments
- 50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs
- Awesome Go