Last time we had already completed unit testing of the logic layer, and this time we will come to unit testing of the Kangkang interface layer. The interface layer is mainly responsible for the processing of requests, and the most common is the processing of HTTP requests.
But unit tests for the interface layer can actually be diverse. It is not as common as the logic layer and the data layer, and there are often many ways to go for its testing.
Due to the different HTTP frameworks used, the implementation of unit tests is different. It can be simulated by programs or tested by real HTTP requests, and implemented by using some external testing tools.
Therefore, this article can only give one idea, and the specific implementation method must be implemented according to the actual framework.
environment
This article uses commonly usedgin
As an example, using a method that I prefer and is very simple to implement unit testing. The main features are:
- No need to start the routing service
- Reuse the request structure in an existing project
Code
Since I have posted it before, I will not elaborate on the service layer code here
base case
package controller import ( "context" "/gin-gonic/gin" "go-demo/m/unit-test/entity" ) //go:generate mockgen -source=./ -destination=../mock/user_service_mock.go -package=mock type UserService interface { AddUser(ctx , username string) (err error) GetUser(ctx , userID int) (user *, err error) } type AddUserRequest struct { Username string `json:"username" binding:"required"` } type GetUserRequest struct { UserID int `form:"user_id" binding:"required"` } type GetUserResponse struct { Username string `json:"username"` } type UserController struct { UserService UserService } func NewUserController(userService UserService) *UserController { return &UserController{UserService: userService} } func (uc *UserController) AddUser(ctx *) { req := &AddUserRequest{} if err := (req); err != nil { return } if err := (ctx, ); err != nil { (400, {"error": ()}) return } (200, {"message": "success"}) } func (uc *UserController) GetUser(ctx *) { req := &GetUserRequest{} if err := (req); err != nil { return } user, err := (ctx, ) if err != nil { (400, {"error": ()}) return } (200, &GetUserResponse{Username: }) }
- Since our service unit test has passed before, we need to mock the service layer interface this time.
mockgen -source=./ -destination=../mock/user_service_mock.go -package=mock
- Here I put the request and return structures such as: GetUserRequest and GetUserResponse here just to facilitate the display of the code.
Unit Testing
The basic code is very simple, which is common to us. The most important thing is to see how unit tests should be written.
Tools and methods
Before writing actual unit tests, we need some tooling methods to help us build some requests.
func createGetReqCtx(req interface{}, handlerFunc ) (isSuccess bool, resp string) { w := () c, _ := (w) encode := structToURLValues(req).Encode() , _ = ("GET", "/?"+encode, nil) handlerFunc(c) return == , () } func createPostReqCtx(req interface{}, handlerFunc ) (isSuccess bool, resp string) { responseRecorder := () ctx, _ := (responseRecorder) body, _ := (req) , _ = ("POST", "/", (body)) ("Content-Type", "application/json") handlerFunc(ctx) return == , () } // Convert the structure to URL parametersfunc structToURLValues(s interface{}) { v := (s) if () == { v = () } t := () values := {} for i := 0; i < (); i++ { field := (i) tag := ("form") if tag == "" { continue } value := (i).Interface() (tag, valueToString(value)) } return values } // Since the parameters of get requests are often not particularly complicated, the usual types should be included, and you can continue to add them if necessaryfunc valueToString(v interface{}) string { switch v := v.(type) { case int: return (v) case string: return v default: return "" } }
Since we don't want to start the route, the most critical issue is how to build a to simulate normal requests.
- pass
Create a context we need to simulate
- pass
To create the request structure we need
Unit Testing
With our tool methods, it is very convenient to write unit tests. The mock method is similar to before, and the remaining one needs to call the corresponding method. And here we can reuse the request structure we have used in the original program, such asGetUserRequest
This way, there is no need to work again.
package controller import ( "fmt" "testing" "/golang/mock/gomock" "/stretchr/testify/assert" "go-demo/m/unit-test/entity" "go-demo/m/unit-test/mock" ) func TestUserController_AddUser(t *) { ctl := (t) defer () req := &AddUserRequest{Username: "LinkinStar"} mockUserService := (ctl) ().AddUser((), ()).Return(nil) userController := NewUserController(mockUserService) success, resp := createPostReqCtx(req, ) (t, success) (resp) } func TestUserController_GetUser(t *) { ctl := (t) defer () req := &GetUserRequest{UserID: 1} user := &{Username: "LinkinStar"} mockUserService := (ctl) ().GetUser((), ()).Return(user, nil) userController := NewUserController(mockUserService) success, resp := createGetReqCtx(req, ) (t, success) (resp) }
You can see that the test method is exactly the same. If you have more details, you only need to parse the requested return value and then assert it.
question
Of course, if you implement unit tests in the above way, some problems will be missed. After all, being lazy has a price.
- Problem with routing path: You can see that the corresponding url address is not registered in the above unit test, so in fact, it may cause 404 due to writing errors in the code route.
- Request structure field error: Since we reused the request structure in the original code, even if the word spelling error is wrong, it can still be successful. Because both sides are wrong, it cannot be discovered even if the field name is inconsistent with the interface document.
In response to these two problems, I think it can be guaranteed by more advanced testing. Since this is just unit tests, I think these costs are still acceptable. Moreover, if you use swagger to generate documents, the unity of the document and code can also be guaranteed. But I still have to come up with a reminder here, after all, I have encountered practical problems.
Optimization point
Of course, the examples here are still too simple, and the actual requests are often more complicated.
- In actual scenarios, some requests often require authentication. This can be done by adding middleware to the previous authentication method.
- Other types of requests are similar, such as PUT, DELETE, etc.
- Currently, it is simply dealing with normal 200 HTTP Code. Other exceptions will occur. It also needs to be handled according to the actual interface.
Summarize
Generally speaking, the tests on this layer often find fewer problems, because there is less logic in this layer. The most common problem after testing is often that the field names and restrictions do not meet the needs. So from the perspective of cost-effectiveness, testing this layer alone is often relatively low, so few of them are seen in reality.
But then again, the purpose of this article is not just to let you understand that unit tests can be written in this way. The methods used in them can often allow you to reuse the handler method at some point to ensure system consistency.
This is the end of this article about the interface layer in Golang implementing the unit test. For more related Golang unit test content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!