diff --git a/build.sh b/build.sh index 3b4f93d7..dca93f33 100755 --- a/build.sh +++ b/build.sh @@ -3,17 +3,16 @@ set -e go get github.com/gorilla/websocket #todo vendor this or learn about the module stuff! -#go get -u golang.org/x/lint/golint -#go get -u github.com/client9/misspell/cmd/misspell -#go get github.com/po3rin/gofmtmd/cmd/gofmtmd +go install github.com/client9/misspell/cmd/misspell@latest +go install github.com/po3rin/gofmtmd/cmd/gofmtmd@latest -#ls *.md | xargs misspell -error -#for md_file in ./*.md; do -# echo "formatting file: $md_file" -# gofmtmd "$md_file" -r -#done +ls *.md | xargs misspell -error + +for md_file in ./*.md; do + echo "formatting file: $md_file" + gofmtmd "$md_file" -r +done go test ./... go vet ./... go fmt ./... -#golint ./... diff --git a/command-line.md b/command-line.md index ffd3b007..67d6c32b 100644 --- a/command-line.md +++ b/command-line.md @@ -68,7 +68,7 @@ Finally, we need to import this package into `main.go` so we can use it to creat The paths will be different on your computer, but it should be similar to this: ```go -//cmd/webserver/main.go +// cmd/webserver/main.go package main import ( @@ -116,7 +116,7 @@ In addition, users can view [the documentation at pkg.go.dev](https://pkg.go.dev Before we get stuck into writing tests, let's add a new application that our project will build. Create another directory inside `cmd` called `cli` (command line interface) and add a `main.go` with the following ```go -//cmd/cli/main.go +// cmd/cli/main.go package main import "fmt" @@ -137,7 +137,7 @@ Before we jump too far ahead though, let's just write a test to check it integra Inside `CLI_test.go` (in the root of the project, not inside `cmd`) ```go -//CLI_test.go +// CLI_test.go package poker import "testing" @@ -172,7 +172,7 @@ At this point, you should be comfortable enough to create our new `CLI` struct w You should end up with code like this ```go -//CLI.go +// CLI.go package poker type CLI struct { @@ -464,7 +464,7 @@ Anecdotally I have used this technique in other shared packages and it has prove So let's create a file called `testing.go` and add our stub and our helpers. ```go -//testing.go +// testing.go package poker import "testing" @@ -622,7 +622,7 @@ Now refactor both of our applications to use this function to create the store. #### CLI application code ```go -//cmd/cli/main.go +// cmd/cli/main.go package main import ( @@ -651,7 +651,7 @@ func main() { #### Web server application code ```go -//cmd/webserver/main.go +// cmd/webserver/main.go package main import ( diff --git a/generics.md b/generics.md index 31ef33d3..b434fe5c 100644 --- a/generics.md +++ b/generics.md @@ -437,7 +437,7 @@ func (s *Stack[T]) Push(value T) { } func (s *Stack[T]) IsEmpty() bool { - return len(s.values)==0 + return len(s.values) == 0 } func (s *Stack[T]) Pop() (T, bool) { @@ -446,7 +446,7 @@ func (s *Stack[T]) Pop() (T, bool) { return zero, false } - index := len(s.values) -1 + index := len(s.values) - 1 el := s.values[index] s.values = s.values[:index] return el, true diff --git a/hello-world.md b/hello-world.md index b35faeb6..a2006a86 100644 --- a/hello-world.md +++ b/hello-world.md @@ -409,20 +409,20 @@ The tests should now pass. Now it is time to _refactor_. You should see some problems in the code, "magic" strings, some of which are repeated. Try and refactor it yourself, with every change make sure you re-run the tests to make sure your refactoring isn't breaking anything. ```go - const spanish = "Spanish" - const englishHelloPrefix = "Hello, " - const spanishHelloPrefix = "Hola, " + const spanish = "Spanish" + const englishHelloPrefix = "Hello, " + const spanishHelloPrefix = "Hola, " -func Hello(name string, language string) string { - if name == "" { - name = "World" - } + func Hello(name string, language string) string { + if name == "" { + name = "World" + } - if language == spanish { - return spanishHelloPrefix + name + if language == spanish { + return spanishHelloPrefix + name + } + return englishHelloPrefix + name } - return englishHelloPrefix + name -} ``` ### French @@ -483,10 +483,10 @@ You could argue that maybe our function is getting a little big. The simplest re const ( french = "French" spanish = "Spanish" - - englishHelloPrefix = "Hello, " - spanishHelloPrefix = "Hola, " - frenchHelloPrefix = "Bonjour, " + + englishHelloPrefix = "Hello, " + spanishHelloPrefix = "Hola, " + frenchHelloPrefix = "Bonjour, " ) func Hello(name string, language string) string { diff --git a/html-templates.md b/html-templates.md index b292a6f6..defeffc5 100644 --- a/html-templates.md +++ b/html-templates.md @@ -531,7 +531,7 @@ func Render(w io.Writer, p Post) error { return err } - if err := templ.ExecuteTemplate(w, "blog.gohtml", p); err != nil { + if err := templ.ExecuteTemplate(w, "blog.gohtml", p); err != nil { return err } @@ -588,7 +588,7 @@ func NewPostRenderer() (*PostRenderer, error) { func (r *PostRenderer) Render(w io.Writer, p Post) error { - if err := r.templ.ExecuteTemplate(w, "blog.gohtml", p); err != nil { + if err := r.templ.ExecuteTemplate(w, "blog.gohtml", p); err != nil { return err } diff --git a/http-server.md b/http-server.md index 67ae73db..b09392d9 100644 --- a/http-server.md +++ b/http-server.md @@ -959,7 +959,7 @@ For that reason, it is recommended that you research _The Test Pyramid_. In the interest of brevity, I am going to show you the final refactored integration test. ```go -//server_integration_test.go +// server_integration_test.go package main import ( @@ -1035,7 +1035,7 @@ func (i *InMemoryPlayerStore) GetPlayerScore(name string) int { The integration test passes, now we just need to change `main` to use `NewInMemoryPlayerStore()` ```go -//main.go +// main.go package main import ( diff --git a/intro-to-acceptance-tests.md b/intro-to-acceptance-tests.md index d80267a1..f4a5e801 100644 --- a/intro-to-acceptance-tests.md +++ b/intro-to-acceptance-tests.md @@ -254,13 +254,13 @@ func waitForServerListening(port string) error { } func randomString(n int) string { - var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") - s := make([]rune, n) - for i := range s { - s[i] = letters[rand.Intn(len(letters))] - } - return string(s) + s := make([]rune, n) + for i := range s { + s[i] = letters[rand.Intn(len(letters))] + } + return string(s) } ``` @@ -294,7 +294,7 @@ import ( const ( port = "8080" - url = " + port + url = " +port ) func TestGracefulShutdown(t *testing.T) { diff --git a/io.md b/io.md index d4a1811e..ae438838 100644 --- a/io.md +++ b/io.md @@ -718,7 +718,7 @@ store := &FileSystemPlayerStore{database} If you run the test it should pass and now we can delete `InMemoryPlayerStore`. `main.go` will now have compilation problems which will motivate us to now use our new store in the "real" code. ```go -//main.go +// main.go package main import ( @@ -822,7 +822,7 @@ How will we test for this though? What we need to do is first refactor our code We'll create a new type to encapsulate our "when we write we go from the beginning" functionality. I'm going to call it `Tape`. Create a new file with the following: ```go -//tape.go +// tape.go package main import "io" diff --git a/math.md b/math.md index 24334d3d..96eccdaa 100644 --- a/math.md +++ b/math.md @@ -131,7 +131,7 @@ So my first test looks like this: package clockface_test import ( - "projectpath/clockface" + "projectpath/clockface" "testing" "time" ) @@ -1068,7 +1068,7 @@ const ( clockCentreY = 150 ) -//SVGWriter writes an SVG representation of an analogue clock, showing the time t, to the writer w +// SVGWriter writes an SVG representation of an analogue clock, showing the time t, to the writer w func SVGWriter(w io.Writer, t time.Time) { io.WriteString(w, svgStart) io.WriteString(w, bezel) @@ -2073,7 +2073,7 @@ const ( clockCentreY = 150 ) -//SVGWriter writes an SVG representation of an analogue clock, showing the time t, to the writer w +// SVGWriter writes an SVG representation of an analogue clock, showing the time t, to the writer w func SVGWriter(w io.Writer, t time.Time) { io.WriteString(w, svgStart) io.WriteString(w, bezel) diff --git a/reflection.md b/reflection.md index e3f887e7..1086e130 100644 --- a/reflection.md +++ b/reflection.md @@ -744,7 +744,7 @@ To fix this, we'll need to move our assertion with the maps to a new test where ```go t.Run("with maps", func(t *testing.T) { aMap := map[string]string{ - "Cow": "Moo", + "Cow": "Moo", "Sheep": "Baa", } diff --git a/roman-numerals.md b/roman-numerals.md index d12a5862..ed7e6ce8 100644 --- a/roman-numerals.md +++ b/roman-numerals.md @@ -1176,13 +1176,13 @@ chapter so, in the interests of full disclosure, here's what he said. > sometimes `arabic` will be written as a decimal integer literal > > ```go -> ConvertToRoman(255) +> ConvertToRoman(255) > ``` > > But it could just as well be written > > ```go -> ConvertToRoman(0xFF) +> ConvertToRoman(0xFF) > ``` > > Really, we're not 'converting' from an Arabic numeral at all, we're 'printing' - diff --git a/scaling-acceptance-tests.md b/scaling-acceptance-tests.md index a0eae3eb..dec41a1a 100644 --- a/scaling-acceptance-tests.md +++ b/scaling-acceptance-tests.md @@ -234,8 +234,12 @@ We wish to run our specification in a Go test. We already have access to a `*tes `specifications.Greeter` is an interface, which we will implement with a `Driver` by changing the new TestGreeterServer code to the following: ```go +import ( + go_specs_greet "github.com/quii/go-specs-greet" +) + func TestGreeterServer(t *testing.T) { - driver := go_specs_greet.Driver{BaseURL: "http://localhost:8080"} + driver := go_specs_greet.Driver{BaseURL: "http://localhost:8080"} specifications.GreetSpecification(t, driver) } ``` @@ -327,10 +331,10 @@ func TestGreeterServer(t *testing.T) { FromDockerfile: testcontainers.FromDockerfile{ Context: "../../.", Dockerfile: "./cmd/httpserver/Dockerfile", - // set to false if you want less spam, but this is helpful if you're having troubles - PrintBuildLog: true, + // set to false if you want less spam, but this is helpful if you're having troubles + PrintBuildLog: true, }, - ExposedPorts: []string{"8080:8080"}, + ExposedPorts: []string{"8080:8080"}, WaitingFor: wait.ForHTTP("/").WithPort("8080"), } container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ @@ -417,6 +421,12 @@ Try to run the test again, and it should fail with the following. Update the handler to behave how our specification wants it to ```go +import ( + "fmt" + "log" + "net/http" +) + func main() { handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { fmt.Fprint(w, "Hello, world") @@ -432,9 +442,14 @@ func main() { Whilst this technically isn't a refactor, we shouldn't rely on the default HTTP client, so let's change our Driver, so we can supply one, which our test will give. ```go +import ( + "io" + "net/http" +) + type Driver struct { BaseURL string - Client *http.Client + Client *http.Client } func (d Driver) Greet() (string, error) { @@ -455,7 +470,7 @@ In our test in `cmd/httpserver/greeter_server_test.go`, update the creation of t ```go client := http.Client{ - Timeout: 1 * time.Second, + Timeout: 1 * time.Second, } driver := go_specs_greet.Driver{BaseURL: "http://localhost:8080", Client: &client} @@ -546,6 +561,8 @@ The change in the specification has meant our driver needs to be updated. Update the driver so that it specifies a `name` query value in the request to ask for a particular `name` to be greeted. ```go +import "io" + func (d Driver) Greet(name string) (string, error) { res, err := d.Client.Get(d.BaseURL + "/greet?name=" + name) if err != nil { @@ -576,6 +593,11 @@ The test should now run, and fail. Extract the `name` from the request and greet. ```go +import ( + "fmt" + "net/http" +) + func Handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, %s", r.URL.Query().Get("name")) } @@ -599,7 +621,6 @@ func Handler(w http.ResponseWriter, r *http.Request) { ``` Create new file `./greet.go`: - ```go package go_specs_greet @@ -624,8 +645,8 @@ package go_specs_greet_test import ( "testing" - "github.com/quii/go-specs-greet/specifications" go_specs_greet "github.com/quii/go-specs-greet" + "github.com/quii/go-specs-greet/specifications" ) func TestGreet(t *testing.T) { @@ -667,8 +688,8 @@ package go_specs_greet_test import ( "testing" - "github.com/quii/go-specs-greet/specifications" gospecsgreet "github.com/quii/go-specs-greet" + "github.com/quii/go-specs-greet/specifications" ) func TestGreet(t *testing.T) { @@ -724,10 +745,10 @@ You'll now need to import the root package into `handler.go` to refer to the Gre package httpserver import ( - "fmt" - "net/http" + "fmt" + "net/http" - go_specs_greet "github.com/quii/go-specs-greet/domain/interactions" + go_specs_greet "github.com/quii/go-specs-greet/domain/interactions" ) func Handler(w http.ResponseWriter, r *http.Request) { @@ -745,29 +766,19 @@ package main import ( "net/http" - "github.com/quii/go-specs-greet/adapters/httpserver" + "github.com/quii/go-specs-greet/adapters/httpserver" ) func main() { handler := http.HandlerFunc(httpserver.Handler) http.ListenAndServe(":8080", handler) } - ``` and update the import and reference to `Driver` in greeter_server_test.go: ```go -import ( - ... - "github.com/quii/go-specs-greet/adapters/httpserver" - ... -) - - ... - driver := httpserver.Driver{BaseURL: "http://localhost:8080", Client: &client} - ... - +driver := httpserver.Driver{BaseURL: "http://localhost:8080", Client: &client} ``` Finally, it's helpful to gather our domain level code in to its own folder too. Don't be lazy and have a `domain` folder in your projects with hundreds of unrelated types and functions. Make an effort to think about your domain and group ideas that belong together, together. This will make your project easier to understand and will improve the quality of your imports. @@ -851,8 +862,8 @@ func StartDockerServer( t.Helper() req := testcontainers.ContainerRequest{ FromDockerfile: testcontainers.FromDockerfile{ - Context: "../../.", - Dockerfile: dockerFilePath, + Context: "../../.", + Dockerfile: dockerFilePath, PrintBuildLog: true, }, ExposedPorts: []string{fmt.Sprintf("%s:%s", port, port)}, @@ -1338,7 +1349,7 @@ We can add to our acceptance tests to see if the user wants to run our acceptanc ```go if testing.Short() { - t.Skip() + t.Skip() } ``` @@ -1557,6 +1568,4 @@ Specifications should then double up as documentation. They should specify clear - In this example, our "DSL" is not much of a DSL; we just used interfaces to decouple our specification from the real world and allow us to express domain logic cleanly. As your system grows, this level of abstraction might become clumsy and unclear. [Read into the "Screenplay Pattern"](https://cucumber.io/blog/bdd/understanding-screenplay-(part-1)/) if you want to find more ideas as to how to structure your specifications. - For emphasis, [Growing Object-Oriented Software, Guided by Tests,](http://www.growing-object-oriented-software.com) is a classic. It demonstrates applying this "London style", "top-down" approach to writing software. Anyone who has enjoyed Learn Go with Tests should get much value from reading GOOS. - [In the example code repository](https://github.com/quii/go-specs-greet), there's more code and ideas I haven't written about here, such as multi-stage docker build, you may wish to check this out. - - In particular, *for fun*, I made a **third program**, a website with some HTML forms to `Greet` and `Curse`. The `Driver` leverages the excellent-looking [https://github.com/go-rod/rod](https://github.com/go-rod/rod) module, which allows it to work with the website with a browser, just like a user would. Looking at the git history, you can see how I started not using any templating tools "just to make it work" Then, once I passed my acceptance test, I had the freedom to do so without fear of breaking things. - - + - In particular, *for fun*, I made a **third program**, a website with some HTML forms to `Greet` and `Curse`. The `Driver` leverages the excellent-looking [https://github.com/go-rod/rod](https://github.com/go-rod/rod) module, which allows it to work with the website with a browser, just like a user would. Looking at the git history, you can see how I started not using any templating tools "just to make it work" Then, once I passed my acceptance test, I had the freedom to do so without fear of breaking things. --> diff --git a/select.md b/select.md index ba5a6572..42a24ef8 100644 --- a/select.md +++ b/select.md @@ -1,6 +1,6 @@ # Select -**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/main/select)** +**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/main/reflection)** You have been asked to make a function called `WebsiteRacer` which takes two URLs and "races" them by hitting them with an HTTP GET and returning the URL which returned first. If none of them return within 10 seconds then it should return an `error`. @@ -258,7 +258,6 @@ Our final requirement was to return an error if `Racer` takes longer than 10 sec ```go func TestRacer(t *testing.T) { - t.Run("compares speeds of servers, returning the url of the fastest one", func(t *testing.T) { slowServer := makeDelayedServer(20 * time.Millisecond) fastServer := makeDelayedServer(0 * time.Millisecond) @@ -290,6 +289,7 @@ func TestRacer(t *testing.T) { t.Error("expected an error but didn't get one") } }) +} ``` We've made our test servers take longer than 10s to return to exercise this scenario and we are expecting `Racer` to return two values now, the winning URL (which we ignore in this test with `_`) and an `error`. diff --git a/working-without-mocks.md b/working-without-mocks.md index f277dc06..3e1a92c0 100644 --- a/working-without-mocks.md +++ b/working-without-mocks.md @@ -36,8 +36,8 @@ Given this interface of a hypothetical recipe API: ```go type RecipeBook interface { -GetRecipes() ([]Recipe, error) -AddRecipes(...Recipe) error + GetRecipes() ([]Recipe, error) + AddRecipes(...Recipe) error } ``` @@ -47,35 +47,41 @@ We can construct test doubles in various ways, depending on how we're trying to ```go type StubRecipeStore struct { -recipes []Recipe -err error + recipes []Recipe + err error } func (s *StubRecipeStore) GetRecipes() ([]Recipe, error) { -return s.recipes, s.err + return s.recipes, s.err } // AddRecipes omitted for brevity +``` +```go // in test, we can set up the stub to always return specific recipes, or an error -stubStore := &StubRecipeStore{recipes: someRecipes} +stubStore := &StubRecipeStore{ + recipes: someRecipes, +} ``` **Spies** are like stubs but also record how they were called so the test can assert that the SUT calls the dependencies in specific ways. ```go type SpyRecipeStore struct { -AddCalls [][]Recipe -err error + AddCalls [][]Recipe + err error } func (s *SpyRecipeStore) AddRecipes(r ...Recipe) error { -s.AddCalls = append(s.AddCalls, r) -return s.err + s.AddCalls = append(s.AddCalls, r) + return s.err } // GetRecipes omitted for brevity +``` +```go // in test spyStore := &SpyRecipeStore{} sut := NewThing(spyStore) @@ -88,8 +94,8 @@ sut.DoStuff() ```go // set up the mock with expected calls -mockStore := &MockRecipeStore -mockStore.WhenCalledWith(someRecipes).return(someError) +mockStore := &MockRecipeStore{} +mockStore.WhenCalledWith(someRecipes).Return(someError) // when the sut uses the dependency, if it doesn't call it with someRecipes, usually mocks will panic ``` @@ -98,16 +104,16 @@ mockStore.WhenCalledWith(someRecipes).return(someError) ```go type FakeRecipeStore struct { -recipes []Recipe + recipes []Recipe } func (f *FakeRecipeStore) GetRecipes() ([]Recipe, error) { -return f.recipes, nil + return f.recipes, nil } func (f *FakeRecipeStore) AddRecipes(r ...Recipe) error { -f.recipes = append(f.recipes, r...) -return nil + f.recipes = append(f.recipes, r...) + return nil } ``` @@ -215,11 +221,13 @@ Using fakes, **we can make assertions based on the final states of the respectiv ```go // take our lego-bricks and assemble the system for the test fakeAPI1 := fakes.NewAPI1() -fakeAPI2 := // etc.. +fakeAPI2 := fakes.NewAPI2() // etc.. customerService := customer.NewService(fakeAPI1, fakeAPI2, etc...) // create new customer -newCustomerRequest := NewCustomerReq{...} +newCustomerRequest := NewCustomerReq{ + // ... +} createdCustomer, err := customerService.New(newCustomerRequest) assert.NoErr(t, err) @@ -227,7 +235,7 @@ assert.NoErr(t, err) fakeAPI1Customer := fakeAPI1.Get(createdCustomer.FakeAPI1Details.ID) assert.Equal(t, fakeAPI1Customer.SocialSecurityNumber, newCustomerRequest.SocialSecurityNumber) -// repeat for the other apis we care about +// repeat for the other apis we care about // update customer updatedCustomerRequest := NewUpdateReq{SocialSecurityNumber: "123", InternalID: createdCustomer.InternalID} @@ -260,54 +268,54 @@ Here is an example of a contract for one of the APIs the system depends on ```go type API1Customer struct { -Name string -ID string + Name string + ID string } type API1 interface { -CreateCustomer(ctx context.Context, name string) (API1Customer, error) -GetCustomer(ctx context.Context, id string) (API1Customer, error) -UpdateCustomer(ctx context.Context, id string, name string) error + CreateCustomer(ctx context.Context, name string) (API1Customer, error) + GetCustomer(ctx context.Context, id string) (API1Customer, error) + UpdateCustomer(ctx context.Context, id string, name string) error } type API1Contract struct { -NewAPI1 func() API1 + NewAPI1 func() API1 } func (c API1Contract) Test(t *testing.T) { -t.Run("can create, get and update a customer", func(t *testing.T) { -var ( -ctx = context.Background() -sut = c.NewAPI1() -name = "Bob" -) - -customer, err := sut.CreateCustomer(ctx, name) -expect.NoErr(t, err) - -got, err := sut.GetCustomer(ctx, customer.ID) -expect.NoErr(t, err) -expect.Equal(t, customer, got) - -newName := "Robert" -expect.NoErr(t, sut.UpdateCustomer(ctx, customer.ID, newName)) - -got, err = sut.GetCustomer(ctx, customer.ID) -expect.NoErr(t, err) -expect.Equal(t, newName, got.Name) -}) - -// example of strange behaviours we didn't expect -t.Run("the system will not allow you to add 'Dave' as a customer", func(t *testing.T) { -var ( -ctx = context.Background() -sut = c.NewAPI1() -name = "Dave" -) - -_, err := sut.CreateCustomer(ctx, name) -expect.Err(t, ErrDaveIsForbidden) -}) + t.Run("can create, get and update a customer", func(t *testing.T) { + var ( + ctx = context.Background() + sut = c.NewAPI1() + name = "Bob" + ) + + customer, err := sut.CreateCustomer(ctx, name) + expect.NoErr(t, err) + + got, err := sut.GetCustomer(ctx, customer.ID) + expect.NoErr(t, err) + expect.Equal(t, customer, got) + + newName := "Robert" + expect.NoErr(t, sut.UpdateCustomer(ctx, customer.ID, newName)) + + got, err = sut.GetCustomer(ctx, customer.ID) + expect.NoErr(t, err) + expect.Equal(t, newName, got.Name) + }) + + // example of strange behaviours we didn't expect + t.Run("the system will not allow you to add 'Dave' as a customer", func(t *testing.T) { + var ( + ctx = context.Background() + sut = c.NewAPI1() + name = "Dave" + ) + + _, err := sut.CreateCustomer(ctx, name) + expect.Err(t, ErrDaveIsForbidden) + }) } ``` @@ -322,9 +330,9 @@ To create our in-memory fake, we can use the contract in a test. ```go func TestInMemoryAPI1(t *testing.T) { -API1Contract{NewAPI1: func() API1 { -return inmemory.NewAPI1() -}}.Test(t) + API1Contract{NewAPI1: func() API1 { + return inmemory.NewAPI1() + }}.Test(t) } ``` @@ -332,37 +340,37 @@ And here is the fake's code ```go func NewAPI1() *API1 { -return &API1{customers: make(map[string]planner.API1Customer)} + return &API1{customers: make(map[string]planner.API1Customer)} } type API1 struct { -i int -customers map[string]planner.API1Customer + i int + customers map[string]planner.API1Customer } func (a *API1) CreateCustomer(ctx context.Context, name string) (planner.API1Customer, error) { -if name == "Dave" { -return planner.API1Customer{}, ErrDaveIsForbidden -} - -newCustomer := planner.API1Customer{ -Name: name, -ID: strconv.Itoa(a.i), -} -a.customers[newCustomer.ID] = newCustomer -a.i++ -return newCustomer, nil + if name == "Dave" { + return planner.API1Customer{}, ErrDaveIsForbidden + } + + newCustomer := planner.API1Customer{ + Name: name, + ID: strconv.Itoa(a.i), + } + a.customers[newCustomer.ID] = newCustomer + a.i++ + return newCustomer, nil } func (a *API1) GetCustomer(ctx context.Context, id string) (planner.API1Customer, error) { -return a.customers[id], nil + return a.customers[id], nil } func (a *API1) UpdateCustomer(ctx context.Context, id string, name string) error { -customer := a.customers[id] -customer.Name = name -a.customers[id] = customer -return nil + customer := a.customers[id] + customer.Name = name + a.customers[id] = customer + return nil } ``` @@ -407,38 +415,38 @@ Returning to the `API1` example, we can create a type that implements the needed ```go type API1Decorator struct { -delegate API1 -CreateCustomerFunc func(ctx context.Context, name string) (API1Customer, error) -GetCustomerFunc func(ctx context.Context, id string) (API1Customer, error) -UpdateCustomerFunc func(ctx context.Context, id string, name string) error + delegate API1 + CreateCustomerFunc func(ctx context.Context, name string) (API1Customer, error) + GetCustomerFunc func(ctx context.Context, id string) (API1Customer, error) + UpdateCustomerFunc func(ctx context.Context, id string, name string) error } // assert API1Decorator implements API1 var _ API1 = &API1Decorator{} func NewAPI1Decorator(delegate API1) *API1Decorator { -return &API1Decorator{delegate: delegate} + return &API1Decorator{delegate: delegate} } func (a *API1Decorator) CreateCustomer(ctx context.Context, name string) (API1Customer, error) { -if a.CreateCustomerFunc != nil { -return a.CreateCustomerFunc(ctx, name) -} -return a.delegate.CreateCustomer(ctx, name) + if a.CreateCustomerFunc != nil { + return a.CreateCustomerFunc(ctx, name) + } + return a.delegate.CreateCustomer(ctx, name) } func (a *API1Decorator) GetCustomer(ctx context.Context, id string) (API1Customer, error) { -if a.GetCustomerFunc != nil { -return a.GetCustomerFunc(ctx, id) -} -return a.delegate.GetCustomer(ctx, id) + if a.GetCustomerFunc != nil { + return a.GetCustomerFunc(ctx, id) + } + return a.delegate.GetCustomer(ctx, id) } func (a *API1Decorator) UpdateCustomer(ctx context.Context, id string, name string) error { -if a.UpdateCustomerFunc != nil { -return a.UpdateCustomerFunc(ctx, id, name) -} -return a.delegate.UpdateCustomer(ctx, id, name) + if a.UpdateCustomerFunc != nil { + return a.UpdateCustomerFunc(ctx, id, name) + } + return a.delegate.UpdateCustomer(ctx, id, name) } ``` @@ -447,8 +455,8 @@ In our tests, we can then use the `XXXFunc` field to modify the behaviour of the ```go failingAPI1 = NewAPI1Decorator(inmemory.NewAPI1()) failingAPI1.UpdateCustomerFunc = func(ctx context.Context, id string, name string) error { -return errors.New("failed to update customer") -}) + return errors.New("failed to update customer") +} ``` However, this _is_ awkward and requires you to exercise some judgement. With this approach, you are losing the guarantees from your contract as you are introducing ad-hoc behaviour to your fake in tests. @@ -498,45 +506,43 @@ Follow the TDD approach described above to drive out your persistence needs. package inmemory_test import ( - "github.com/quii/go-fakes-and-contracts/adapters/driven/persistence/inmemory" - "github.com/quii/go-fakes-and-contracts/domain/planner" - "testing" + "github.com/quii/go-fakes-and-contracts/adapters/driven/persistence/inmemory" + "github.com/quii/go-fakes-and-contracts/domain/planner" + "testing" ) func TestInMemoryPantry(t *testing.T) { - planner.PantryContract{ - NewPantry: func() planner.Pantry { - return inmemory.NewPantry() - }, - }.Test(t) + planner.PantryContract{ + NewPantry: func() planner.Pantry { + return inmemory.NewPantry() + }, + }.Test(t) } - ``` ```go package sqlite_test import ( - "github.com/quii/go-fakes-and-contracts/adapters/driven/persistence/sqlite" - "github.com/quii/go-fakes-and-contracts/domain/planner" - "testing" + "github.com/quii/go-fakes-and-contracts/adapters/driven/persistence/sqlite" + "github.com/quii/go-fakes-and-contracts/domain/planner" + "testing" ) func TestSQLitePantry(t *testing.T) { - client := sqlite.NewSQLiteClient() - t.Cleanup(func() { - if err := client.Close(); err != nil { - t.Error(err) - } - }) - - planner.PantryContract{ - NewPantry: func() planner.Pantry { - return sqlite.NewPantry(client) - }, - }.Test(t) + client := sqlite.NewSQLiteClient() + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Error(err) + } + }) + + planner.PantryContract{ + NewPantry: func() planner.Pantry { + return sqlite.NewPantry(client) + }, + }.Test(t) } - ``` Whilst Docker et al. _do_ make running databases locally easier, they can still carry a significant performance overhead. Fakes with contracts allow you to use restrict the need to use the "heavier" dependency to only when you're validating the contract, and not needed for other kinds of tests. @@ -582,4 +588,4 @@ So many times in my career, I have seen carefully written software written by ta Some teams rely on everyone deploying to a shared environment and testing there. The problem is this doesn't give you **isolated** feedback, and the **feedback is slow**. You still won't be able to construct different experiments with how your system works with other dependencies, at least not efficiently. -**We have to tame this complexity by adopting more sophisticated ways of modelling our dependencies** to quickly test/experiment on our dev machines before it gets to production. Create realistic and manageable fakes of your dependencies, verified by contracts. Then, you can start writing more meaningful tests and experimenting with your system, making you more likely to succeed. +**We have to tame this complexity by adopting more sophisticated ways of modelling our dependencies** to quickly test/experiment on our dev machines before it gets to production. Create realistic and manageable fakes of your dependencies, verified by contracts. Then, you can start writing more meaningful tests and experimenting with your system, making you more likely to succeed.