diff --git a/README.md b/README.md index ae2ee6412..7d5a85c3a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [Art by Denise](https://twitter.com/deniseyu21) -[![Build Status](https://travis-ci.org/quii/learn-go-with-tests.svg?branch=master)](https://travis-ci.org/quii/learn-go-with-tests) +[![Build Status](https://travis-ci.org/quii/learn-go-with-tests.svg?branch=main)](https://travis-ci.org/quii/learn-go-with-tests) [![Go Report Card](https://goreportcard.com/badge/github.com/quii/learn-go-with-tests)](https://goreportcard.com/report/github.com/quii/learn-go-with-tests) ## Formats @@ -16,7 +16,7 @@ ## Translations -- [中文](https://studygolang.gitbook.io/learn-go-with-tests) +- [中文](https://studygolang.gitbook.io/learn-go-with-tests) - [Português](https://larien.gitbook.io/aprenda-go-com-testes/) - [日本語](https://andmorefine.gitbook.io/learn-go-with-tests/) diff --git a/arrays-and-slices.md b/arrays-and-slices.md index f25eb5668..4fb26aff1 100644 --- a/arrays-and-slices.md +++ b/arrays-and-slices.md @@ -1,6 +1,6 @@ # Arrays and slices -**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/master/arrays)** +**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/main/arrays)** Arrays allow you to store multiple elements of the same type in a variable in a particular order. @@ -559,10 +559,10 @@ Another handy way to experiment with Go other than writing tests is the Go playground. You can try most things out and you can easily share your code if you need to ask questions. [I have made a go playground with a slice in it for you to experiment with.](https://play.golang.org/p/ICCWcRGIO68) -[Here is an example](https://play.golang.org/p/bTrRmYfNYCp) of slicing an array -and how changing the slice affects the original array; but a "copy" of the slice +[Here is an example](https://play.golang.org/p/bTrRmYfNYCp) of slicing an array +and how changing the slice affects the original array; but a "copy" of the slice will not affect the original array. -[Another example](https://play.golang.org/p/Poth8JS28sc) of why it's a good idea +[Another example](https://play.golang.org/p/Poth8JS28sc) of why it's a good idea to make a copy of a slice after slicing a very large slice. [for]: ../iteration.md# diff --git a/command-line.md b/command-line.md index e620ffc4a..e20ecdf61 100644 --- a/command-line.md +++ b/command-line.md @@ -1,6 +1,6 @@ # Command line and project structure -**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/master/command-line)** +**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/main/command-line)** Our product owner now wants to _pivot_ by introducing a second application - a command line application. diff --git a/concurrency.md b/concurrency.md index 89cf130d3..c02b5ba86 100644 --- a/concurrency.md +++ b/concurrency.md @@ -1,6 +1,6 @@ # Concurrency -**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/master/concurrency)** +**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/main/concurrency)** Here's the setup: a colleague has written a function, `CheckWebsites`, that checks the status of a list of URLs. diff --git a/context-aware-reader.md b/context-aware-reader.md index 52c7c22ab..b89aad954 100644 --- a/context-aware-reader.md +++ b/context-aware-reader.md @@ -1,6 +1,6 @@ # Context-aware readers -**[You can find all the code here](https://github.com/quii/learn-go-with-tests/tree/master/q-and-a/context-aware-reader)** +**[You can find all the code here](https://github.com/quii/learn-go-with-tests/tree/main/q-and-a/context-aware-reader)** This chapter demonstrates how to test-drive a context aware `io.Reader` as written by Mat Ryer and David Hernandez in [The Pace Dev Blog](https://pace.dev/blog/2020/02/03/context-aware-ioreader-for-golang-by-mat-ryer). diff --git a/context.md b/context.md index a4e04ace8..f7e023081 100644 --- a/context.md +++ b/context.md @@ -1,16 +1,16 @@ # Context -**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/master/context)** +**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/main/context)** -Software often kicks off long-running, resource-intensive processes (often in goroutines). If the action that caused this gets cancelled or fails for some reason you need to stop these processes in a consistent way through your application. +Software often kicks off long-running, resource-intensive processes (often in goroutines). If the action that caused this gets cancelled or fails for some reason you need to stop these processes in a consistent way through your application. -If you don't manage this your snappy Go application that you're so proud of could start having difficult to debug performance problems. +If you don't manage this your snappy Go application that you're so proud of could start having difficult to debug performance problems. In this chapter we'll use the package `context` to help us manage long-running processes. -We're going to start with a classic example of a web server that when hit kicks off a potentially long-running process to fetch some data for it to return in the response. +We're going to start with a classic example of a web server that when hit kicks off a potentially long-running process to fetch some data for it to return in the response. -We will exercise a scenario where a user cancels the request before the data can be retrieved and we'll make sure the process is told to give up. +We will exercise a scenario where a user cancels the request before the data can be retrieved and we'll make sure the process is told to give up. I've set up some code on the happy path to get us started. Here is our server code. @@ -95,17 +95,17 @@ Let's add a new test where we cancel the request before 100 milliseconds and che t.Run("tells store to cancel work if request is cancelled", func(t *testing.T) { store := &SpyStore{response: data} svr := Server(store) - + request := httptest.NewRequest(http.MethodGet, "/", nil) - + cancellingCtx, cancel := context.WithCancel(request.Context()) time.AfterFunc(5 * time.Millisecond, cancel) request = request.WithContext(cancellingCtx) - + response := httptest.NewRecorder() - + svr.ServeHTTP(response, request) - + if !store.cancelled { t.Errorf("store was not told to cancel") } @@ -116,7 +116,7 @@ From the [Go Blog: Context](https://blog.golang.org/context) > The context package provides functions to derive new Context values from existing ones. These values form a tree: when a Context is canceled, all Contexts derived from it are also canceled. -It's important that you derive your contexts so that cancellations are propagated throughout the call stack for a given request. +It's important that you derive your contexts so that cancellations are propagated throughout the call stack for a given request. What we do is derive a new `cancellingCtx` from our `request` which returns us a `cancel` function. We then schedule that function to be called in 5 milliseconds by using `time.AfterFunc`. Finally we use this new context in our request by calling `request.WithContext`. @@ -132,7 +132,7 @@ The test fails as we'd expect. ## Write enough code to make it pass -Remember to be disciplined with TDD. Write the _minimal_ amount of code to make our test pass. +Remember to be disciplined with TDD. Write the _minimal_ amount of code to make our test pass. ```go func Server(store Store) http.HandlerFunc { @@ -143,11 +143,11 @@ func Server(store Store) http.HandlerFunc { } ``` -This makes this test pass but it doesn't feel good does it! We surely shouldn't be cancelling `Store` before we fetch on _every request_. +This makes this test pass but it doesn't feel good does it! We surely shouldn't be cancelling `Store` before we fetch on _every request_. -By being disciplined it highlighted a flaw in our tests, this is a good thing! +By being disciplined it highlighted a flaw in our tests, this is a good thing! -We'll need to update our happy path test to assert that it does not get cancelled. +We'll need to update our happy path test to assert that it does not get cancelled. ```go t.Run("returns data from store", func(t *testing.T) { @@ -162,7 +162,7 @@ t.Run("returns data from store", func(t *testing.T) { if response.Body.String() != data { t.Errorf(`got "%s", want "%s"`, response.Body.String(), data) } - + if store.cancelled { t.Error("it should not have cancelled the store") } @@ -218,7 +218,7 @@ func (s *SpyStore) assertWasNotCancelled() { } ``` -Remember to pass in the `*testing.T` when creating the spy. +Remember to pass in the `*testing.T` when creating the spy. ```go func TestServer(t *testing.T) { @@ -259,11 +259,11 @@ func TestServer(t *testing.T) { } ``` -This approach is ok, but is it idiomatic? +This approach is ok, but is it idiomatic? -Does it make sense for our web server to be concerned with manually cancelling `Store`? What if `Store` also happens to depend on other slow-running processes? We'll have to make sure that `Store.Cancel` correctly propagates the cancellation to all of its dependants. +Does it make sense for our web server to be concerned with manually cancelling `Store`? What if `Store` also happens to depend on other slow-running processes? We'll have to make sure that `Store.Cancel` correctly propagates the cancellation to all of its dependants. -One of the main points of `context` is that it is a consistent way of offering cancellation. +One of the main points of `context` is that it is a consistent way of offering cancellation. [From the go doc](https://golang.org/pkg/context/) @@ -281,7 +281,7 @@ Feeling a bit uneasy? Good. Let's try and follow that approach though and instea We'll have to change our existing tests as their responsibilities are changing. The only thing our handler is responsible for now is making sure it sends a context through to the downstream `Store` and that it handles the error that will come from the `Store` when it is cancelled. -Let's update our `Store` interface to show the new responsibilities. +Let's update our `Store` interface to show the new responsibilities. ```go type Store interface { @@ -333,13 +333,13 @@ func (s *SpyStore) Fetch(ctx context.Context) (string, error) { } ``` -We have to make our spy act like a real method that works with `context`. +We have to make our spy act like a real method that works with `context`. -We are simulating a slow process where we build the result slowly by appending the string, character by character in a goroutine. When the goroutine finishes its work it writes the string to the `data` channel. The goroutine listens for the `ctx.Done` and will stop the work if a signal is sent in that channel. +We are simulating a slow process where we build the result slowly by appending the string, character by character in a goroutine. When the goroutine finishes its work it writes the string to the `data` channel. The goroutine listens for the `ctx.Done` and will stop the work if a signal is sent in that channel. Finally the code uses another `select` to wait for that goroutine to finish its work or for the cancellation to occur. -It's similar to our approach from before, we use Go's concurrency primitives to make two asynchronous processes race each other to determine what we return. +It's similar to our approach from before, we use Go's concurrency primitives to make two asynchronous processes race each other to determine what we return. You'll take a similar approach when writing your own functions and methods that accept a `context` so make sure you understand what's going on. @@ -385,7 +385,7 @@ Our happy path should be... happy. Now we can fix the other test. ## Write the test first -We need to test that we do not write any kind of response on the error case. Sadly `httptest.ResponseRecorder` doesn't have a way of figuring this out so we'll have to role our own spy to test for this. +We need to test that we do not write any kind of response on the error case. Sadly `httptest.ResponseRecorder` doesn't have a way of figuring this out so we'll have to role our own spy to test for this. ```go type SpyResponseWriter struct { @@ -450,13 +450,13 @@ func Server(store Store) http.HandlerFunc { if err != nil { return // todo: log error however you like } - + fmt.Fprint(w, data) } } ``` -We can see after this that the server code has become simplified as it's no longer explicitly responsible for cancellation, it simply passes through `context` and relies on the downstream functions to respect any cancellations that may occur. +We can see after this that the server code has become simplified as it's no longer explicitly responsible for cancellation, it simply passes through `context` and relies on the downstream functions to respect any cancellations that may occur. ## Wrapping up @@ -474,13 +474,13 @@ We can see after this that the server code has become simplified as it's no long > If you use ctx.Value in my (non-existent) company, you’re fired -Some engineers have advocated passing values through `context` as it _feels convenient_. +Some engineers have advocated passing values through `context` as it _feels convenient_. -Convenience is often the cause of bad code. +Convenience is often the cause of bad code. -The problem with `context.Values` is that it's just an untyped map so you have no type-safety and you have to handle it not actually containing your value. You have to create a coupling of map keys from one module to another and if someone changes something things start breaking. +The problem with `context.Values` is that it's just an untyped map so you have no type-safety and you have to handle it not actually containing your value. You have to create a coupling of map keys from one module to another and if someone changes something things start breaking. -In short, **if a function needs some values, put them as typed parameters rather than trying to fetch them from `context.Value`**. This makes is statically checked and documented for everyone to see. +In short, **if a function needs some values, put them as typed parameters rather than trying to fetch them from `context.Value`**. This makes is statically checked and documented for everyone to see. #### But... diff --git a/dependency-injection.md b/dependency-injection.md index 2fc4689e5..11f3dd35d 100644 --- a/dependency-injection.md +++ b/dependency-injection.md @@ -1,6 +1,6 @@ # Dependency Injection -**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/master/di)** +**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/main/di)** It is assumed that you have read the structs section before as some understanding of interfaces will be needed for this. diff --git a/error-types.md b/error-types.md index f973f3160..81772d0ef 100644 --- a/error-types.md +++ b/error-types.md @@ -1,6 +1,6 @@ # Error types -**[You can find all the code here](https://github.com/quii/learn-go-with-tests/tree/master/q-and-a/error-types)** +**[You can find all the code here](https://github.com/quii/learn-go-with-tests/tree/main/q-and-a/error-types)** **Creating your own types for errors can be an elegant way of tidying up your code, making your code easier to use and test.** diff --git a/hello-world.md b/hello-world.md index ab4730c00..cea7060e1 100644 --- a/hello-world.md +++ b/hello-world.md @@ -1,8 +1,8 @@ # Hello, World -**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/master/hello-world)** +**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/main/hello-world)** -It is traditional for your first program in a new language to be [Hello, World](https://en.m.wikipedia.org/wiki/%22Hello,_World!%22_program). +It is traditional for your first program in a new language to be [Hello, World](https://en.m.wikipedia.org/wiki/%22Hello,_World!%22_program). In the [previous chapter](install-go.md#go-environment) we discussed how Go is opinionated as to where you put your files. diff --git a/http-handlers-revisited.md b/http-handlers-revisited.md index 42d905864..c3046a37b 100644 --- a/http-handlers-revisited.md +++ b/http-handlers-revisited.md @@ -1,6 +1,6 @@ # HTTP Handlers Revisited -**[You can find all the code here](https://github.com/quii/learn-go-with-tests/tree/master/q-and-a/http-handlers-revisited)** +**[You can find all the code here](https://github.com/quii/learn-go-with-tests/tree/main/q-and-a/http-handlers-revisited)** This book already has a chapter on [testing a HTTP handler](http-server.md) but this will feature a broader discussion on designing them, so they are simple to test. diff --git a/http-server.md b/http-server.md index 02b8effb0..27e91d6ff 100644 --- a/http-server.md +++ b/http-server.md @@ -1,6 +1,6 @@ # HTTP Server -**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/master/http-server)** +**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/main/http-server)** You have been asked to create a web server where users can track how many games players have won. diff --git a/integers.md b/integers.md index 70f367381..a91bd6e2e 100644 --- a/integers.md +++ b/integers.md @@ -1,6 +1,6 @@ # Integers -**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/master/integers)** +**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/main/integers)** Integers work as you would expect. Let's write an `Add` function to try things out. Create a test file called `adder_test.go` and write this code. diff --git a/io.md b/io.md index d463d7311..75f93a44e 100644 --- a/io.md +++ b/io.md @@ -1,6 +1,6 @@ # IO and sorting -**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/master/io)** +**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/main/io)** [In the previous chapter](json.md) we continued iterating on our application by adding a new endpoint `/league`. Along the way we learned about how to deal with JSON, embedding types and routing. @@ -829,7 +829,7 @@ func NewFileSystemPlayerStore(database io.ReadWriteSeeker) *FileSystemPlayerStor Finally, we can get the amazing payoff we wanted by removing the `Seek` call from `RecordWin`. Yes, it doesn't feel much, but at least it means if we do any other kind of writes we can rely on our `Write` to behave how we need it to. Plus it will now let us test the potentially problematic code separately and fix it. -Let's write the test where we want to update the entire contents of a file with something that is smaller than the original contents. +Let's write the test where we want to update the entire contents of a file with something that is smaller than the original contents. ## Write the test first diff --git a/iteration.md b/iteration.md index 0a6b7674e..76b8f0058 100644 --- a/iteration.md +++ b/iteration.md @@ -1,6 +1,6 @@ # Iteration -**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/master/for)** +**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/main/for)** To do stuff repeatedly in Go, you'll need `for`. In Go there are no `while`, `do`, `until` keywords, you can only use `for`. Which is a good thing! diff --git a/json.md b/json.md index 2fb879e10..76f278d3a 100644 --- a/json.md +++ b/json.md @@ -1,6 +1,6 @@ # JSON, routing & embedding -**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/master/json)** +**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/main/json)** [In the previous chapter](http-server.md) we created a web server to store how many games players have won. diff --git a/maps.md b/maps.md index f6bfbb018..856911d28 100644 --- a/maps.md +++ b/maps.md @@ -1,6 +1,6 @@ # Maps -**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/master/maps)** +**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/main/maps)** In [arrays & slices](arrays-and-slices.md), you saw how to store values in order. Now, we will look at a way to store items by a `key` and look them up quickly. diff --git a/math.md b/math.md index bc15f55fd..aaee4cbce 100644 --- a/math.md +++ b/math.md @@ -1,6 +1,6 @@ # Mathematics -**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/master/math)** +**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/main/math)** For all the power of modern computers to perform huge sums at lightning speed, the average developer rarely uses any mathematics diff --git a/mocking.md b/mocking.md index cd97afb5b..661950138 100644 --- a/mocking.md +++ b/mocking.md @@ -1,6 +1,6 @@ # Mocking -**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/master/mocking)** +**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/main/mocking)** You have been asked to write a program which counts down from 3, printing each number on a new line (with a 1 second pause) and when it reaches zero it will print "Go!" and exit. diff --git a/os-exec.md b/os-exec.md index 4b9a2061e..ffc3c5aad 100644 --- a/os-exec.md +++ b/os-exec.md @@ -1,6 +1,6 @@ # OS Exec -**[You can find all the code here](https://github.com/quii/learn-go-with-tests/tree/master/q-and-a/os-exec)** +**[You can find all the code here](https://github.com/quii/learn-go-with-tests/tree/main/q-and-a/os-exec)** [keith6014](https://www.reddit.com/user/keith6014) asks on [reddit](https://www.reddit.com/r/golang/comments/aaz8ji/testdata_and_function_setup_help/) @@ -15,7 +15,7 @@ A few things - When something is difficult to test, it's often due to the separation of concerns not being quite right -- Don't add "test modes" into your code, instead use [Dependency Injection](/dependency-injection.md) so that you can model your dependencies and separate concerns. +- Don't add "test modes" into your code, instead use [Dependency Injection](/dependency-injection.md) so that you can model your dependencies and separate concerns. I have taken the liberty of guessing what the code might look like @@ -42,7 +42,7 @@ func GetData() string { - It uses `exec.Command` which allows you to execute an external command to the process - We capture the output in `cmd.StdoutPipe` which returns us a `io.ReadCloser` (this will become important) -- The rest of the code is more or less copy and pasted from the [excellent documentation](https://golang.org/pkg/os/exec/#example_Cmd_StdoutPipe). +- The rest of the code is more or less copy and pasted from the [excellent documentation](https://golang.org/pkg/os/exec/#example_Cmd_StdoutPipe). - We capture any output from stdout into an `io.ReadCloser` and then we `Start` the command and then wait for all the data to be read by calling `Wait`. In between those two calls we decode the data into our `Payload` struct. Here is what is contained inside `msg.xml` @@ -73,7 +73,7 @@ Testable code is decoupled and single purpose. To me it feels like there are two 1. Retrieving the raw XML data 2. Decoding the XML data and applying our business logic (in this case `strings.ToUpper` on the ``) -The first part is just copying the example from the standard lib. +The first part is just copying the example from the standard lib. The second part is where we have our business logic and by looking at the code we can see where the "seam" in our logic starts; it's where we get our `io.ReadCloser`. We can use this existing abstraction to separate concerns and make our code testable. @@ -134,6 +134,6 @@ func TestGetData(t *testing.T) { ``` -Here is an example of a unit test for `GetData`. +Here is an example of a unit test for `GetData`. By separating the concerns and using existing abstractions within Go testing our important business logic is a breeze. diff --git a/pointers-and-errors.md b/pointers-and-errors.md index 3c8f5cae8..73284c0ea 100644 --- a/pointers-and-errors.md +++ b/pointers-and-errors.md @@ -1,6 +1,6 @@ # Pointers & errors -**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/master/pointers)** +**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/main/pointers)** We learned about structs in the last section which let us capture a number of values related around a concept. diff --git a/reflection.md b/reflection.md index 88448a577..98794b670 100644 --- a/reflection.md +++ b/reflection.md @@ -1,6 +1,6 @@ # Reflection -**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/master/reflection)** +**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/main/reflection)** [From Twitter](https://twitter.com/peterbourgon/status/1011403901419937792?s=09) @@ -834,7 +834,7 @@ func walk(x interface{}, fn func(input string)) { walk(val.MapIndex(key).Interface(), fn) } case reflect.Chan: - for v, ok := val.Recv(); ok; v, ok = val.Recv() { + for v, ok := val.Recv(); ok; v, ok = val.Recv() { walk(v.Interface(), fn) } } @@ -895,7 +895,7 @@ func walk(x interface{}, fn func(input string)) { walk(val.MapIndex(key).Interface(), fn) } case reflect.Chan: - for v, ok := val.Recv(); ok; v, ok = val.Recv() { + for v, ok := val.Recv(); ok; v, ok = val.Recv() { walk(v.Interface(), fn) } case reflect.Func: diff --git a/roman-numerals.md b/roman-numerals.md index 55eedb5e5..45be55fb4 100644 --- a/roman-numerals.md +++ b/roman-numerals.md @@ -1,6 +1,6 @@ # Roman Numerals -**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/master/roman-numerals)** +**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/main/roman-numerals)** Some companies will ask you to do the [Roman Numeral Kata](http://codingdojo.org/kata/RomanNumerals/) as part of the interview process. This chapter will show how you can tackle it with TDD. diff --git a/select.md b/select.md index ce4f21297..4ab386c92 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/master/select)** +**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/main/select)** 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`. @@ -224,7 +224,7 @@ func ping(url string) chan struct{} { #### `ping` -We have defined a function `ping` which creates a `chan struct{}` and returns it. +We have defined a function `ping` which creates a `chan struct{}` and returns it. In our case, we don't _care_ what type is sent to the channel, _we just want to signal we are done_ and closing the channel works perfectly! diff --git a/structs-methods-and-interfaces.md b/structs-methods-and-interfaces.md index e429113f2..8f483c4bf 100644 --- a/structs-methods-and-interfaces.md +++ b/structs-methods-and-interfaces.md @@ -1,6 +1,6 @@ # Structs, methods & interfaces -**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/master/structs)** +**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/main/structs)** Suppose that we need some geometry code to calculate the perimeter of a rectangle given a height and width. We can write a `Perimeter(width float64, height float64)` function, where `float64` is for floating-point numbers like `123.45`. diff --git a/sync.md b/sync.md index 4954896db..9941bdf2a 100644 --- a/sync.md +++ b/sync.md @@ -1,8 +1,8 @@ # Sync -**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/master/sync)** +**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/main/sync)** -We want to make a counter which is safe to use concurrently. +We want to make a counter which is safe to use concurrently. We'll start with an unsafe counter and verify its behaviour works in a single-threaded environment. @@ -10,7 +10,7 @@ Then we'll exercise it's unsafeness with multiple goroutines trying to use it vi ## Write the test first -We want our API to give us a method to increment the counter and then retrieve its value. +We want our API to give us a method to increment the counter and then retrieve its value. ```go func TestCounter(t *testing.T) { @@ -20,7 +20,7 @@ func TestCounter(t *testing.T) { counter.Inc() counter.Inc() - if counter.Value() != 3 { + if counter.Value() != 3 { t.Errorf("got %d, want %d", counter.Value(), 3) } }) @@ -35,11 +35,11 @@ func TestCounter(t *testing.T) { ## Write the minimal amount of code for the test to run and check the failing test output -Let's define `Counter`. +Let's define `Counter`. ```go type Counter struct { - + } ``` @@ -54,7 +54,7 @@ So to finally make the test run we can define those methods ```go func (c *Counter) Inc() { - + } func (c *Counter) Value() int { @@ -100,7 +100,7 @@ t.Run("incrementing the counter 3 times leaves it at 3", func(t *testing.T) { counter.Inc() counter.Inc() counter.Inc() - + assertCounter(t, counter, 3) }) @@ -114,7 +114,7 @@ func assertCounter(t *testing.T, got Counter, want int) { ## Next steps -That was easy enough but now we have a requirement that it must be safe to use in a concurrent environment. We will need to write a failing test to exercise this. +That was easy enough but now we have a requirement that it must be safe to use in a concurrent environment. We will need to write a failing test to exercise this. ## Write the test first @@ -138,7 +138,7 @@ t.Run("it runs safely concurrently", func(t *testing.T) { }) ``` -This will loop through our `wantedCount` and fire a goroutine to call `counter.Inc()`. +This will loop through our `wantedCount` and fire a goroutine to call `counter.Inc()`. We are using [`sync.WaitGroup`](https://golang.org/pkg/sync/#WaitGroup) which is a convenient way of synchronising concurrent processes. @@ -177,13 +177,13 @@ func (c *Counter) Inc() { } ``` -What this means is any goroutine calling `Inc` will acquire the lock on `Counter` if they are first. All the other goroutines will have to wait for it to be `Unlock`ed before getting access. +What this means is any goroutine calling `Inc` will acquire the lock on `Counter` if they are first. All the other goroutines will have to wait for it to be `Unlock`ed before getting access. If you now re-run the test it should now pass because each goroutine has to wait its turn before making a change. -## I've seen other examples where the `sync.Mutex` is embedded into the struct. +## I've seen other examples where the `sync.Mutex` is embedded into the struct. -You may see examples like this +You may see examples like this ```go type Counter struct { @@ -202,9 +202,9 @@ func (c *Counter) Inc() { } ``` -This _looks_ nice but while programming is a hugely subjective discipline, this is **bad and wrong**. +This _looks_ nice but while programming is a hugely subjective discipline, this is **bad and wrong**. -Sometimes people forget that embedding types means the methods of that type becomes _part of the public interface_; and you often will not want that. Remember that we should be very careful with our public APIs, the moment we make something public is the moment other code can couple themselves to it. We always want to avoid unnecessary coupling. +Sometimes people forget that embedding types means the methods of that type becomes _part of the public interface_; and you often will not want that. Remember that we should be very careful with our public APIs, the moment we make something public is the moment other code can couple themselves to it. We always want to avoid unnecessary coupling. Exposing `Lock` and `Unlock` is at best confusing but at worst potentially very harmful to your software if callers of your type start calling these methods. @@ -227,7 +227,7 @@ A look at the documentation of [`sync.Mutex`](https://golang.org/pkg/sync/#Mutex > A Mutex must not be copied after first use. -When we pass our `Counter` (by value) to `assertCounter` it will try and create a copy of the mutex. +When we pass our `Counter` (by value) to `assertCounter` it will try and create a copy of the mutex. To solve this we should pass in a pointer to our `Counter` instead, so change the signature of `assertCounter` @@ -254,14 +254,14 @@ We've covered a few things from the [sync package](https://golang.org/pkg/sync/) ### When to use locks over channels and goroutines? -[We've previously covered goroutines in the first concurrency chapter](concurrency.md) which let us write safe concurrent code so why would you use locks? +[We've previously covered goroutines in the first concurrency chapter](concurrency.md) which let us write safe concurrent code so why would you use locks? [The go wiki has a page dedicated to this topic; Mutex Or Channel](https://github.com/golang/go/wiki/MutexOrChannel) > A common Go newbie mistake is to over-use channels and goroutines just because it's possible, and/or because it's fun. Don't be afraid to use a sync.Mutex if that fits your problem best. Go is pragmatic in letting you use the tools that solve your problem best and not forcing you into one style of code. Paraphrasing: -- **Use channels when passing ownership of data** +- **Use channels when passing ownership of data** - **Use mutexes for managing state** ### go vet diff --git a/time.md b/time.md index ea84b3a00..978ff62f3 100644 --- a/time.md +++ b/time.md @@ -1,12 +1,12 @@ # Time -**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/master/time)** +**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/main/time)** The product owner wants us to expand the functionality of our command line application by helping a group of people play Texas-Holdem Poker. ## Just enough information on poker -You won't need to know much about poker, only that at certain time intervals all the players need to be informed of a steadily increasing "blind" value. +You won't need to know much about poker, only that at certain time intervals all the players need to be informed of a steadily increasing "blind" value. Our application will help keep track of when the blind should go up, and how much it should be. @@ -86,7 +86,7 @@ t.Run("it schedules printing of blind values", func(t *testing.T) { cli := poker.NewCLI(playerStore, in, blindAlerter) cli.PlayPoker() - + if len(blindAlerter.alerts) != 1 { t.Fatal("expected a blind alert to be scheduled") } @@ -141,7 +141,7 @@ And then add it to the constructor func NewCLI(store PlayerStore, in io.Reader, alerter BlindAlerter) *CLI ``` -Your other tests will now fail as they don't have a `BlindAlerter` passed in to `NewCLI`. +Your other tests will now fail as they don't have a `BlindAlerter` passed in to `NewCLI`. Spying on BlindAlerter is not relevant for the other tests so in the test file add @@ -403,7 +403,7 @@ func StdOutAlerter(duration time.Duration, amount int) { } ``` -Remember that any _type_ can implement an interface, not just `structs`. If you are making a library that exposes an interface with one function defined it is a common idiom to also expose a `MyInterfaceFunc` type. +Remember that any _type_ can implement an interface, not just `structs`. If you are making a library that exposes an interface with one function defined it is a common idiom to also expose a `MyInterfaceFunc` type. This type will be a `func` which will also implement your interface. That way users of your interface have the option to implement your interface with just a function; rather than having to create an empty `struct` type. @@ -419,13 +419,13 @@ Before running you might want to change the `blindTime` increment in `CLI` to be You should see it print the blind values as we'd expect every 10 seconds. Notice how you can still type `Shaun wins` into the CLI and it will stop the program how we'd expect. -The game won't always be played with 5 people so we need to prompt the user to enter a number of players before the game starts. +The game won't always be played with 5 people so we need to prompt the user to enter a number of players before the game starts. ## Write the test first To check we are prompting for the number of players we'll want to record what is written to StdOut. We've done this a few times now, we know that `os.Stdout` is an `io.Writer` so we can check what is written if we use dependency injection to pass in a `bytes.Buffer` in our test and see what our code will write. -We don't care about our other collaborators in this test just yet so we've made some dummies in our test file. +We don't care about our other collaborators in this test just yet so we've made some dummies in our test file. We should be a little wary that we now have 4 dependencies for `CLI`, that feels like maybe it is starting to have too many responsibilities. Let's live with it for now and see if a refactoring emerges as we add this new functionality. @@ -471,7 +471,7 @@ We have a new dependency so we'll have to update `NewCLI` func NewCLI(store PlayerStore, in io.Reader, out io.Writer, alerter BlindAlerter) *CLI ``` -Now the _other_ tests will fail to compile because they don't have an `io.Writer` being passed into `NewCLI`. +Now the _other_ tests will fail to compile because they don't have an `io.Writer` being passed into `NewCLI`. Add `dummyStdout` for the other tests. @@ -570,7 +570,7 @@ t.Run("it prompts the user to enter the number of players", func(t *testing.T) { }) ``` -Ouch! A lot of changes. +Ouch! A lot of changes. - We remove our dummy for StdIn and instead send in a mocked version representing our user entering 7 - We also remove our dummy on the blind alerter so we can see that the number of players has had an effect on the scheduling @@ -597,7 +597,7 @@ Remember, we are free to commit whatever sins we need to make this work. Once we ```go func (cli *CLI) PlayPoker() { fmt.Fprint(cli.out, PlayerPrompt) - + numberOfPlayers, _ := strconv.Atoi(cli.readLine()) cli.scheduleBlindAlerts(numberOfPlayers) @@ -626,18 +626,18 @@ While our new test has been fixed, a lot of others have failed because now our s ## Refactor -This all feels a bit horrible right? Let's **listen to our tests**. +This all feels a bit horrible right? Let's **listen to our tests**. - In order to test that we are scheduling some alerts we set up 4 different dependencies. Whenever you have a lot of dependencies for a _thing_ in your system, it implies it's doing too much. Visually we can see it in how cluttered our test is. -- To me it feels like **we need to make a cleaner abstraction between reading user input and the business logic we want to do** -- A better test would be _given this user input, do we call a new type `Game` with the correct number of players_. +- To me it feels like **we need to make a cleaner abstraction between reading user input and the business logic we want to do** +- A better test would be _given this user input, do we call a new type `Game` with the correct number of players_. - We would then extract the testing of the scheduling into the tests for our new `Game`. We can refactor toward our `Game` first and our test should continue to pass. Once we've made the structural changes we want we can think about how we can refactor the tests to reflect our new separation of concerns Remember when making changes in refactoring try to keep them as small as possible and keep re-running the tests. -Try it yourself first. Think about the boundaries of what a `Game` would offer and what our `CLI` should be doing. +Try it yourself first. Think about the boundaries of what a `Game` would offer and what our `CLI` should be doing. For now **don't** change the external interface of `NewCLI` as we don't want to change the test code and the client code at the same time as that is too much to juggle and we could end up breaking things. @@ -713,9 +713,9 @@ From a "domain" perspective: - We want to `Start` a `Game`, indicating how many people are playing - We want to `Finish` a `Game`, declaring the winner -The new `Game` type encapsulates this for us. +The new `Game` type encapsulates this for us. -With this change we've passed `BlindAlerter` and `PlayerStore` to `Game` as it is now responsible for alerting and storing results. +With this change we've passed `BlindAlerter` and `PlayerStore` to `Game` as it is now responsible for alerting and storing results. Our `CLI` is now just concerned with: @@ -774,7 +774,7 @@ cli := poker.NewCLI(os.Stdin, os.Stdout, game) cli.PlayPoker() ``` -Now that we have extracted out `Game` we should move our game specific assertions into tests separate from CLI. +Now that we have extracted out `Game` we should move our game specific assertions into tests separate from CLI. This is just an exercise in copying our `CLI` tests but with less dependencies @@ -831,9 +831,9 @@ func TestGame_Finish(t *testing.T) { } ``` -The intent behind what happens when a game of poker starts is now much clearer. +The intent behind what happens when a game of poker starts is now much clearer. -Make sure to also move over the test for when the game ends. +Make sure to also move over the test for when the game ends. Once we are happy we have moved the tests over for game logic we can simplify our CLI tests so they reflect our intended responsibilities clearer @@ -899,11 +899,11 @@ Here is an example of one of the tests being fixed; try and do the rest yourself }) ``` -Now that we have a clean separation of concerns, checking edge cases around IO in our `CLI` should be easier. +Now that we have a clean separation of concerns, checking edge cases around IO in our `CLI` should be easier. We need to address the scenario where a user puts a non numeric value when prompted for the number of players: -Our code should not start the game and it should print a handy error to the user and then exit. +Our code should not start the game and it should print a handy error to the user and then exit. ## Write the test first @@ -917,7 +917,7 @@ t.Run("it prints an error when a non numeric value is entered and does not start cli := poker.NewCLI(in, stdout, game) cli.PlayPoker() - + if game.StartCalled { t.Errorf("game should not have started") } @@ -945,7 +945,7 @@ if err != nil { } ``` -Next we need to inform the user of what they did wrong so we'll assert on what is printed to `stdout`. +Next we need to inform the user of what they did wrong so we'll assert on what is printed to `stdout`. ## Write the test first @@ -984,7 +984,7 @@ if err != nil { ## Refactor -Now refactor the message into a constant like `PlayerPrompt` +Now refactor the message into a constant like `PlayerPrompt` ```go wantPrompt := poker.PlayerPrompt + poker.BadPlayerInputErrMsg @@ -1013,7 +1013,7 @@ Using the vararg syntax (`...string`) is handy here because we need to assert on Use this helper in both of the tests where we assert on messages sent to the user. -There are a number of tests that could be helped with some `assertX` functions so practice your refactoring by cleaning up our tests so they read nicely. +There are a number of tests that could be helped with some `assertX` functions so practice your refactoring by cleaning up our tests so they read nicely. Take some time and think about the value of some of the tests we've driven out. Remember we don't want more tests than necessary, can you refactor/remove some of them _and still be confident it all works_ ? @@ -1062,7 +1062,7 @@ func TestCLI(t *testing.T) { }) } ``` -The tests now reflect the main capabilities of CLI, it is able to read user input in terms of how many people are playing and who won and handles when a bad value is entered for number of players. By doing this it is clear to the reader what `CLI` does, but also what it doesn't do. +The tests now reflect the main capabilities of CLI, it is able to read user input in terms of how many people are playing and who won and handles when a bad value is entered for number of players. By doing this it is clear to the reader what `CLI` does, but also what it doesn't do. What happens if instead of putting `Ruth wins` the user puts in `Lloyd is a killer` ? @@ -1074,7 +1074,7 @@ Finish this chapter by writing a test for this scenario and making it pass. For the past 5 chapters we have slowly TDD'd a fair amount of code -- We have two applications, a command line application and a web server. +- We have two applications, a command line application and a web server. - Both these applications rely on a `PlayerStore` to record winners - The web server can also display a league table of who is winning the most games - The command line app helps players play a game of poker by tracking what the current blind value is. @@ -1085,22 +1085,22 @@ A very handy way of scheduling a function call after a specific duration. It is Some of my favourites are -- `time.After(duration)` returns a `chan Time` when the duration has expired. So if you wish to do something _after_ a specific time, this can help. -- `time.NewTicker(duration)` returns a `Ticker` which is similar to the above in that it returns a channel but this one "ticks" every duration, rather than just once. This is very handy if you want to execute some code every `N duration`. +- `time.After(duration)` returns a `chan Time` when the duration has expired. So if you wish to do something _after_ a specific time, this can help. +- `time.NewTicker(duration)` returns a `Ticker` which is similar to the above in that it returns a channel but this one "ticks" every duration, rather than just once. This is very handy if you want to execute some code every `N duration`. ### More examples of good separation of concerns -_Generally_ it is good practice to separate the responsibilities of dealing with user input and responses away from domain code. You see that here in our command line application and also our web server. +_Generally_ it is good practice to separate the responsibilities of dealing with user input and responses away from domain code. You see that here in our command line application and also our web server. -Our tests got messy. We had too many assertions (check this input, schedules these alerts, etc) and too many dependencies. We could visually see it was cluttered; it is **so important to listen to your tests**. +Our tests got messy. We had too many assertions (check this input, schedules these alerts, etc) and too many dependencies. We could visually see it was cluttered; it is **so important to listen to your tests**. - If your tests look messy try and refactor them. - If you've done this and they're still a mess it is very likely pointing to a flaw in your design - This is one of the real strengths of tests. -Even though the tests and the production code was a bit cluttered we could freely refactor backed by our tests. +Even though the tests and the production code was a bit cluttered we could freely refactor backed by our tests. -Remember when you get into these situations to always take small steps and re-run the tests after every change. +Remember when you get into these situations to always take small steps and re-run the tests after every change. It would've been dangerous to refactor both the test code _and_ the production code at the same time, so we first refactored the production code (in the current state we couldn't improve the tests much) without changing its interface so we could rely on our tests as much as we could while changing things. _Then_ we refactored the tests after the design improved. diff --git a/websockets.md b/websockets.md index dccf729d5..d5da91cce 100644 --- a/websockets.md +++ b/websockets.md @@ -1,15 +1,15 @@ # WebSockets -**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/master/websockets)** +**[You can find all the code for this chapter here](https://github.com/quii/learn-go-with-tests/tree/main/websockets)** -In this chapter we'll learn how to use WebSockets to improve our application. +In this chapter we'll learn how to use WebSockets to improve our application. ## Project recap We have two applications in our poker codebase - *Command line app*. Prompts the user to enter the number of players in a game. From then on informs the players of what the "blind bet" value is, which increases over time. At any point a user can enter `"{Playername} wins"` to finish the game and record the victor in a store. -- *Web app*. Allows users to record winners of games and displays a league table. Shares the same store as the command line app. +- *Web app*. Allows users to record winners of games and displays a league table. Shares the same store as the command line app. ## Next steps @@ -19,27 +19,27 @@ On the face of it, it sounds quite simple but as always we must emphasise taking First of all we will need to serve HTML. So far all of our HTTP endpoints have returned either plaintext or JSON. We _could_ use the same techniques we know (as they're all ultimately strings) but we can also use the [html/template](https://golang.org/pkg/html/template/) package for a cleaner solution. -We also need to be able to asynchronously send messages to the user saying `The blind is now *y*` without having to refresh the browser. We can use [WebSockets](https://en.wikipedia.org/wiki/WebSocket) to facilitate this. +We also need to be able to asynchronously send messages to the user saying `The blind is now *y*` without having to refresh the browser. We can use [WebSockets](https://en.wikipedia.org/wiki/WebSocket) to facilitate this. > WebSocket is a computer communications protocol, providing full-duplex communication channels over a single TCP connection -Given we are taking on a number of techniques it's even more important we do the smallest amount of useful work possible first and then iterate. +Given we are taking on a number of techniques it's even more important we do the smallest amount of useful work possible first and then iterate. -For that reason the first thing we'll do is create a web page with a form for the user to record a winner. Rather than using a plain form, we will use WebSockets to send that data to our server for it to record. +For that reason the first thing we'll do is create a web page with a form for the user to record a winner. Rather than using a plain form, we will use WebSockets to send that data to our server for it to record. -After that we'll work on the blind alerts by which point we will have a bit of infrastructure code set up. +After that we'll work on the blind alerts by which point we will have a bit of infrastructure code set up. ### What about tests for the JavaScript ? -There will be some JavaScript written to do this but I won't go in to writing tests. +There will be some JavaScript written to do this but I won't go in to writing tests. -It is of course possible but for the sake of brevity I won't be including any explanations for it. +It is of course possible but for the sake of brevity I won't be including any explanations for it. Sorry folks. Lobby O'Reilly to pay me to make a "Learn JavaScript with tests". ## Write the test first -First thing we need to do is serve up some HTML to users when they hit `/game`. +First thing we need to do is serve up some HTML to users when they hit `/game`. Here's a reminder of the pertinent code in our web server @@ -93,7 +93,7 @@ func TestGame(t *testing.T) { ## Write enough code to make it pass -Our server has a router setup so it's relatively easy to fix. +Our server has a router setup so it's relatively easy to fix. To our router add @@ -111,9 +111,9 @@ func (p *PlayerServer) game(w http.ResponseWriter, r *http.Request) { ## Refactor -The server code is already fine due to us slotting in more code into the existing well-factored code very easily. +The server code is already fine due to us slotting in more code into the existing well-factored code very easily. -We can tidy up the test a little by adding a test helper function `newGameRequest` to make the request to `/game`. Try writing this yourself. +We can tidy up the test a little by adding a test helper function `newGameRequest` to make the request to `/game`. Try writing this yourself. ```go func TestGame(t *testing.T) { @@ -167,10 +167,10 @@ Now we need to make the endpoint return some HTML, here it is ``` -We have a very simple web page - +We have a very simple web page + - A text input for the user to enter the winner into - - A button they can click to declare the winner. + - A button they can click to declare the winner. - Some JavaScript to open a WebSocket connection to our server and handle the submit button being pressed `WebSocket` is built into most modern browsers so we don't need to worry about bringing in any libraries. The web page won't work for older browsers, but we're ok with that for this scenario. @@ -181,9 +181,9 @@ There are a few ways. As has been emphasised throughout the book, it is importan 1. Write a browser based test, using something like Selenium. These tests are the most "realistic" of all approaches because they start an actual web browser of some kind and simulates a user interacting with it. These tests can give you a lot of confidence your system works but are more difficult to write than unit tests and much slower to run. For the purposes of our product this is overkill. 2. Do an exact string match. This _can_ be ok but these kind of tests end up being very brittle. The moment someone changes the markup you will have a test failing when in practice nothing has _actually broken_. -3. Check we call the correct template. We will be using a templating library from the standard lib to serve the HTML (discussed shortly) and we could inject in the _thing_ to generate the HTML and spy on its call to check we're doing it right. This would have an impact on our code's design but doesn't actually test a great deal; other than we're calling it with the correct template file. Given we will only have the one template in our project the chance of failure here seems low. +3. Check we call the correct template. We will be using a templating library from the standard lib to serve the HTML (discussed shortly) and we could inject in the _thing_ to generate the HTML and spy on its call to check we're doing it right. This would have an impact on our code's design but doesn't actually test a great deal; other than we're calling it with the correct template file. Given we will only have the one template in our project the chance of failure here seems low. -So in the book "Learn Go with Tests" for the first time, we're not going to write a test. +So in the book "Learn Go with Tests" for the first time, we're not going to write a test. Put the markup in a file called `game.html` @@ -197,24 +197,24 @@ func (p *PlayerServer) game(w http.ResponseWriter, r *http.Request) { http.Error(w, fmt.Sprintf("problem loading template %s", err.Error()), http.StatusInternalServerError) return } - + tmpl.Execute(w, nil) } ``` -[`html/template`](https://golang.org/pkg/html/template/) is a Go package for creating HTML. In our case we call `template.ParseFiles`, giving the path of our html file. Assuming there is no error you can then `Execute` the template, which writes it to an `io.Writer`. In our case we want it to `Write` to the internet, so we give it our `http.ResponseWriter`. +[`html/template`](https://golang.org/pkg/html/template/) is a Go package for creating HTML. In our case we call `template.ParseFiles`, giving the path of our html file. Assuming there is no error you can then `Execute` the template, which writes it to an `io.Writer`. In our case we want it to `Write` to the internet, so we give it our `http.ResponseWriter`. -As we have not written a test, it would be prudent to manually test our web server just to make sure things are working as we'd hope. Go to `cmd/webserver` and run the `main.go` file. Visit `http://localhost:5000/game`. +As we have not written a test, it would be prudent to manually test our web server just to make sure things are working as we'd hope. Go to `cmd/webserver` and run the `main.go` file. Visit `http://localhost:5000/game`. -You _should_ have got an error about not being able to find the template. You can either change the path to be relative to your folder, or you can have a copy of the `game.html` in the `cmd/webserver` directory. I chose to create a symlink (`ln -s ../../game.html game.html`) to the file inside the root of the project so if I make changes they are reflected when running the server. +You _should_ have got an error about not being able to find the template. You can either change the path to be relative to your folder, or you can have a copy of the `game.html` in the `cmd/webserver` directory. I chose to create a symlink (`ln -s ../../game.html game.html`) to the file inside the root of the project so if I make changes they are reflected when running the server. -If you make this change and run again you should see our UI. +If you make this change and run again you should see our UI. Now we need to test that when we get a string over a WebSocket connection to our server that we declare it as a winner of a game. ## Write the test first -For the first time we are going to use an external library so that we can work with WebSockets. +For the first time we are going to use an external library so that we can work with WebSockets. Run `go get github.com/gorilla/websocket` @@ -245,9 +245,9 @@ t.Run("when we get a message over a websocket it is a winner of a game", func(t Make sure that you have an import for the `websocket` library. My IDE automatically did it for me, so should yours. -To test what happens from the browser we have to open up our own WebSocket connection and write to it. +To test what happens from the browser we have to open up our own WebSocket connection and write to it. -Our previous tests around our server just called methods on our server but now we need to have a persistent connection to our server. To do that we use `httptest.NewServer` which takes a `http.Handler` and will spin it up and listen for connections. +Our previous tests around our server just called methods on our server but now we need to have a persistent connection to our server. To do that we use `httptest.NewServer` which takes a `http.Handler` and will spin it up and listen for connections. Using `websocket.DefaultDialer.Dial` we try to dial in to our server and then we'll try and send a message with our `winner`. @@ -260,7 +260,7 @@ Finally we assert on the player store to check the winner was recorded. server_test.go:124: could not open a ws connection on ws://127.0.0.1:55838/ws websocket: bad handshake ``` -We have not changed our server to accept WebSocket connections on `/ws` so we're not shaking hands yet. +We have not changed our server to accept WebSocket connections on `/ws` so we're not shaking hands yet. ## Write enough code to make it pass @@ -306,24 +306,24 @@ func (p *PlayerServer) webSocket(w http.ResponseWriter, r *http.Request) { (Yes, we're ignoring a lot of errors right now!) -`conn.ReadMessage()` blocks on waiting for a message on the connection. Once we get one we use it to `RecordWin`. This would finally close the WebSocket connection. +`conn.ReadMessage()` blocks on waiting for a message on the connection. Once we get one we use it to `RecordWin`. This would finally close the WebSocket connection. -If you try and run the test, it's still failing. +If you try and run the test, it's still failing. -The issue is timing. There is a delay between our WebSocket connection reading the message and recording the win and our test finishes before it happens. You can test this by putting a short `time.Sleep` before the final assertion. +The issue is timing. There is a delay between our WebSocket connection reading the message and recording the win and our test finishes before it happens. You can test this by putting a short `time.Sleep` before the final assertion. Let's go with that for now but acknowledge that putting in arbitrary sleeps into tests **is very bad practice**. ```go time.Sleep(10 * time.Millisecond) AssertPlayerWin(t, store, winner) -``` +``` ## Refactor -We committed many sins to make this test work both in the server code and the test code but remember this is the easiest way for us to work. +We committed many sins to make this test work both in the server code and the test code but remember this is the easiest way for us to work. -We have nasty, horrible, _working_ software backed by a test, so now we are free to make it nice and know we won't break anything accidentally. +We have nasty, horrible, _working_ software backed by a test, so now we are free to make it nice and know we won't break anything accidentally. Let's start with the server code. @@ -383,7 +383,7 @@ func (p *PlayerServer) game(w http.ResponseWriter, r *http.Request) { } ``` -By changing the signature of `NewPlayerServer` we now have compilation problems. Try and fix them yourself or refer to the source code if you struggle. +By changing the signature of `NewPlayerServer` we now have compilation problems. Try and fix them yourself or refer to the source code if you struggle. For the test code I made a helper called `mustMakePlayerServer(t *testing.T, store PlayerStore) *PlayerServer` so that I could hide the error noise away from the tests. @@ -395,7 +395,7 @@ func mustMakePlayerServer(t *testing.T, store PlayerStore) *PlayerServer { } return server } -``` +``` Similarly I created another helper `mustDialWS` so that I could hide nasty error noise when creating the WebSocket connection. @@ -409,7 +409,7 @@ func mustDialWS(t *testing.T, url string) *websocket.Conn { return ws } -``` +``` Finally in our test code we can create a helper to tidy up sending messages @@ -505,11 +505,11 @@ First of all update `game.html` to update our client side code for the new requi ``` -The main changes is bringing in a section to enter the number of players and a section to display the blind value. We have a little logic to show/hide the user interface depending on the stage of the game. +The main changes is bringing in a section to enter the number of players and a section to display the blind value. We have a little logic to show/hide the user interface depending on the stage of the game. Any message we receive via `conn.onmessage` we assume to be blind alerts and so we set the `blindContainer.innerText` accordingly. -How do we go about sending the blind alerts? In the previous chapter we introduced the idea of `Game` so our CLI code could call a `Game` and everything else would be taken care of including scheduling blind alerts. This turned out to be a good separation of concern. +How do we go about sending the blind alerts? In the previous chapter we introduced the idea of `Game` so our CLI code could call a `Game` and everything else would be taken care of including scheduling blind alerts. This turned out to be a good separation of concern. ```go type Game interface { @@ -547,9 +547,9 @@ func StdOutAlerter(duration time.Duration, amount int) { } ``` -This works in CLI because we _always want to send the alerts to `os.Stdout`_ but this won't work for our web server. For every request we get a new `http.ResponseWriter` which we then upgrade to `*websocket.Conn`. So we can't know when constructing our dependencies where our alerts need to go. +This works in CLI because we _always want to send the alerts to `os.Stdout`_ but this won't work for our web server. For every request we get a new `http.ResponseWriter` which we then upgrade to `*websocket.Conn`. So we can't know when constructing our dependencies where our alerts need to go. -For that reason we need to change `BlindAlerter.ScheduleAlertAt` so that it takes a destination for the alerts so that we can re-use it in our webserver. +For that reason we need to change `BlindAlerter.ScheduleAlertAt` so that it takes a destination for the alerts so that we can re-use it in our webserver. Open BlindAlerter.go and add the parameter `to io.Writer` @@ -575,9 +575,9 @@ func Alerter(duration time.Duration, amount int, to io.Writer) { } ``` -If you try and compile, it will fail in `TexasHoldem` because it is calling `ScheduleAlertAt` without a destination, to get things compiling again _for now_ hard-code it to `os.Stdout`. +If you try and compile, it will fail in `TexasHoldem` because it is calling `ScheduleAlertAt` without a destination, to get things compiling again _for now_ hard-code it to `os.Stdout`. -Try and run the tests and they will fail because `SpyBlindAlerter` no longer implements `BlindAlerter`, fix this by updating the signature of `ScheduleAlertAt`, run the tests and we should still be green. +Try and run the tests and they will fail because `SpyBlindAlerter` no longer implements `BlindAlerter`, fix this by updating the signature of `ScheduleAlertAt`, run the tests and we should still be green. It doesn't make any sense for `TexasHoldem` to know where to send blind alerts. Let's now update `Game` so that when you start a game you declare _where_ the alerts should go. @@ -598,17 +598,17 @@ If you've got everything right, everything should be green! Now we can try and u ## Write the test first -The requirements of `CLI` and `Server` are the same! It's just the delivery mechanism is different. +The requirements of `CLI` and `Server` are the same! It's just the delivery mechanism is different. Let's take a look at our `CLI` test for inspiration. ```go t.Run("start game with 3 players and finish game with 'Chris' as winner", func(t *testing.T) { game := &GameSpy{} - + out := &bytes.Buffer{} in := userSends("3", "Chris wins") - + poker.NewCLI(in, out, game).PlayPoker() assertMessagesSentToUser(t, out, poker.PlayerPrompt) @@ -642,7 +642,7 @@ t.Run("start a game with 3 players and declare Ruth the winner", func(t *testing - As discussed we create a spy `Game` and pass it into `mustMakePlayerServer` (be sure to update the helper to support this). - We then send the web socket messages for a game. -- Finally we assert that the game is started and finished with what we expect. +- Finally we assert that the game is started and finished with what we expect. ## Try to run the test @@ -664,13 +664,13 @@ The final error is where we are trying to pass in `Game` to `NewPlayerServer` bu ## Write the minimal amount of code for the test to run and check the failing test output -Just add it as an argument for now just to get the test running +Just add it as an argument for now just to get the test running ```go func NewPlayerServer(store PlayerStore, game Game) (*PlayerServer, error) { ``` -Finally! +Finally! ``` === RUN TestGame/start_a_game_with_3_players_and_declare_Ruth_the_winner @@ -683,7 +683,7 @@ FAIL ## Write enough code to make it pass -We need to add `Game` as a field to `PlayerServer` so that it can use it when it gets requests. +We need to add `Game` as a field to `PlayerServer` so that it can use it when it gets requests. ```go type PlayerServer struct { @@ -709,7 +709,7 @@ func NewPlayerServer(store PlayerStore, game Game) (*PlayerServer, error) { } p.game = game - + // etc ``` @@ -728,9 +728,9 @@ func (p *PlayerServer) webSocket(w http.ResponseWriter, r *http.Request) { } ``` -Hooray! The tests pass. +Hooray! The tests pass. -We are not going to send the blind messages anywhere _just yet_ as we need to have a think about that. When we call `game.Start` we send in `ioutil.Discard` which will just discard any messages written to it. +We are not going to send the blind messages anywhere _just yet_ as we need to have a think about that. When we call `game.Start` we send in `ioutil.Discard` which will just discard any messages written to it. For now start the web server up. You'll need to update the `main.go` to pass a `Game` to the `PlayerServer` @@ -762,9 +762,9 @@ func main() { } ``` -Discounting the fact we're not getting blind alerts yet, the app does work! We've managed to re-use `Game` with `PlayerServer` and it has taken care of all the details. Once we figure out how to send our blind alerts through to the web sockets rather than discarding them it _should_ all work. +Discounting the fact we're not getting blind alerts yet, the app does work! We've managed to re-use `Game` with `PlayerServer` and it has taken care of all the details. Once we figure out how to send our blind alerts through to the web sockets rather than discarding them it _should_ all work. -Before that though, let's tidy up some code. +Before that though, let's tidy up some code. ## Refactor @@ -809,21 +809,21 @@ func (p *PlayerServer) webSocket(w http.ResponseWriter, r *http.Request) { } ``` -Once we figure out how to not discard the blind messages we're done. +Once we figure out how to not discard the blind messages we're done. -### Let's _not_ write a test! +### Let's _not_ write a test! -Sometimes when we're not sure how to do something, it's best just to play around and try things out! Make sure your work is committed first because once we've figured out a way we should drive it through a test. +Sometimes when we're not sure how to do something, it's best just to play around and try things out! Make sure your work is committed first because once we've figured out a way we should drive it through a test. -The problematic line of code we have is +The problematic line of code we have is ```go p.game.Start(numberOfPlayers, ioutil.Discard) //todo: Don't discard the blinds messages! ``` -We need to pass in an `io.Writer` for the game to write the blind alerts to. +We need to pass in an `io.Writer` for the game to write the blind alerts to. -Wouldn't it be nice if we could pass in our `playerServerWS` from before? It's our wrapper around our WebSocket so it _feels_ like we should be able to send that to our `Game` to send messages to. +Wouldn't it be nice if we could pass in our `playerServerWS` from before? It's our wrapper around our WebSocket so it _feels_ like we should be able to send that to our `Game` to send messages to. Give it a go: @@ -833,7 +833,7 @@ func (p *PlayerServer) webSocket(w http.ResponseWriter, r *http.Request) { numberOfPlayersMsg := ws.WaitForMsg() numberOfPlayers, _ := strconv.Atoi(numberOfPlayersMsg) - p.game.Start(numberOfPlayers, ws) + p.game.Start(numberOfPlayers, ws) //etc... ``` @@ -866,17 +866,17 @@ Beforehand edit `TexasHoldem` so that the blind increment time is shorter so you blindIncrement := time.Duration(5+numberOfPlayers) * time.Second // (rather than a minute) ``` -You should see it working! The blind amount increments in the browser as if by magic. +You should see it working! The blind amount increments in the browser as if by magic. -Now let's revert the code and think how to test it. In order to _implement_ it all we did was pass through to `StartGame` was `playerServerWS` rather than `ioutil.Discard` so that might make you think we should perhaps spy on the call to verify it works. +Now let's revert the code and think how to test it. In order to _implement_ it all we did was pass through to `StartGame` was `playerServerWS` rather than `ioutil.Discard` so that might make you think we should perhaps spy on the call to verify it works. -Spying is great and helps us check implementation details but we should always try and favour testing the _real_ behaviour if we can because when you decide to refactor it's often spy tests that start failing because they are usually checking implementation details that you're trying to change. +Spying is great and helps us check implementation details but we should always try and favour testing the _real_ behaviour if we can because when you decide to refactor it's often spy tests that start failing because they are usually checking implementation details that you're trying to change. Our test currently opens a websocket connection to our running server and sends messages to make it do things. Equally we should be able to test the messages our server sends back over the websocket connection. ## Write the test first -We'll edit our existing test. +We'll edit our existing test. Currently our `GameSpy` does not send any data to `out` when you call `Start`. We should change it so we can configure it to send a canned message and then we can check that message gets sent to the websocket. This should give us confidence that we have configured things correctly whilst still exercising the real behaviour we want. @@ -903,7 +903,7 @@ func (g *GameSpy) Start(numberOfPlayers int, out io.Writer) { } ``` -This now means when we exercise `PlayerServer` when it tries to `Start` the game it should end up sending messages through the websocket if things are working right. +This now means when we exercise `PlayerServer` when it tries to `Start` the game it should end up sending messages through the websocket if things are working right. Finally we can update the test @@ -939,7 +939,7 @@ t.Run("start a game with 3 players, send some blind alerts down WS and declare R ## Try to run the test -You should find the test hangs forever. This is because `ws.ReadMessage()` will block until it gets a message, which it never will. +You should find the test hangs forever. This is because `ws.ReadMessage()` will block until it gets a message, which it never will. ## Write the minimal amount of code for the test to run and check the failing test output @@ -965,9 +965,9 @@ func within(t *testing.T, d time.Duration, assert func()) { } ``` -What `within` does is take a function `assert` as an argument and then runs it in a go routine. If/When the function finishes it will signal it is done via the `done` channel. +What `within` does is take a function `assert` as an argument and then runs it in a go routine. If/When the function finishes it will signal it is done via the `done` channel. -While that happens we use a `select` statement which lets us wait for a channel to send a message. From here it is a race between the `assert` function and `time.After` which will send a signal when the duration has occurred. +While that happens we use a `select` statement which lets us wait for a channel to send a message. From here it is a race between the `assert` function and `time.After` which will send a signal when the duration has occurred. Finally I made a helper function for our assertion just to make things a bit neater @@ -1005,7 +1005,7 @@ t.Run("start a game with 3 players, send some blind alerts down WS and declare R }) ``` -Now if you run the test... +Now if you run the test... ``` === RUN TestGame @@ -1035,7 +1035,7 @@ func (p *PlayerServer) webSocket(w http.ResponseWriter, r *http.Request) { ## Refactor -The server code was a very small change so there's not a lot to change here but the test code still has a `time.Sleep` call because we have to wait for our server to do its work asynchronously. +The server code was a very small change so there's not a lot to change here but the test code still has a `time.Sleep` call because we have to wait for our server to do its work asynchronously. We can refactor our helpers `assertGameStartedWith` and `assertFinishCalledWith` so that they can retry their assertions for a short period before failing. @@ -1071,9 +1071,9 @@ func retryUntil(d time.Duration, f func() bool) bool { ## Wrapping up -Our application is now complete. A game of poker can be started via a web browser and the users are informed of the blind bet value as time goes by via WebSockets. When the game finishes they can record the winner which is persisted using code we wrote a few chapters ago. The players can find out who is the best (or luckiest) poker player using the website's `/league` endpoint. +Our application is now complete. A game of poker can be started via a web browser and the users are informed of the blind bet value as time goes by via WebSockets. When the game finishes they can record the winner which is persisted using code we wrote a few chapters ago. The players can find out who is the best (or luckiest) poker player using the website's `/league` endpoint. -Through the journey we have made mistakes but with the TDD flow we have never been very far away from working software. We were free to keep iterating and experimenting. +Through the journey we have made mistakes but with the TDD flow we have never been very far away from working software. We were free to keep iterating and experimenting. The final chapter will retrospect on the approach, the design we've arrived at and tie up some loose ends. @@ -1081,11 +1081,11 @@ We covered a few things in this chapter ### WebSockets -- Convenient way of sending messages between clients and servers that does not require the client to keep polling the server. Both the client and server code we have is very simple. -- Trivial to test, but you have to be wary of the asynchronous nature of the tests +- Convenient way of sending messages between clients and servers that does not require the client to keep polling the server. Both the client and server code we have is very simple. +- Trivial to test, but you have to be wary of the asynchronous nature of the tests ### Handling code in tests that can be delayed or never finish -- Create helper functions to retry assertions and add timeouts. -- We can use go routines to ensure the assertions don't block anything and then use channels to let them signal that they have finished, or not. +- Create helper functions to retry assertions and add timeouts. +- We can use go routines to ensure the assertions don't block anything and then use channels to let them signal that they have finished, or not. - The `time` package has some helpful functions which also send signals via channels about events in time so we can set timeouts