# Presentation --- ## 1 ```go= // main.go package main import ( "net/http" "github.com/gin-gonic/gin" ) func main() { router := gin.Default() router.GET("/ping", pingController) _ = router.Run(":8080") } func pingController(c *gin.Context) { c.String(http.StatusOK, "ping") } ``` Run: ``` go build ./presentation curl localhost:8080/ping curl localhost:8080/whatever ``` --- ## 2 Relocate the `pingController` function in anothe file, in another package. Create a folder: `controllers` Create a file inside: `ping_controller.go` and **Export** the function. ```go // controllers/ping_controller.go package controllers import ( "github.com/gin-gonic/gin" "net/http" ) func PingController(c *gin.Context) { c.String(http.StatusOK, "ping") } ``` ```go= // main.go package main import ( "github.com/drpaneas/zen/controllers" "github.com/gin-gonic/gin" ) func main() { router := gin.Default() router.GET("/ping", controllers.PingController) // <-- _ = router.Run(":8080") } ``` --- ## 3 Instead of returning "pong" string, call another function, that supposedely this would be calling an external service. Create folder `services` Create a file: `ping_service.go` and **Export** it. ```go= // controllers/ping_controller.go package controllers import ( "github.com/drpaneas/zen/services" "github.com/gin-gonic/gin" "net/http" ) func PingController(c *gin.Context) { c.String(http.StatusOK, services.PingService()) // <-- } ``` ```go= // services/ping_service.go package services func PingService() string { return "pong" } ``` --- ## 4 Write a test for the **PingController** function. Also using **controllers_test** package to make sure you test the exported API as another person would do. ```go= // controllers/ping_controller_test.go package controllers_test import ( "github.com/drpaneas/zen/controllers" "github.com/gin-gonic/gin" "net/http" "net/http/httptest" "testing" ) func TestPingController(t *testing.T) { // Setup fakeHTTPResponseWriter := httptest.NewRecorder() fakeGinContext, _ := gin.CreateTestContext(fakeHTTPResponseWriter) // Run the test (passing the fake input) controllers.PingController(fakeGinContext) // Scenario 1: // Given the service is healthy, when I hit the endpoint '/ping' // - TC-1: Then I expect 200 code if fakeHTTPResponseWriter.Code != http.StatusOK { t.Error("response code should be 200") } // - TC-2: Then I expect to receive 'pong' as an answer if fakeHTTPResponseWriter.Body.String() != "pong" { t.Error("response string should be 'pong'") } } ``` Run the test: `go test -v ./...` or even with `-cover` Notice the 100% coverage. --- ## 5 Most real-life service return an error as well. Make the **PingService** function to return an `err` as well. ```go= // services/ping_service.go package services import "fmt" func PingService() (string, error) { // <-- return "pong", nil // <-- } ``` This will create a problem with the **PingController** function, as we will need to adjust it, to receive the err and act upon it. Pretty much rewrite again the whole **PingController** function ```go= // controllers/ping_controller.go package controllers import ( "github.com/drpaneas/zen/services" "github.com/gin-gonic/gin" "net/http" ) // <-- REWRITTEN ---> /// func PingController(c *gin.Context) { response, err := services.PingService() if err != nil { c.String(http.StatusInternalServerError, err.Error()) } else { c.String(http.StatusOK, response) } } ``` If you run the tests with cover you will see only 75% now. Hehe. `go test -v ./... -cover` **!!! put a println in the service function!!!** and see also how many times the Connecting to an external database gets printed. You are spamming the 3rd party service. ```go= // services/ping_service.go package services import "fmt" func PingService() (string, error) { // Add this print !!!!!!! fmt.Println("Connecting to an external 3rd party database") return "pong", nil } ``` 1. Spamming the service every time you test (you call the real thing) 2. This returns always "pong" and nil (compiled) Mention that you cannot make the PingService function return something problematic. `while true; do curl localhost:8080/ping; done` --- ## 6 **Start Mocking** **We need to refactor our code first** Convert the PingService func into a method. We need a struct for that. ```go= // services/ping_service.go package services import "fmt" type PingServiceStruct struct{} // <-- func (s PingServiceStruct) PingService() (string, error) { // <-- fmt.Println("Connecting to an external 3rd party database") return "pong", nil } ``` This creates a problem in the `ping_controller.go`. Go fix it: ```go= // controllers/ping_controller.go package controllers import ( "github.com/drpaneas/zen/services" "github.com/gin-gonic/gin" "net/http" ) func PingController(c *gin.Context) { service := services.PingServiceStruct{} // <-- bad practice response, err := service.PingService() // <-- if err != nil { c.String(http.StatusInternalServerError, err.Error()) } else { c.String(http.StatusOK, response) } } ``` Bad practice because everytime someone calls it, it creates a new instance of the same service in the same call, it's a bad thinng. All services should be singelton and stateless. You can see that by writting a benchmarks or running a profiler in Go. --- ## 7 To fix that bad practice, we will introduce a public variable that creates one instance just once. ```go= // services/ping_service.go package services import "fmt" type PingServiceStruct struct{} var PingServiceVar = PingServiceStruct{} // <-- func (s PingServiceStruct) PingService() (string, error) { fmt.Println("Connecting to an external 3rd party database") return "pong", nil } ``` ```go= // controllers/ping_controller.go package controllers import ( "github.com/drpaneas/zen/services" "github.com/gin-gonic/gin" "net/http" ) func PingController(c *gin.Context) { response, err := services.PingServiceVar.PingService() // <-- if err != nil { c.String(http.StatusInternalServerError, err.Error()) } else { c.String(http.StatusOK, response) } } ``` There a lot's of implementation for Singleton Pattern in Go. Async, Sync, Thread Safe, Non-Thread safe, etc Read: https://medium.com/golang-issue/how-singleton-pattern-works-with-golang-2fdd61cd5a7f Still we haven't fixed the issue, we just made our code a little bit better. Still 75%. --- ## 7 **Start mocking!** Create an interface, pass the PingService function and then modify the type of the variable, to be type of the interface. ```go= // services/ping_service.go package services import "fmt" type pingServiceInterface interface { // <-- PingService() (string, error) // <-- } type PingServiceStruct struct{} var PingServiceVar pingServiceInterface = PingServiceStruct{} // <-- func (s PingServiceStruct) PingService() (string, error) { fmt.Println("Connecting to an external 3rd party database") return "pong", nil } ``` So now I can create a fake PingService method :D ```go= // controllers/ping_controller_test.go package controllers_test import ( "fmt" "github.com/drpaneas/zen/controllers" "github.com/gin-gonic/gin" "net/http" "net/http/httptest" "testing" ) type mockPingServiceStruct struct {} // <-- // copy paste the PingService and change only the receiver ;) // and the Println as well func (s mockPingServiceStruct) PingService() (string, error) { // <-- fmt.Println("This is the mocked service") // <--- return "pong", nil } func TestPingController(t *testing.T) { // Prepare the input fakeHTTPResponseWriter := httptest.NewRecorder() fakeGinContext, _ := gin.CreateTestContext(fakeHTTPResponseWriter) // Run the test (passing the fake input) controllers.PingController(fakeGinContext) // Scenario 1: // Given the service is healthy, when I hit the endpoint '/ping' // - TC-1: Then I expect 200 code if fakeHTTPResponseWriter.Code != http.StatusOK { t.Error("response code should be 200") } // - TC-2: Then I expect to receive 'pong' as an answer if fakeHTTPResponseWriter.Body.String() != "pong" { t.Error("response string should be 'pong'") } } ``` Now let's use this function for our test. So in the same file, add only one line before calling PingController: ``services.PingServiceVar = mockPingServiceStruct{}`` ```go= // controllers/ping_controller_test.go package controllers_test import ( "fmt" "github.com/drpaneas/zen/controllers" "github.com/drpaneas/zen/services" "github.com/gin-gonic/gin" "net/http" "net/http/httptest" "testing" ) type mockPingServiceStruct struct{} func (s mockPingServiceStruct) PingService() (string, error) { fmt.Println("This is the mocked service") return "pong", nil } func TestPingController(t *testing.T) { // Prepare the input fakeHTTPResponseWriter := httptest.NewRecorder() fakeGinContext, _ := gin.CreateTestContext(fakeHTTPResponseWriter) // ===== // // !!!! Trick the program (use the mock version of PingController) services.PingServiceVar = mockPingServiceStruct{} // <-- // Run the test (passing the fake input) controllers.PingController(fakeGinContext) // Scenario 1: // Given the service is healthy, when I hit the endpoint '/ping' // - TC-1: Then I expect 200 code if fakeHTTPResponseWriter.Code != http.StatusOK { t.Error("response code should be 200") } // - TC-2: Then I expect to receive 'pong' as an answer if fakeHTTPResponseWriter.Body.String() != "pong" { t.Error("response string should be 'pong'") } } ``` Run the program: * it's calling the real thing Run the test: * It's calling the fake thing So we fixed one the problems, not spamming the real thing. Let's now fix the other, by testing the not so happy path. ## 8 Let's create our tests to get 100%. To do that we need first to create a PingService fake method that returns a problematic situation, such as not ping and not 200 code. To do that, just copy paste the current one and change a few things. Then copy paste the testing function and change the variable, pointing to the new struct (the one with the error): `services.PingServiceVar = mockPingServiceStructWithError{} // <--` Lastly write the new **Scenario 2** test-cases. ```go= // controllers/ping_controller_test.go package controllers_test import ( "fmt" "github.com/drpaneas/zen/controllers" "github.com/drpaneas/zen/services" "github.com/gin-gonic/gin" "net/http" "net/http/httptest" "testing" ) type mockPingServiceStruct struct{} func (s mockPingServiceStruct) PingService() (string, error) { fmt.Println("This is the mocked service") return "pong", nil } // ------------START----------------------- type mockPingServiceStructWithError struct{} // <--- // Copy paste την επάνω και άλλαξε τον reciver και τα returns σε "" και error func (s mockPingServiceStructWithError) PingService() (string, error) { fmt.Println("This is the mocked service") err := fmt.Errorf(http.StatusText(http.StatusInternalServerError)) return "", err } // --------------END----------------------------- // !!!!! Rename to NoError func TestPingControllerNoError(t *testing.T) { // Prepare the input fakeHTTPResponseWriter := httptest.NewRecorder() fakeGinContext, _ := gin.CreateTestContext(fakeHTTPResponseWriter) // !!!! Trick the program (use the mock version of PingController) services.PingServiceVar = mockPingServiceStruct{} // <-- // Run the test (passing the fake input) controllers.PingController(fakeGinContext) // Scenario 1: // Given the service is healthy, when I hit the endpoint '/ping' // - TC-1: Then I expect 200 code if fakeHTTPResponseWriter.Code != http.StatusOK { t.Error("response code should be 200") } // - TC-2: Then I expect to receive 'pong' as an answer if fakeHTTPResponseWriter.Body.String() != "pong" { t.Error("response string should be 'pong'") } } // 1. RenameWithError // 2. Change the variable // 3. Change the tests func TestPingControllerWithError(t *testing.T) { // Prepare the input fakeHTTPResponseWriter := httptest.NewRecorder() fakeGinContext, _ := gin.CreateTestContext(fakeHTTPResponseWriter) // CHANGE THIS // services.PingServiceVar = mockPingServiceStructWithError{} // <-- // Run the test (passing the fake input) controllers.PingController(fakeGinContext) // Scenario 2: // Given the service is not healthy, when I hit the endpoint '/ping' // - TC-1: Then I do not expect 200 code if fakeHTTPResponseWriter.Code == http.StatusOK { t.Error("response code should not be 200") } // - TC-2: Then I expect to not receive 'pong' as an answer if fakeHTTPResponseWriter.Body.String() == "pong" { t.Error("response string should not be 'pong'") } } ``` Run the tests 100% coverage. --- ## 9 Let's make the testing code a little bit better. Instead of of having function per struct, we can have one struct that takes a func. Then we can manipulate that function inside each test. ```go= // controllers/ping_controller_test.go package controllers_test import ( "fmt" "github.com/drpaneas/zen/controllers" "github.com/drpaneas/zen/services" "github.com/gin-gonic/gin" "net/http" "net/http/httptest" "testing" ) type mockPingServiceStruct struct { // <-- FakePingService func() (string, error) // <-- } // <-- func (s mockPingServiceStruct) PingService() (string, error) { fmt.Println("This is the mocked service") return s.FakePingService() // <-- return the function! } // delete the other two (struct and func) func TestPingControllerNoError(t *testing.T) { // Prepare the input fakeHTTPResponseWriter := httptest.NewRecorder() fakeGinContext, _ := gin.CreateTestContext(fakeHTTPResponseWriter) // Create a instance of the struct, called mockPingController mockPingController := mockPingServiceStruct{} // <-- // Implement the function based on the scenario you are testing (expected) mockPingController.FakePingService = func() (string, error) { // <-- return "pong", nil // <-- } // Change the singleton var services.PingServiceVar = mockPingController // <--- // Run the test (passing the fake input) controllers.PingController(fakeGinContext) // Scenario 1: // Given the service is healthy, when I hit the endpoint '/ping' // - TC-1: Then I expect 200 code if fakeHTTPResponseWriter.Code != http.StatusOK { t.Error("response code should be 200") } // - TC-2: Then I expect to receive 'pong' as an answer if fakeHTTPResponseWriter.Body.String() != "pong" { t.Error("response string should be 'pong'") } } func TestPingControllerWithError(t *testing.T) { // Prepare the input fakeHTTPResponseWriter := httptest.NewRecorder() fakeGinContext, _ := gin.CreateTestContext(fakeHTTPResponseWriter) // Same logic here mockPingController := mockPingServiceStruct{} // <-- mockPingController.FakePingService = func() (string, error) { // <-- return "", fmt.Errorf(http.StatusText(http.StatusInternalServerError)) // <-- } // <-- services.PingServiceVar = mockPingController // <-- // Run the test (passing the fake input) controllers.PingController(fakeGinContext) // Scenario 2: // Given the service is not healthy, when I hit the endpoint '/ping' // - TC-1: Then I do not expect 200 code if fakeHTTPResponseWriter.Code == http.StatusOK { t.Error("response code should not be 200") } // - TC-2: Then I expect to not receive 'pong' as an answer if fakeHTTPResponseWriter.Body.String() == "pong" { t.Error("response string should not be 'pong'") } } ``` ## Use mockgen ``` mockgen \ -source=services/ping_service.go \ -destination=mocks/ping_service_mock.go \ -package=$GOPACKAGE ``` and go to that file to see what's inside. Run `go mod tidy` to sync. And modify the code: ```go= // controllers/ping_controller_test.go package controllers_test import ( "fmt" "github.com/drpaneas/zen/controllers" mock_services "github.com/drpaneas/zen/mocks" "github.com/drpaneas/zen/services" "github.com/gin-gonic/gin" "github.com/golang/mock/gomock" "net/http" "net/http/httptest" "testing" ) // === Remove all the structs and functions here ==== func TestPingControllerNoError(t *testing.T) { // Prepare the input fakeHTTPResponseWriter := httptest.NewRecorder() fakeGinContext, _ := gin.CreateTestContext(fakeHTTPResponseWriter) // Prepare GoMock mockCtrl := gomock.NewController(t) // <-- defer mockCtrl.Finish() // <-- // ==> 1. Use GoMock mockPingController := mock_services.NewMockpingServiceInterface(mockCtrl) // <-- // ==> 2. Implement the function based on the scenario you are testing (expected) mockPingController.EXPECT().PingService().Return("pong", nil) // <-- // Change the singleton var services.PingServiceVar = mockPingController // Run the test (passing the fake input) controllers.PingController(fakeGinContext) // Scenario 1: // Given the service is healthy, when I hit the endpoint '/ping' // - TC-1: Then I expect 200 code if fakeHTTPResponseWriter.Code != http.StatusOK { t.Error("response code should be 200") } // - TC-2: Then I expect to receive 'pong' as an answer if fakeHTTPResponseWriter.Body.String() != "pong" { t.Error("response string should be 'pong'") } } func TestPingControllerWithError(t *testing.T) { // Prepare the input fakeHTTPResponseWriter := httptest.NewRecorder() fakeGinContext, _ := gin.CreateTestContext(fakeHTTPResponseWriter) // Prepare GoMock mockCtrl := gomock.NewController(t) // <-- defer mockCtrl.Finish() // <-- // Use GoMock mockPingController := mock_services.NewMockpingServiceInterface(mockCtrl) // <-- // Implement the function based on the scenario you are testing (expected) mockPingController.EXPECT().PingService().Return("", fmt.Errorf(http.StatusText(http.StatusInternalServerError))) // <-- // Change the singleton var services.PingServiceVar = mockPingController // Run the test (passing the fake input) controllers.PingController(fakeGinContext) // Scenario 2: // Given the service is not healthy, when I hit the endpoint '/ping' // - TC-1: Then I do not expect 200 code if fakeHTTPResponseWriter.Code == http.StatusOK { t.Error("response code should not be 200") } // - TC-2: Then I expect to not receive 'pong' as an answer if fakeHTTPResponseWriter.Body.String() == "pong" { t.Error("response string should not be 'pong'") } } ```