TDD or not to TDD? How do I debug? What’s a Go Vet?
Intro
In this post, the objective is to introduce a couple of concepts that makes Go development a bit easier. Some of this can be transferred to other languages as well to improve your development journey overall.
We’re going to look at some of the built-in tools (Go fmt, Go vet) within the language and then some methods to ensure that you are delivering tested, stable code that conform to the requirements set.
Some background on my experience: I have been using Golang for the last 4 years particularly for building out API’s, starting with Go 1.13, although the examples here were compiled with Go 1.19.1, I try my best to follow the K.I.S.S philosophy , not the band, but to Keep It Simple.
1. The built in tools : Go vet and Go fmt.
go fmt is a command to format your go code, it’s one of the first things you learn. It does what it says, it ensures consistent formatting of your code.
go vet is a command detects suspicious code that may not be picked up by the compiler, but it’s limited in what it can pick up. This is a basic list of the things that Go Vet would help with: typos, syntax errors, unassigned/unused variables, unreachable code ( bad loops ) and defers in for loops. (go vet ./… for all packages )
Typically, if you’re using a code editor or and IDE ( like VS Code or Jetbrains Goland), these things are built in and you may never actually need to run these commands manually using the go cli which is installed by default when you install go on your machine.
Given that Goland is a paid product, I can assume that most developers learning go would be using something like Visual Studio Code which has a very useful Golang extension. Built into that is Gopls ( Go please) , which is the official language server for Go. This gives your IDE power to detect build and vet errors found in the workspace. These errors show up in the (view-> problems) section of the editor.
After you have fixed your typos and syntax errors, now it’s time to ensure your application conforms to the expected behaviour. This is where testing and debugging your go application is important. There’s no one way to do this, and it varies based on your workflow.
2. Testing and debugging your Go application.
Debugging is the process of finding and removing issues from your system. Bugs can come in different flavours, but at the end of the day it all comes down to the system not working in the way it was intended. Another way of putting it is, the system has departed from the requirements set out for the behaviour of that system. Typically, most debugging happens through tests (unit and integration) in my current workflow. I know test driven development has it’s pros and it’s cons, but to keep things short, in my opinion it’s better to TDD than to not TDD. If a function is written, it’s tested against what the function should do.
If a production bug is found, it’s important to attempt to replicate that bug so that a test can be written against it. That gives a wonderful sense of security that your patch is valid. ( there are edge cases where you won’t be able to, but I prefer to not use the exceptions as the rule )
Using a cli tool like Delve, you can set break points and debug your code, but this is also built in to VS Code’s debugger. Once you have the Golang extension installed, then you should see an option to debug/run your tests right above the test. Set the breakpoints in either the test code or the application code to step through, step into , step out or continue execution to the next breakpoint.
( I must apologise for switching example codes here, Will explain a bit later on)
The approach that (hopefully) works best is to use an example to outline these concepts. Let’s consider the following scenario, you have a simple API for temporarily storing data. This API contains two endpoints, a GET and a POST. Let’s walk through how one may approach testing and debugging this application.
The code snippet below sets up a Storage Handler, if a POST is sent, it stores the data sent in the POST and if a GET is sent, it tries to retrieve the data. ( a very simplified way of doing this ). The code has intentional issues for the purposes of this article. Please don’t use this in a production environment.
- Requirement 1: Have the ability to store a string of data ( error if none is sent )
- Requirement 2 Have the ability to retrieve a string of data ( error if non-existent )
As a developer, it’s important to test these requirements, as well as the edge cases. Know how the system is supposed to perform. ( naturally, questions to your manager should arise here if the behaviour you’re seeing is unexpected or undocumented )
func StorageHandler(c echo.Context) error {
id := c.Param("id")
switch c.Request().Method {
case http.MethodPost:
reqBody, err := ioutil.ReadAll(c.Request().Body)
if err != nil {
return err
}
_, err = redisClient.Set(c.Request().Context(),id,reqBody,10).Result()
if err != nil{
return err
}
c.Response().WriteHeader(http.StatusCreated)
return nil
case http.MethodGet:
result, err := redisClient.Get(c.Request().Context(),id).Result()
if err != nil {
return err
}
c.Response().Writer.Write([]byte(result))
return nil
}
return nil
}
func Test_StorageHandler(t *testing.T) {
t.Run("POST happy path", func(t *testing.T) {
// Setup
e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/", nil)
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
// Assertions
if assert.NoError(t, StorageHandler(c)) {
assert.Equal(t, http.StatusCreated, rec.Code)
}
})
t.Run("POST empty string should return error", func(t *testing.T) {
// Setup
e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/", nil)
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
StorageHandler(c)
assert.Equal(t,http.StatusBadRequest,c.Response().Status)
})
t.Run("POST redis unable to connect should return error", func(t *testing.T) {
e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/", nil)
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
StorageHandler(c)
assert.Equal(t,http.StatusInternalServerError,c.Response().Status)
})
}
The code fails the expectations, now begins the iterative process of fixing (debugging). I’ll fix the first issue as an example, but the point here isn’t to make this code perfect, but it’s to show a thought-process and flow that should make your life easier as a Golang developer, and a developer on the whole.
So the first issue, the code is not returning the expected 400 response when no data is sent in the request body. Let’s see if we can rectify that,
Hooray! We passed the test by adding validation to the request body.
Before I wrap up, I just wanted to outline some limitations with VS Code’s test runner and debugger and some things I may have skipped through. Currently sub-tests are used in the sample code, and you cannot run sub-tests independently in the UI, so a simple t.Skipnow() can be used to skip the tests that you don’t want run. This would help you in debugging. Test tables can also be used for repetitive input/output test scenarios.
As you may have noticed, Redis is not an issue in testing. That’s where mocking comes in. I’m not going to spend alot of time on mocking ( maybe another post ). Mocking and stubbing are methods of faking a behaviour of a service that’s not within the scope of the code that you’re testing. It’s there to make testing easier and faster. In my case, ory/dockertest was used to configure a test environment using a Redis docker container. There are many options here however that’s more specific to your workflow.
It’s a Wrap
In Summary, This post started off with look at the basic Go Vet and Go fmt commands, then dove deeper into some tips, using an example of the StorageApplication, to show how to debug and test your code against requirements. I do hope this is helpful, if you have any questions, feedback or comments, feel free to leave them below this post.
The Sample Code used has not been published to Github.