From b21f9c511a1e7b0614630253a2622622dbf6c850 Mon Sep 17 00:00:00 2001 From: Oren Rosen Date: Mon, 28 Nov 2022 13:35:50 +0200 Subject: [PATCH 1/9] add todos --- coolfacts/cmd/coolfacts_server/main.go | 7 ++++- coolfacts/cmd/coolfacts_server/server.go | 35 +++++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/coolfacts/cmd/coolfacts_server/main.go b/coolfacts/cmd/coolfacts_server/main.go index 1dade50..cf9ec1a 100644 --- a/coolfacts/cmd/coolfacts_server/main.go +++ b/coolfacts/cmd/coolfacts_server/main.go @@ -4,12 +4,17 @@ import ( "fmt" "log" "net/http" + + "github.com/FTBpro/go-workshop/coolfacts/coolfact" + "github.com/FTBpro/go-workshop/coolfacts/inmem" ) func main() { fmt.Println("Hello, Server!") - server := NewServer() + factsRepo := inmem.NewFactsRepository() + service := coolfact.NewService(factsRepo) + server := NewServer(service) log.Println("starting server on port 9002") log.Println("You can go to http://127.0.0.1:9002/ping") diff --git a/coolfacts/cmd/coolfacts_server/server.go b/coolfacts/cmd/coolfacts_server/server.go index 9f18ebc..140e336 100644 --- a/coolfacts/cmd/coolfacts_server/server.go +++ b/coolfacts/cmd/coolfacts_server/server.go @@ -8,15 +8,24 @@ import ( "strings" ) -type server struct{} +type FactsService interface { + // TODO: add methods declerations + // 1. getFacts - returns a slice of fact.Fact and an error +} + +type server struct { + // TODO: add factsService field +} func NewServer() *server { + // TODO: returns an initializes server with the factsService return &server{} } func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.Println("incoming request", r.Method, r.URL.Path) + // TODO: add case for GET /facts, that will call to `HandleGetFacts` switch r.Method { case http.MethodGet: switch strings.ToLower(r.URL.Path) { @@ -32,6 +41,30 @@ func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } +func (s *server) HandleGetFacts(w http.ResponseWriter) { + log.Println("Handling getFact ...") + + facts, err := s.factsService.GetFacts() + if err != nil { + s.HandleError(w, fmt.Errorf("server.GetFactsHandler: %w", err)) + return + } + + // TODO: + // 1. format the facts to a json response + // 2. write status 200 + // 3. set content type application/json + // 4. write json response: + // { + // "facts": [ + // { + // "id": "..." + // "description": "..." + // }, + // ... + // ] +} + func (s *server) HandlePing(w http.ResponseWriter) { log.Println("Handling Ping ...") From 103c54f8454cbdde4c2b2de959540862c021eb03 Mon Sep 17 00:00:00 2001 From: Oren Rosen Date: Mon, 28 Nov 2022 13:40:29 +0200 Subject: [PATCH 2/9] add folders --- coolfacts/coolfact/fact.go | 5 +++++ coolfacts/coolfact/service.go | 18 ++++++++++++++++++ coolfacts/inmem/factsrepo.go | 17 +++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 coolfacts/coolfact/fact.go create mode 100644 coolfacts/coolfact/service.go create mode 100644 coolfacts/inmem/factsrepo.go diff --git a/coolfacts/coolfact/fact.go b/coolfacts/coolfact/fact.go new file mode 100644 index 0000000..6836f08 --- /dev/null +++ b/coolfacts/coolfact/fact.go @@ -0,0 +1,5 @@ +package coolfact + +type Fact struct { + // TODO: add fields for the entity fact: description and an image (strings), and createdAt (time.Time) +} diff --git a/coolfacts/coolfact/service.go b/coolfacts/coolfact/service.go new file mode 100644 index 0000000..389c309 --- /dev/null +++ b/coolfacts/coolfact/service.go @@ -0,0 +1,18 @@ +package coolfact + +type Repository interface { + // TODO: add functions decleration + // - getFacts. Returns a slice of Fact and an error +} + +type service struct { + // TODO: add field factsRepo +} + +func NewService(factsRepo Repository) *service { + // TODO: init a new service with factsRepo +} + +func (s *service) GetFacts() ([]Fact, error) { + // TODO: implement getFacts, using the factsRepo +} diff --git a/coolfacts/inmem/factsrepo.go b/coolfacts/inmem/factsrepo.go new file mode 100644 index 0000000..f763df4 --- /dev/null +++ b/coolfacts/inmem/factsrepo.go @@ -0,0 +1,17 @@ +package inmem + +import ( + "github.com/FTBpro/go-workshop/coolfacts/coolfact" +) + +type factsRepo struct { + facts []coolfact.Fact +} + +func NewFactsRepository() *factsRepo { + // TODO: init facts repo +} + +func (r *factsRepo) GetFacts() ([]coolfact.Fact, error) { + // TODO: implement +} From 6f0203542debe58af0f3dfdef8655ea6a555e715 Mon Sep 17 00:00:00 2001 From: Oren Rosen Date: Mon, 28 Nov 2022 14:58:02 +0200 Subject: [PATCH 3/9] add readme --- coolfacts/docs/ex2-ping.md | 5 + coolfacts/docs/ex3-get-facts.md | 285 ++++++++++++++++++++++++++++++++ 2 files changed, 290 insertions(+) create mode 100644 coolfacts/docs/ex3-get-facts.md diff --git a/coolfacts/docs/ex2-ping.md b/coolfacts/docs/ex2-ping.md index f74a3f5..096b949 100644 --- a/coolfacts/docs/ex2-ping.md +++ b/coolfacts/docs/ex2-ping.md @@ -172,6 +172,11 @@ func (s *server) HandleError(w http.ResponseWriter, err error) { } ``` +Since we are returning a JSON, we can use the Go `json` package, which can write a JSON encoding of the response to the writer. +The `NewEncoder` method receives `io.Writer`. We already can notice the power of Go interfaces, and especially interfaces with one method. +We can send the `http.ResponseWriter` to different packages methods, and use a different encoding, and the packages are totally agnostic to HTTP. + + # Finish! diff --git a/coolfacts/docs/ex3-get-facts.md b/coolfacts/docs/ex3-get-facts.md new file mode 100644 index 0000000..5c43469 --- /dev/null +++ b/coolfacts/docs/ex3-get-facts.md @@ -0,0 +1,285 @@ +# Part 3 + +In this exercise, you will add an API to the server for presenting the existing facts. + +## The Starting Point +To get the initial application, run the next command: +```commandline +$ git clone --branch v3-server-get-facts https://github.com/FTBpro/go-workshop.git +``` +This will clone the branch `v3-server-get-facts` which we will use for building and running our web server. + +## Your Goal + +After implementing all the TODOs, the server will export another API for getting the current facts. +```json +GET /facts + +response: +{ + "facts": [ + { + "image": "...", + "description": "..." + } + //... + ] +} +``` + +## Getting Started +Take a look around the program. You will notice new folders - `coolfact` and `inmem`. In addition you will notice some changes in the main package. + +### coolfact +This is a package containing the entity `fact` and the service for implementing the use case (business logic). +In this application we won't have much BL, if any. Our application is a very simple CRUD app, and the service will mainly call the repo as we will see. + +### inmem +In this package we will implement the facts-repository. We will use in memory. +In Go, packages names are very important. A package name should say what it provides, not what it contains. +We won't have packages names like `repos`, `models`, `utils`, `common`, etc. + +A package’s name provides context for its contents, making it easier for clients to understand what the package is for and how to use it. +For example, Go package `time` indicates that it contains functionality for handling times, probably a struct and behaviour for measuring and displaying time. Go package `http` lets you speak `HTTP`. + +In our case, package `inmem` imply that it uses memory mechanism. If we have SQL database, we will have package `sql` that implies it lets you speak SQL. + +## Step 0 - notice changes in main.go +Let's have a look at `coolfacts_server/main.go`: +```go +package main + +import ( + "fmt" + "log" + "net/http" + + // new imports + "github.com/FTBpro/go-workshop/coolfacts/coolfact" + "github.com/FTBpro/go-workshop/coolfacts/inmem" +) + +func main() { + fmt.Println("Hello, Server!") + + // new initializations + factsRepo := inmem.NewFactsRepository() + service := coolfact.NewService(factsRepo) + server := NewServer(service) + + log.Println("starting server on port 9002") + log.Println("You can go to http://127.0.0.1:9002/ping") + if err := http.ListenAndServe(":9002", server); err != nil { + panic(fmt.Errorf("server crashed! err: %w", err)) + } +} +``` +We first can notice the new imports: +```go +"github.com/FTBpro/go-workshop/coolfacts/coolfact" +"github.com/FTBpro/go-workshop/coolfacts/inmem" +``` +we are importing packages from our own module. We have the module path and then the path to the package we want to import. The module path is `github.com/FTBpro/go-workshop/coolfacts`. + +In the next lines we initializing the repo, service and the server. A pacage name is only the last param, and each type in Go has a name composed from the package name and the type identifier. For example, we call `inmem.NewFactsRepository()`. The package name is `inmem`, and the type identifier is `inmem.NewFactsRepository()` + +What you will have to complete is: +## Step 1 - package `coolfact` +This package handles the BL of the application. + +### file `coolfact/fact.go`: +In here we have the entity of the application. A struct named `Fact`. +- Complete the definition of `Fact`. It should have the following fields: + - Image string + - Description string + +### file `coolfact/service.go`: +In service.go we have the service which will handle the "BL" for the application. In the service you will: +- **Finish `Repository` interface{}** + - in Go we declare interfaces where we use them, not where we implement them. For the service to operate properly, it requires a `Repository` interface which he defines. It makes sense, since the service knows what it needs to do, and what the dependancy it needs to have. We can note that the name of the interface{} isn't InmemRepo or SQLRepo or something else, since the service is agnostic to the way the repo operates. He doesn't care about the mechanism, only behaviour. + - Add one method for getting facts. + - `GetFacts` - return slice of `coolfact.Fact` and an error + - Finish definition of service. + - Implement `NewService`. Return instance of `service` initialized with its field. + - Implement methods `GetFacts`. + +## Step 2 - Package `inmem` +### file `inmem/factsrepo.go`: +Here we will implement the facts repository. Currently, only with functionality to return facts. +- Implement `NewFactsRepository` + - Just so we will have initial data, initialize the repo with two facts. +- Implement method `GetFacts`. + +## Step 3 - `cmd/server.go` +As mentioned before, you will implement a new API for the `server`, what you will have to complete in the server is: +- Finish definition of the FactsService interface. Which method does the server needs in order to operate? +- Add field in the `server` for the factsService, and complete the initialization. In function `NewServer` add pass argument. +- In method `serveHTTP`, add a case for the new API, that will call method `HandleGetFacts` +- Implement `HandleGetFacts` method. The method for handling the `GET /facts` API + - Call the service in order to get the facts. + - Format the response to JSON: + ```json + { + "facts": [ + { + "image": "...", + "description": "..." + }, + //... + ] + } + ``` + - Write status 200. + - Set "content-type" header to "application/json". + +### Building and Running + +If everything is implemented well, this is what the final result should look like when running the program: +![factsgif](https://user-images.githubusercontent.com/5252381/204143457-6eaf59d3-6c52-4fbb-8d2a-19d22436cbd8.gif) + +# Full walkthrough +In the following section you fill find a full walkthrough. Use it in case you are stuck. + +## Step 1 - Implement the core entity and the service + +In `fact.go` we simply add fiedls to the entity: +```go +type Fact struct { + Image string + Description string +} +``` +We can notice that these are exported (public) fields, since it’s an entity and the rest of the application should be aware of. + +#### In `service.go` +The `Repository` interface currently only should have one method for getting facts. +The `service` has one field for the `factsRepo`, notice that the service and the field are _private_, we don’t want that someone will set or get it from outside. +We want to require the consumer to use the initializer. +```go +type Repository interface { + GetFacts() ([]Fact, error) +} + +type service struct { + factsRepo Repository +} + +func NewService(factsRepo Repository) *service { + return &service{ + factsRepo: factsRepo, + } +} +``` + +And the `GetFacts` implementation: +```go +func (s *service) GetFacts() ([]Fact, error) { + facts, err := s.factsRepo.GetFacts() + if err != nil { + return nil, fmt.Errorf("factsService.GetFacts: %w", err) + } + + return facts, nil +} +``` +In GetFacts the service calls the repo. Notice that if there is an error, the service wraps it and adding some context, so we will have friendlier message. + +## Step 2 - The repo +We initialize the `factsRepo` with a slice including 2 cool facts +```go +func NewFactsRepository() *factsRepo { + return &factsRepo{ + facts: []coolfact.Fact{ + { + Image: "https://images2.minutemediacdn.com/image/upload/v1556645500/shape/cover/entertainment/D5aliXvWsAEcYoK-fe997566220c082b98030508e654948e.jpg", + Description: "Did you know sonic is a hedgehog?!", + }, + { + Image: "https://images2.minutemediacdn.com/image/upload/v1556641470/shape/cover/entertainment/uncropped-Screen-Shot-2019-04-30-at-122411-PM-3b804f143c543dfab4b75c81833bed1b.jpg", + Description: "You won't believe what happened to Arya!", + }, + }, + } +} + +func (r *factsRepo) GetFacts() ([]coolfact.Fact, error) { + return r.facts, nil +} +``` + +## Step 3 - The HTTP transport layer +The `server` receives the `Service interface{}` which implements the BL. We're injecting `Service` interface as a dependency to the `server`, for the `server` to operate. This is what the `server` requires. +```go +type FactsService interface { + GetFacts() ([]coolfact.Fact, error) +} + +type server struct { + factsService Service +} + +func NewServer(service Service) *server { + return &server{ + factsService: service, + } +} +``` +Notice the type `coolfact.Fact`. A common anti-pattern in Go is to repeat a word in a type name. For example `http.Handler` and not `http.HttpHandler`. +In case of entities we sometimes encounter such repetition. + +`HandleGetFacts` method: + +First, we receive the slice of facts from the service and then format them to a JSON which will be returned to the client. +```go +func (s *server) HandleGetFacts(w http.ResponseWriter) { + facts, err := s.factsService.GetFacts() + if err != nil { + s.HandleError(w, fmt.Errorf("server.GetFactsHandler: %w", err)) + } + + // we first format the facts to map[string]interface. + formattedFacts := make([]map[string]interface{}, len(facts)) + for i, coolFact := range facts { + formattedFacts[i] = map[string]interface{}{ + "image": coolFact.Image, + "description": coolFact.Description, + } + } + + response := map[string]interface{}{ + "facts": formattedFacts, + } + + // code omitted +} +``` +You may ask yourself why don’t we just return the facts to the client? +This is because we want to keep separation of concerns. The entity should know nothing about the outer layer, for example the client. + +The API response and the entity field should be considered different. We don’t want that changes in the entity will have unexpected cascading changes, +and we don’t want that a requirement to modify the response will trigger a change to the entity. + +The rest of the method is to write the status and content type header, and then write the response body to the writer: +```go +func (s *server) HandleGetFacts(w http.ResponseWriter) { + // code omitted + + // write status and content-type + // status must be written before the body + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + + // write the body. We use json encoding + if err := json.NewEncoder(w).Encode(response); err != nil { + fmt.Printf("HandleGetFacts ERROR writing response: %s", err) +} +``` + +Since we are returning a JSON, we can use the Go `json` package, which can writes a JSON encoding of the response to the writer. +The `NewEncoder` method receives `io.Writer`. We already can notice the power of Go interfaces, and especially interfaces with one method. +We can send the `http.ResponseWriter` to different packages methods, and use a different encoding, and the packages are totally agnostic to HTTP. + +# Finish! +Congratulation! You've just implemented a new API with a totally cool use-case! + +In the following exercise we will add an API for creating a fact, and a client application for calling our server. \ No newline at end of file From 37bb15e60634af044ad9ad4e453ad102fd73563b Mon Sep 17 00:00:00 2001 From: Oren Rosen Date: Mon, 28 Nov 2022 15:31:18 +0200 Subject: [PATCH 4/9] reorder --- coolfacts/cmd/coolfacts_server/server.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/coolfacts/cmd/coolfacts_server/server.go b/coolfacts/cmd/coolfacts_server/server.go index 140e336..30f2252 100644 --- a/coolfacts/cmd/coolfacts_server/server.go +++ b/coolfacts/cmd/coolfacts_server/server.go @@ -41,6 +41,17 @@ func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } +func (s *server) HandlePing(w http.ResponseWriter) { + log.Println("Handling Ping ...") + + w.WriteHeader(http.StatusOK) + + if _, err := fmt.Fprint(w, "PONG"); err != nil { + fmt.Printf("ERROR writing to ResponseWriter: %s\n", err) + return + } +} + func (s *server) HandleGetFacts(w http.ResponseWriter) { log.Println("Handling getFact ...") @@ -65,17 +76,6 @@ func (s *server) HandleGetFacts(w http.ResponseWriter) { // ] } -func (s *server) HandlePing(w http.ResponseWriter) { - log.Println("Handling Ping ...") - - w.WriteHeader(http.StatusOK) - - if _, err := fmt.Fprint(w, "PONG"); err != nil { - fmt.Printf("ERROR writing to ResponseWriter: %s\n", err) - return - } -} - func (s *server) HandleNotFound(w http.ResponseWriter, err error) { log.Println("Handling notFound ...") From 6fa487415b4d19e64324eaae97d6f125ac44eede Mon Sep 17 00:00:00 2001 From: Oren Rosen Date: Fri, 2 Dec 2022 19:42:29 +0200 Subject: [PATCH 5/9] add arg --- coolfacts/inmem/factsrepo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coolfacts/inmem/factsrepo.go b/coolfacts/inmem/factsrepo.go index f763df4..46e3c3c 100644 --- a/coolfacts/inmem/factsrepo.go +++ b/coolfacts/inmem/factsrepo.go @@ -8,7 +8,7 @@ type factsRepo struct { facts []coolfact.Fact } -func NewFactsRepository() *factsRepo { +func NewFactsRepository(facts ...coolfact.Fact) *factsRepo { // TODO: init facts repo } From 6be68d0057f7fb8d260f8fec42fa002a9fffe7c9 Mon Sep 17 00:00:00 2001 From: Oren Rosen Date: Fri, 2 Dec 2022 19:43:53 +0200 Subject: [PATCH 6/9] add facts --- coolfacts/cmd/coolfacts_server/main.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/coolfacts/cmd/coolfacts_server/main.go b/coolfacts/cmd/coolfacts_server/main.go index cf9ec1a..13d8ca4 100644 --- a/coolfacts/cmd/coolfacts_server/main.go +++ b/coolfacts/cmd/coolfacts_server/main.go @@ -12,7 +12,7 @@ import ( func main() { fmt.Println("Hello, Server!") - factsRepo := inmem.NewFactsRepository() + factsRepo := inmem.NewFactsRepository(seedFacts()...) service := coolfact.NewService(factsRepo) server := NewServer(service) @@ -22,3 +22,16 @@ func main() { panic(fmt.Errorf("server crashed! err: %w", err)) } } + +func seedFacts() []coolfact.Fact { + return []coolfact.Fact{ + { + Topic: "Games", + Description: "Did you know sonic is a hedgehog?!", + }, + { + Topic: "TV", + Description: "You won't believe what happened to Arya!", + }, + } +} From fc1aecbd692361889c40f3052fa428e720ef420e Mon Sep 17 00:00:00 2001 From: Oren Rosen Date: Sat, 10 Dec 2022 14:26:52 +0200 Subject: [PATCH 7/9] add test --- coolfacts/coolfact/service_test.go | 97 ++++++++++++++++++++++++++++++ coolfacts/go.mod | 8 +++ coolfacts/go.sum | 17 ++++++ 3 files changed, 122 insertions(+) create mode 100644 coolfacts/coolfact/service_test.go create mode 100644 coolfacts/go.sum diff --git a/coolfacts/coolfact/service_test.go b/coolfacts/coolfact/service_test.go new file mode 100644 index 0000000..23d06f8 --- /dev/null +++ b/coolfacts/coolfact/service_test.go @@ -0,0 +1,97 @@ +package coolfact_test + +import ( + "fmt" + "math/rand" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/FTBpro/go-workshop/coolfacts/coolfact" + "github.com/FTBpro/go-workshop/coolfacts/inmem" +) + +func Test_service_GetFacts(t *testing.T) { + facts := generateRandomFactsDesc(10) + + tests := []struct { + name string + repoField coolfact.Repository + want []coolfact.Fact + wantErr bool + }{ + { + name: "with facts", + repoField: inmem.NewFactsRepository(facts...), + want: facts, + wantErr: false, + }, + { + name: "no facts - should get nil", + repoField: inmem.NewFactsRepository(), + want: nil, + wantErr: false, + }, + { + name: "repo returns error", + repoField: mockRepoError{}, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := coolfact.NewService(tt.repoField) + got, err := s.GetFacts() + if (err != nil) != tt.wantErr { + t.Errorf("GetFacts() error = %v, wantErr %v", err, tt.wantErr) + return + } + + expectEqualFacts(t, tt.want, got) + }) + } +} + +// generateRandomFactsDesc creates new random facts sorted by DESC +func generateRandomFactsDesc(n int) []coolfact.Fact { + var facts []coolfact.Fact + for i := 0; i < n; i++ { + fact := randomFact() + facts = append(facts, fact) + } + + return facts +} + +func randomFact() coolfact.Fact { + rand.Seed(time.Now().UnixNano()) + return coolfact.Fact{ + Topic: fmt.Sprintf("Topic %d", rand.Intn(10000)), + Description: fmt.Sprintf("Some Description %d", rand.Intn(10000)), + } +} + +func expectEqualFacts(t *testing.T, expected, got []coolfact.Fact) { + require.Equalf(t, len(expected), len(got), "expectEqualFacts: different length") + + for _, fact := range got { + require.Contains(t, expected, fact, "expectEqualFacts: got unexpected fact") + } + + for _, fact := range expected { + require.Contains(t, got, fact, "expectEqualFacts: didn't got expected fact") + } +} + +type mockRepoError struct { +} + +func (m mockRepoError) GetFacts() ([]coolfact.Fact, error) { + return nil, fmt.Errorf("mock repo returns error") +} + +func (m mockRepoError) CreateFact(fact coolfact.Fact) error { + return fmt.Errorf("mock repo returns error") +} diff --git a/coolfacts/go.mod b/coolfacts/go.mod index b1dd8ae..479e548 100644 --- a/coolfacts/go.mod +++ b/coolfacts/go.mod @@ -1,3 +1,11 @@ module github.com/FTBpro/go-workshop/coolfacts go 1.18 + +require github.com/stretchr/testify v1.8.1 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/coolfacts/go.sum b/coolfacts/go.sum new file mode 100644 index 0000000..2ec90f7 --- /dev/null +++ b/coolfacts/go.sum @@ -0,0 +1,17 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 7d4102263c5a18bcd7b338f748ec73a9ed810e08 Mon Sep 17 00:00:00 2001 From: Oren Rosen Date: Sat, 10 Dec 2022 15:00:53 +0200 Subject: [PATCH 8/9] doc --- coolfacts/docs/ex3-get-facts.md | 113 +++++++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 2 deletions(-) diff --git a/coolfacts/docs/ex3-get-facts.md b/coolfacts/docs/ex3-get-facts.md index 8252707..b1930c1 100644 --- a/coolfacts/docs/ex3-get-facts.md +++ b/coolfacts/docs/ex3-get-facts.md @@ -24,7 +24,12 @@ response: ``` ## Getting Started -Take a look around the program. You will notice new folders - `coolfact` and `inmem`. In addition, you will notice some changes in the main package. +Take a look around the program. You will notice some changes which we will cover below. +- New folders - `coolfact` and `inmem`. +- A few changes in the main package. +- Test file for our service. +- Changes in _go.mod_, and a new file - _go.sum_. + ### coolfact This is a package containing the entity `fact` and the service for implementing the use case (business logic). @@ -106,8 +111,112 @@ If you already have multiple args in a slice, you apply them to a variadic funct inmem.NewFactsRepository(seedFacts()...) ``` +## Step 0.1 - Notice `coolfact/service_test.go` + +You can notice that we've added tests for our service, before you start to implement, let's understand what's in it. + +Test files in go have the suffix `_test`. These files are not been built when you build the application. They are only considered when running the `go test` command: +```commandline +go test [build/test flags] [packages] [build/test flags & test binary flags] +``` + +For runnning all the tests, you need to be on the root folder (the one with the go.mod) and run +```commandline +.../coolfacts$ go test ./... +``` +Let's take a look in the file itself. notice it's package name `coolfact_test`. In Go, the only valid case for a folder to contain two packages is a test package. The suffix `_test` to the package isn't mandatory, but it helps when you only wish to test the public interface of the package. This helps us to check how does the interface "feels" from a real consumer POV. + +When running the `go test` command, Go searches in all of the `_test.go` files for functions with `Test` prefix. These files receive one argument `t *testing.T` which is a type passed to Test functions to manage test state and support formatted test logs. + +Test functions can be named anything with a `Test` prefix, but there is some convention: +```go +func Test__ +``` +In here, we test our `service` method `GetFacts`, so our test function is named: +```go +func Test_service_AllFacts(t *testing.T) {...} +``` + +The structure of the test is an example of _Table Driven Tests_: + +```go +func Test...(t *testing.T) { + type testCase struct {...} + + tests := []testCase{ + {...}, + {...}, + } + + for _, tc := range tests { + t.Run(tc.Name, func(t *testing.T) { + // Here we write the test itself + }) + } +} +``` +We declaring type `testCase` which is a struct that holds the parameters for the tests, these can be: +- Name of the testCase. +- Input for initializing the service. +- Arguments for the methods. +- Expected result. +- Indicator if we expect an error. + +In the `t.Run` method we write the test: + +```go +func Test_service_GetFacts(t *testing.T) { + testCases := []struct { + name string + repoField coolfact.Repository + want []coolfact.Fact + wantErr bool + }{ + // code omitted + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + s := coolfact.NewService(tc.repoField) + got, err := s.GetFacts() + if err != nil { + require.True(t, tc.wantErr, "got an unexpected error from service") + return + } + + require.False(t, tc.wantErr, "expected an error but didn't receive one.") + expectEqualFacts(t, tc.want, got) + }) + } +``` + +* We initialize the service with the repo we set in the `repoField` field. +* Call the method `GetFacts` +* Check if we expect an error. +* Check that the facts we got from the service is what we expected. + +### `require` / `go.mod` / `go.sum` +`require` is a package that provides helpful methods for testing. It also prints the failure in a more readable way. Since it's an external library, you can see that we've added a _require_ in `go.mod`: +```text +require github.com/stretchr/testify v1.8.1 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) +``` + +The `// indirect` comment indicates that our module depends on these packages, but doesn't directly import them. + +You can run `go mod tidy` command for sync the `go.mod`. (Doesn't upgrade versions implicitly) +```commandline +.../coolfacts$ go mod tidy +``` + +### _go.sum_ +Another file that `go mod tidy` generates is `go.sum`. This file lists down the checksum of direct and indirect dependency required along with the version. It is to be mentioned that the go.mod file is enough for a successful build. The checksum present in _go.sum_ file is used to validate the checksum of each of direct and indirect dependency to confirm that none of them has been modified. -What you will have to complete is: ## Step 1 - package `coolfact` This package handles the BL of the application. From 1112c922ff2e34e9a19e4dc3861c237af3761b45 Mon Sep 17 00:00:00 2001 From: Oren Rosen Date: Sat, 10 Dec 2022 15:01:12 +0200 Subject: [PATCH 9/9] Add test[ --- coolfacts/coolfact/service_test.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/coolfacts/coolfact/service_test.go b/coolfacts/coolfact/service_test.go index 23d06f8..b1bd197 100644 --- a/coolfacts/coolfact/service_test.go +++ b/coolfacts/coolfact/service_test.go @@ -15,7 +15,7 @@ import ( func Test_service_GetFacts(t *testing.T) { facts := generateRandomFactsDesc(10) - tests := []struct { + testCases := []struct { name string repoField coolfact.Repository want []coolfact.Fact @@ -40,16 +40,17 @@ func Test_service_GetFacts(t *testing.T) { wantErr: true, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - s := coolfact.NewService(tt.repoField) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + s := coolfact.NewService(tc.repoField) got, err := s.GetFacts() - if (err != nil) != tt.wantErr { - t.Errorf("GetFacts() error = %v, wantErr %v", err, tt.wantErr) + if err != nil { + require.True(t, tc.wantErr, "got an unexpected error from service") return } - expectEqualFacts(t, tt.want, got) + require.False(t, tc.wantErr, "expected an error but didn't receive one.") + expectEqualFacts(t, tc.want, got) }) } }