# 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'")
}
}
```