# Table of Contents - [Learn Go with Tests | Learn Go with tests](#learn-go-with-tests-learn-go-with-tests) - [Learn Go with Tests | Primary version | Learn Go with tests](#learn-go-with-tests-primary-version-learn-go-with-tests) - [Install Go | Primary version | Learn Go with tests](#install-go-primary-version-learn-go-with-tests) - [Integers | Primary version | Learn Go with tests](#integers-primary-version-learn-go-with-tests) - [Hello, World | Primary version | Learn Go with tests](#hello-world-primary-version-learn-go-with-tests) - [Iteration | Primary version | Learn Go with tests](#iteration-primary-version-learn-go-with-tests) - [Arrays and slices | Primary version | Learn Go with tests](#arrays-and-slices-primary-version-learn-go-with-tests) - [Structs, methods & interfaces | Primary version | Learn Go with tests](#structs-methods-interfaces-primary-version-learn-go-with-tests) - [Intro | Primary version | Learn Go with tests](#intro-primary-version-learn-go-with-tests) - [Pointers & errors | Primary version | Learn Go with tests](#pointers-errors-primary-version-learn-go-with-tests) - [Maps | Primary version | Learn Go with tests](#maps-primary-version-learn-go-with-tests) - [Dependency Injection | Primary version | Learn Go with tests](#dependency-injection-primary-version-learn-go-with-tests) - [Sync | Primary version | Learn Go with tests](#sync-primary-version-learn-go-with-tests) - [Contributing | Primary version | Learn Go with tests](#contributing-primary-version-learn-go-with-tests) - [Chapter Template | Primary version | Learn Go with tests](#chapter-template-primary-version-learn-go-with-tests) - [Concurrency | Primary version | Learn Go with tests](#concurrency-primary-version-learn-go-with-tests) - [Context-aware Reader | Primary version | Learn Go with tests](#context-aware-reader-primary-version-learn-go-with-tests) - [Select | Primary version | Learn Go with tests](#select-primary-version-learn-go-with-tests) - [OS Exec | Primary version | Learn Go with tests](#os-exec-primary-version-learn-go-with-tests) - [Mocking | Primary version | Learn Go with tests](#mocking-primary-version-learn-go-with-tests) - [Why unit tests and how to make them work for you | Primary version | Learn Go with tests](#why-unit-tests-and-how-to-make-them-work-for-you-primary-version-learn-go-with-tests) - [Context | Primary version | Learn Go with tests](#context-primary-version-learn-go-with-tests) - [Error types | Primary version | Learn Go with tests](#error-types-primary-version-learn-go-with-tests) - [Revisiting HTTP Handlers | Primary version | Learn Go with tests](#revisiting-http-handlers-primary-version-learn-go-with-tests) - [Reflection | Primary version | Learn Go with tests](#reflection-primary-version-learn-go-with-tests) - [Command line & package structure | Primary version | Learn Go with tests](#command-line-package-structure-primary-version-learn-go-with-tests) - [JSON, routing and embedding | Primary version | Learn Go with tests](#json-routing-and-embedding-primary-version-learn-go-with-tests) - [Integers | Learn Go with tests](#integers-learn-go-with-tests) - [Install Go | Learn Go with tests](#install-go-learn-go-with-tests) - [Iteration | Learn Go with tests](#iteration-learn-go-with-tests) - [WebSockets | Primary version | Learn Go with tests](#websockets-primary-version-learn-go-with-tests) - [HTTP server | Primary version | Learn Go with tests](#http-server-primary-version-learn-go-with-tests) - [Intro to property based tests | Primary version | Learn Go with tests](#intro-to-property-based-tests-primary-version-learn-go-with-tests) - [Time | Primary version | Learn Go with tests](#time-primary-version-learn-go-with-tests) - [Dependency Injection | Learn Go with tests](#dependency-injection-learn-go-with-tests) - [Intro | Learn Go with tests](#intro-learn-go-with-tests) - [Concurrency | Learn Go with tests](#concurrency-learn-go-with-tests) - [Contributing | Learn Go with tests](#contributing-learn-go-with-tests) - [Sync | Learn Go with tests](#sync-learn-go-with-tests) - [Error types | Learn Go with tests](#error-types-learn-go-with-tests) - [Arrays and slices | Learn Go with tests](#arrays-and-slices-learn-go-with-tests) - [Select | Learn Go with tests](#select-learn-go-with-tests) - [Structs, methods & interfaces | Learn Go with tests](#structs-methods-interfaces-learn-go-with-tests) - [IO and sorting | Primary version | Learn Go with tests](#io-and-sorting-primary-version-learn-go-with-tests) - [Chapter Template | Learn Go with tests](#chapter-template-learn-go-with-tests) - [Context-aware Reader | Learn Go with tests](#context-aware-reader-learn-go-with-tests) - [Anti-patterns | Learn Go with tests](#anti-patterns-learn-go-with-tests) --- # Learn Go with Tests | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/gb-readme.md) . ![](https://quii.gitbook.io/learn-go-with-tests/~gitbook/image?url=https%3A%2F%2F413231286-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F-L9Tqx5WSaiE4u24Pk05-2910905616%252Fuploads%252Fgit-blob-cad524fa8cb34476d131615dfd4861f9aa63a7c4%252Fred-green-blue-gophers-smaller.png%3Falt%3Dmedia&width=768&dpr=3&quality=100&sign=ac8ac5f9&sv=2) [Art by Denise](https://deniseyu.io/) [](https://quii.gitbook.io/learn-go-with-tests#support-me) Support me -------------------------------------------------------------------------- I am proud to offer this resource for free, but if you wish to give some appreciation * [Tweet me @quii](https://twitter.com/quii) * [Mastodon](https://mastodon.cloud/@quii) * [Buy me a coffee](https://www.buymeacoffee.com/quii) * [Sponsor me on GitHub](https://github.com/sponsors/quii) [](https://quii.gitbook.io/learn-go-with-tests#learn-test-driven-development-with-go) Learn test-driven development with Go -------------------------------------------------------------------------------------------------------------------------------- * Explore the Go language by writing tests * **Get a grounding with TDD**. Go is a good language for learning TDD because it is a simple language to learn and testing is built-in * Be confident that you'll be able to start writing robust, well-tested systems in Go Translations: * [中文](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/) * [Français](https://goosegeesejeez.gitbook.io/apprendre-go-par-les-tests) * [한국어](https://miryang.gitbook.io/learn-go-with-tests/) * [Türkçe](https://halilkocaoz.gitbook.io/go-programlama-dilini-ogren/) * [Nederlands](https://bobkosse.gitbook.io/leer-go-met-tests) [](https://quii.gitbook.io/learn-go-with-tests#background) Background -------------------------------------------------------------------------- I have some experience introducing Go to development teams and have tried different approaches as to how to grow a team from some people curious about Go into highly effective writers of Go systems. ### [](https://quii.gitbook.io/learn-go-with-tests#what-didnt-work) What didn't work #### [](https://quii.gitbook.io/learn-go-with-tests#read-the-book) Read _the_ book An approach we tried was to take [the blue book](https://www.amazon.co.uk/Programming-Language-Addison-Wesley-Professional-Computing/dp/0134190440) and every week discuss the next chapter along with the exercises. I love this book but it requires a high level of commitment. The book is very detailed in explaining concepts, which is obviously great but it means that the progress is slow and steady - this is not for everyone. I found that whilst a small number of people would read chapter X and do the exercises, many people didn't. #### [](https://quii.gitbook.io/learn-go-with-tests#solve-some-problems) Solve some problems Katas are fun but they are usually limited in their scope for learning a language; you're unlikely to use goroutines to solve a kata. Another problem is when you have varying levels of enthusiasm. Some people just learn way more of the language than others and when demonstrating what they have done end up confusing people with features the others are not familiar with. This ends up making the learning feel quite _unstructured_ and _ad hoc_. ### [](https://quii.gitbook.io/learn-go-with-tests#what-did-work) What did work By far the most effective way was by slowly introducing the fundamentals of the language by reading through [go by example](https://gobyexample.com/) , exploring them with examples and discussing them as a group. This was a more interactive approach than "read chapter x for homework". Over time the team gained a solid foundation of the _grammar_ of the language so we could then start to build systems. This to me seems analogous to practicing scales when trying to learn guitar. It doesn't matter how artistic you think you are, you are unlikely to write good music without understanding the fundamentals and practicing the mechanics. ### [](https://quii.gitbook.io/learn-go-with-tests#what-works-for-me) What works for me When _I_ learn a new programming language I usually start by messing around in a REPL but eventually, I need more structure. What I like to do is explore concepts and then solidify the ideas with tests. Tests verify the code I write is correct and documents the feature I have learned. Taking my experience of learning with a group and my own personal way I am going to try and create something that hopefully proves useful to other teams. Learning the fundamentals by writing small tests so that you can then take your existing software design skills and ship some great systems. [](https://quii.gitbook.io/learn-go-with-tests#who-this-is-for) Who this is for ------------------------------------------------------------------------------------ * People who are interested in picking up Go * People who already know some Go, but want to explore testing more [](https://quii.gitbook.io/learn-go-with-tests#what-youll-need) What you'll need ------------------------------------------------------------------------------------- * A computer! * [Installed Go](https://golang.org/) * A text editor * Some experience with programming. Understanding of concepts like `if`, variables, functions etc. * Comfortable using the terminal [](https://quii.gitbook.io/learn-go-with-tests#feedback) Feedback ---------------------------------------------------------------------- * Add issues/submit PRs [here](https://github.com/quii/learn-go-with-tests) or [tweet me @quii](https://twitter.com/quii) [MIT license](https://github.com/quii/learn-go-with-tests/blob/main/LICENSE.md) [NextInstall Go](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/install-go) Last updated 1 month ago * [Support me](https://quii.gitbook.io/learn-go-with-tests#support-me) * [Learn test-driven development with Go](https://quii.gitbook.io/learn-go-with-tests#learn-test-driven-development-with-go) * [Background](https://quii.gitbook.io/learn-go-with-tests#background) * [What didn't work](https://quii.gitbook.io/learn-go-with-tests#what-didnt-work) * [What did work](https://quii.gitbook.io/learn-go-with-tests#what-did-work) * [What works for me](https://quii.gitbook.io/learn-go-with-tests#what-works-for-me) * [Who this is for](https://quii.gitbook.io/learn-go-with-tests#who-this-is-for) * [What you'll need](https://quii.gitbook.io/learn-go-with-tests#what-youll-need) * [Feedback](https://quii.gitbook.io/learn-go-with-tests#feedback) --- # Learn Go with Tests | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/master.md) . ![](https://quii.gitbook.io/learn-go-with-tests/~gitbook/image?url=https%3A%2F%2F4221162866-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-legacy-files%2Fo%2Fassets%252F-L9Tqx5WSaiE4u24Pk05%252F-LmyGcIwYFqlc-kxzIGZ%252F-LXAJRSbtm02phRcFvU4%252Fred-green-blue-gophers-smaller.png%3Fgeneration%3D1566560397041519%26alt%3Dmedia&width=768&dpr=3&quality=100&sign=1f92560a&sv=2) [Art by Denise](https://twitter.com/deniseyu21) [Buy me a coffee :coffee:](https://www.buymeacoffee.com/quii) ! _Learn test-driven development with Go_ Translations: [中文](https://studygolang.gitbook.io/learn-go-with-tests) , [Português](https://larien.gitbook.io/aprenda-go-com-testes/) * Explore the Go language by writing tests * **Get a grounding with TDD**. Go is a good language for learning TDD because it is a simple language to learn and testing is built-in * Be confident that you'll be able to start writing robust, well-tested systems in Go [](https://quii.gitbook.io/learn-go-with-tests/master#background) Background --------------------------------------------------------------------------------- I have some experience introducing Go to development teams and have tried different approaches as to how to grow a team from some people curious about Go into highly effective writers of Go systems. ### [](https://quii.gitbook.io/learn-go-with-tests/master#what-didnt-work) What didn't work #### [](https://quii.gitbook.io/learn-go-with-tests/master#read-the-book) Read _the_ book An approach we tried was to take [the blue book](https://www.amazon.co.uk/Programming-Language-Addison-Wesley-Professional-Computing/dp/0134190440) and every week discuss the next chapter along with the exercises. I love this book but it requires a high level of commitment. The book is very detailed in explaining concepts, which is obviously great but it means that the progress is slow and steady - this is not for everyone. I found that whilst a small number of people would read chapter X and do the exercises, many people didn't. #### [](https://quii.gitbook.io/learn-go-with-tests/master#solve-some-problems) Solve some problems Katas are fun but they are usually limited in their scope for learning a language; you're unlikely to use goroutines to solve a kata. Another problem is when you have varying levels of enthusiasm. Some people just learn way more of the language than others and when demonstrating what they have done end up confusing people with features the others are not familiar with. This ends up making the learning feel quite _unstructured_ and _ad hoc_. ### [](https://quii.gitbook.io/learn-go-with-tests/master#what-did-work) What did work By far the most effective way was by slowly introducing the fundamentals of the language by reading through [go by example](https://gobyexample.com/) , exploring them with examples and discussing them as a group. This was a more interactive approach than "read chapter x for homework". Over time the team gained a solid foundation of the _grammar_ of the language so we could then start to build systems. This to me seems analogous to practicing scales when trying to learn guitar. It doesn't matter how artistic you think you are, you are unlikely to write good music without understanding the fundamentals and practicing the mechanics. ### [](https://quii.gitbook.io/learn-go-with-tests/master#what-works-for-me) What works for me When _I_ learn a new programming language I usually start by messing around in a REPL but eventually, I need more structure. What I like to do is explore concepts and then solidify the ideas with tests. Tests verify the code I write is correct and documents the feature I have learned. Taking my experience of learning with a group and my own personal way I am going to try and create something that hopefully proves useful to other teams. Learning the fundamentals by writing small tests so that you can then take your existing software design skills and ship some great systems. [](https://quii.gitbook.io/learn-go-with-tests/master#who-this-is-for) Who this is for ------------------------------------------------------------------------------------------- * People who are interested in picking up Go * People who already know some Go, but want to explore testing more [](https://quii.gitbook.io/learn-go-with-tests/master#what-youll-need) What you'll need -------------------------------------------------------------------------------------------- * A computer! * [Installed Go](https://golang.org/) * A text editor * Some experience with programming. Understanding of concepts like `if`, variables, functions etc. * Comfortable with using the terminal [](https://quii.gitbook.io/learn-go-with-tests/master#feedback) Feedback ----------------------------------------------------------------------------- * Add issues/submit PRs [here](https://github.com/quii/learn-go-with-tests) or [tweet me @quii](https://twitter.com/quii) [MIT license](https://github.com/quii/learn-go-with-tests/tree/ddbba96d56729aacf33fec9b79d612565b990522/LICENSE.md) [NextInstall Go](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/install-go) Last updated 6 years ago * [Background](https://quii.gitbook.io/learn-go-with-tests/master#background) * [What didn't work](https://quii.gitbook.io/learn-go-with-tests/master#what-didnt-work) * [What did work](https://quii.gitbook.io/learn-go-with-tests/master#what-did-work) * [What works for me](https://quii.gitbook.io/learn-go-with-tests/master#what-works-for-me) * [Who this is for](https://quii.gitbook.io/learn-go-with-tests/master#who-this-is-for) * [What you'll need](https://quii.gitbook.io/learn-go-with-tests/master#what-youll-need) * [Feedback](https://quii.gitbook.io/learn-go-with-tests/master#feedback) --- # Install Go | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/install-go.md) . The official installation instructions for Go are available [here](https://golang.org/doc/install) . This guide will assume that you are using a package manager for e.g. [Homebrew](https://brew.sh/) , [Chocolatey](https://chocolatey.org/) , [Apt](https://help.ubuntu.com/community/AptGet/Howto) or [yum](https://access.redhat.com/solutions/9934) . For demonstration purposes we will show the installation procedure for OSX using Homebrew. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/install-go#installation) Installation ---------------------------------------------------------------------------------------------------------------- The process of installation is very easy. First, what you have to do is to run this command to install homebrew. It has a dependency on Xcode so you should ensure this is installed first. Copy xcode-select --install Then you run the following to install homebrew: Copy /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" At this point you can now install Go with: Copy brew install go _You should follow any instructions recommended by your package manager._ _**Note**_ _these may be host os specific_. You can verify the installation with: Copy $ go version go version go1.14 darwin/amd64 [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/install-go#go-environment) Go Environment -------------------------------------------------------------------------------------------------------------------- ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/install-go#usdgopath) $GOPATH Go is opinionated. By convention, all Go code lives within a single workspace (folder). This workspace could be anywhere in your machine. If you don't specify, Go will assume `$HOME/go` as the default workspace. The workspace is identified (and modified) by the environment variable [GOPATH](https://golang.org/cmd/go/#hdr-GOPATH_environment_variable) . You should set the environment variable so that you can use it later in scripts, shells, etc. Update your `.bash_profile` to contain the following exports: _Note_ you should open a new shell to pickup these environment variables. Go assumes that your workspace contains a specific directory structure. Go places its files in three directories: All source code lives in src, package objects lives in pkg, and the compiled programs live in bin. You can create these directories as follows: At this point you can `go get` and the `src/package/bin` will be installed correctly in the appropriate `$GOPATH/xxx` directory. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/install-go#go-modules) Go Modules Go 1.11 introduced [Modules](https://github.com/golang/go/wiki/Modules) , enabling an alternative workflow. This new approach will gradually [become the default](https://blog.golang.org/modules2019) mode, deprecating the use of `GOPATH`. Modules aim to solve problems related to dependency management, version selection and reproducible builds; they also enable users to run Go code outside of `GOPATH`. Using Modules is pretty straightforward. Select any directory outside `GOPATH` as the root of your project, and create a new module with the `go mod init` command. A `go.mod` file will be generated, containing the module path, a Go version, and its dependency requirements, which are the other modules needed for a successful build. If no `` is specified, `go mod init` will try to guess the module path from the directory structure, but it can also be overrided, by supplying an argument. A `go.mod` file could look like this: The built-in documentation provides an overview of all available `go mod` commands. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/install-go#go-editor) Go Editor ---------------------------------------------------------------------------------------------------------- Editor preference is very individualistic, you may already have a preference that supports Go. If you don't you should consider an Editor such as [Visual Studio Code](https://code.visualstudio.com/) , which has exceptional Go support. You can install it using the following command: You can confirm VS Code installed correctly you can run the following in your shell. VS Code is shipped with very little software enabled, you can enable new software by installing extensions. To add Go support you must install an extension, there are a variety available for VS Code, an exceptional one is [Luke Hoban's package](https://github.com/golang/vscode-go) . This can be installed as follows: When you open a Go file for the first time in VS Code, it will indicate that the Analysis tools are missing, you should click the button to install these. The list of tools that gets installed (and used) by VS Code are available [here](https://github.com/golang/vscode-go/blob/master/docs/tools.md) . [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/install-go#go-debugger) Go Debugger -------------------------------------------------------------------------------------------------------------- A good option for debugging Go (that's integrated with VS Code) is Delve. This can be installed as follows: For additional help configuring and running the Go debugger in VS Code, please reference the [VS Code debugging documentation](https://github.com/golang/vscode-go/blob/master/docs/debugging.md) . [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/install-go#go-linting) Go Linting ------------------------------------------------------------------------------------------------------------ An improvement over the default linter can be configured using [GolangCI-Lint](https://golangci-lint.run/) . This can be installed as follows: [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/install-go#refactoring-and-your-tooling) Refactoring and your tooling ------------------------------------------------------------------------------------------------------------------------------------------------ A big emphasis of this book is around the importance of refactoring. Your tools can help you do bigger refactoring with confidence. You should be familiar enough with your editor to perform the following with a simple key combination: * **Extract/Inline variable**. Being able to take magic values and give them a name lets you simplify your code quickly * **Extract method/function**. It is vital to be able to take a section of code and extract functions/methods * **Rename**. You should be able to confidently rename symbols across files. * **go fmt**. Go has an opinioned formatter called `go fmt`. Your editor should be running this on every file save. * **Run tests**. It goes without saying that you should be able to do any of the above and then quickly re-run your tests to ensure your refactoring hasn't broken anything In addition, to help you work with your code you should be able to: * **View function signature** - You should never be unsure how to call a function in Go. Your IDE should describe a function in terms of its documentation, its parameters and what it returns. * **View function definition** - If it's still not clear what a function does, you should be able to jump to the source code and try and figure it out yourself. * **Find usages of a symbol** - Being able to see the context of a function being called can help your decision process when refactoring. Mastering your tools will help you concentrate on the code and reduce context switching. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/install-go#wrapping-up) Wrapping up -------------------------------------------------------------------------------------------------------------- At this point you should have Go installed, an editor available and some basic tooling in place. Go has a very large ecosystem of third party products. We have identified a few useful components here, for a more complete list see [https://awesome-go.com](https://awesome-go.com/) . [PreviousLearn Go with Tests](https://quii.gitbook.io/learn-go-with-tests/master) [NextHello, World](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world) Last updated 6 years ago * [Installation](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/install-go#installation) * [Go Environment](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/install-go#go-environment) * [$GOPATH](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/install-go#usdgopath) * [Go Modules](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/install-go#go-modules) * [Go Editor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/install-go#go-editor) * [Go Debugger](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/install-go#go-debugger) * [Go Linting](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/install-go#go-linting) * [Refactoring and your tooling](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/install-go#refactoring-and-your-tooling) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/install-go#wrapping-up) Copy export GOPATH=$HOME/go export PATH=$PATH:$GOPATH/bin Copy mkdir -p $GOPATH/src $GOPATH/pkg $GOPATH/bin Copy mkdir my-project cd my-project go mod init Copy module cmd go 1.14 require ( github.com/google/pprof v0.0.0-20190515194954-54271f7e092f golang.org/x/arch v0.0.0-20190815191158-8a70ba74b3a1 golang.org/x/tools v0.0.0-20190611154301-25a4f137592f ) Copy go help mod go help mod init Copy brew cask install visual-studio-code Copy code . Copy code --install-extension golang.go Copy go get -u github.com/go-delve/delve/cmd/dlv Copy brew install golangci/tap/golangci-lint --- # Integers | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/integers.md) . [**You can find all the code for this chapter here**](https://github.com/quii/learn-go-with-tests/tree/master/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. **Note:** Go source files can only have one `package` per directory, make sure that your files are organised separately. [Here is a good explanation on this.](https://dave.cheney.net/2014/12/01/five-suggestions-for-setting-up-a-go-project) [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/integers#write-the-test-first) Write the test first ------------------------------------------------------------------------------------------------------------------------------ Copy package integers import "testing" func TestAdder(t *testing.T) { sum := Add(2, 2) expected := 4 if sum != expected { t.Errorf("expected '%d' but got '%d'", expected, sum) } } You will notice that we're using `%d` as our format strings rather than `%q`. That's because we want it to print an integer rather than a string. Also note that we are no longer using the main package, instead we've defined a package named `integers`, as the name suggests this will group functions for working with integers such as `Add`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/integers#try-and-run-the-test) Try and run the test ------------------------------------------------------------------------------------------------------------------------------ Run the test `go test` Inspect the compilation error `./adder_test.go:6:9: undefined: Add` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/integers#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Write enough code to satisfy the compiler _and that's all_ - remember we want to check that our tests fail for the correct reason. When you have more than one argument of the same type (in our case two integers) rather than having `(x int, y int)` you can shorten it to `(x, y int)`. Now run the tests and we should be happy that the test is correctly reporting what is wrong. `adder_test.go:10: expected '4' but got '0'` If you have noticed we learnt about _named return value_ in the [last](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#one...last...refactor?) section but aren't using the same here. It should generally be used when the meaning of the result isn't clear from context, in our case it's pretty much clear that `Add` function will add the parameters. You can refer [this](https://github.com/golang/go/wiki/CodeReviewComments#named-result-parameters) wiki for more details. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/integers#write-enough-code-to-make-it-pass) Write enough code to make it pass -------------------------------------------------------------------------------------------------------------------------------------------------------- In the strictest sense of TDD we should now write the _minimal amount of code to make the test pass_. A pedantic programmer may do this Ah hah! Foiled again, TDD is a sham right? We could write another test, with some different numbers to force that test to fail but that feels like [a game of cat and mouse](https://en.m.wikipedia.org/wiki/Cat_and_mouse) . Once we're more familiar with Go's syntax I will introduce a technique called _"Property Based Testing"_, which would stop annoying developers and help you find bugs. For now, let's fix it properly If you re-run the tests they should pass. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/integers#refactor) Refactor ------------------------------------------------------------------------------------------------------ There's not a lot in the _actual_ code we can really improve on here. We explored earlier how by naming the return argument it appears in the documentation but also in most developer's text editors. This is great because it aids the usability of code you are writing. It is preferable that a user can understand the usage of your code by just looking at the type signature and documentation. You can add documentation to functions with comments, and these will appear in Go Doc just like when you look at the standard library's documentation. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/integers#examples) Examples If you really want to go the extra mile you can make [examples](https://blog.golang.org/examples) . You will find a lot of examples in the documentation of the standard library. Often code examples that can be found outside the codebase, such as a readme file often become out of date and incorrect compared to the actual code because they don't get checked. Go examples are executed just like tests so you can be confident examples reflect what the code actually does. Examples are compiled (and optionally executed) as part of a package's test suite. As with typical tests, examples are functions that reside in a package's `_test.go` files. Add the following `ExampleAdd` function to the `adder_test.go` file. (If your editor doesn't automatically import packages for you, the compilation step will fail because you will be missing `import "fmt"` in `adder_test.go`. It is strongly recommended you research how to have these kind of errors fixed for you automatically in whatever editor you are using.) If your code changes so that the example is no longer valid, your build will fail. Running the package's test suite, we can see the example function is executed with no further arrangement from us: Please note that the example function will not be executed if you remove the comment `//Output: 6`. Although the function will be compiled, it won't be executed. By adding this code the example will appear in the documentation inside `godoc`, making your code even more accessible. To try this out, run `godoc -http=:6060` and navigate to `http://localhost:6060/pkg/` Inside here you'll see a list of all the packages in your `$GOPATH`, so assuming you wrote this code in somewhere like `$GOPATH/src/github.com/{your_id}` you'll be able to find your example documentation. If you publish your code with examples to a public URL, you can share the documentation of your code at [pkg.go.dev](https://pkg.go.dev/) . For example, [here](https://pkg.go.dev/github.com/quii/learn-go-with-tests/integers/v2) is the finalised API for this chapter. This web interface allows you to search for documentation of standard library packages and third-party packages. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/integers#wrapping-up) Wrapping up ------------------------------------------------------------------------------------------------------------ What we have covered: * More practice of the TDD workflow * Integers, addition * Writing better documentation so users of our code can understand its usage quickly * Examples of how to use our code, which are checked as part of our tests [PreviousHello, World](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world) [NextIteration](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/iteration) Last updated 6 years ago * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/integers#write-the-test-first) * [Try and run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/integers#try-and-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/integers#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/integers#write-enough-code-to-make-it-pass) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/integers#refactor) * [Examples](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/integers#examples) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/integers#wrapping-up) Copy package integers func Add(x, y int) int { return 0 } Copy func Add(x, y int) int { return 4 } Copy func Add(x, y int) int { return x + y } Copy // Add takes two integers and returns the sum of them. func Add(x, y int) int { return x + y } Copy func ExampleAdd() { sum := Add(1, 5) fmt.Println(sum) // Output: 6 } Copy $ go test -v === RUN TestAdder --- PASS: TestAdder (0.00s) === RUN ExampleAdd --- PASS: ExampleAdd (0.00s) --- # Hello, World | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world.md) . [**You can find all the code for this chapter here**](https://github.com/quii/learn-go-with-tests/tree/master/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) . In the [previous chapter](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/install-go#go-environment) we discussed how Go is opinionated as to where you put your files. Make a directory in the following path `$GOPATH/src/github.com/{your-user-id}/hello`. So if you're on a unix based OS and you are happy to stick with Go's conventions about `$GOPATH` (which is the easiest way of setting up) you could run `mkdir -p $GOPATH/src/github.com/$USER/hello`. For subsequent chapters, you can make a new folder with whatever name you like to put the code in e.g `$GOPATH/src/github.com/{your-user-id}/integers` for the next chapter might be sensible. Some readers of this book like to make an enclosing folder for all the work such as "learn-go-with-tests/hello". In short, it's up to you how you structure your folders. Create a file in this directory called `hello.go` and write this code. To run it type `go run hello.go`. Copy package main import "fmt" func main() { fmt.Println("Hello, world") } [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#how-it-works) How it works ----------------------------------------------------------------------------------------------------------------- When you write a program in Go you will have a `main` package defined with a `main` func inside it. Packages are ways of grouping up related Go code together. The `func` keyword is how you define a function with a name and a body. With `import "fmt"` we are importing a package which contains the `Println` function that we use to print. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#how-to-test) How to test --------------------------------------------------------------------------------------------------------------- How do you test this? It is good to separate your "domain" code from the outside world (side-effects). The `fmt.Println` is a side effect (printing to stdout) and the string we send in is our domain. So let's separate these concerns so it's easier to test We have created a new function again with `func` but this time we've added another keyword `string` in the definition. This means this function returns a `string`. Now create a new file called `hello_test.go` where we are going to write a test for our `Hello` function Before explaining, let's just run the code. Run `go test` in your terminal. It should've passed! Just to check, try deliberately breaking the test by changing the `want` string. Notice how you have not had to pick between multiple testing frameworks and then figure out how to install. Everything you need is built in to the language and the syntax is the same as the rest of the code you will write. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#writing-tests) Writing tests Writing a test is just like writing a function, with a few rules * It needs to be in a file with a name like `xxx_test.go` * The test function must start with the word `Test` * The test function takes one argument only `t *testing.T` For now it's enough to know that your `t` of type `*testing.T` is your "hook" into the testing framework so you can do things like `t.Fail()` when you want to fail. We've covered some new topics: #### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#if) `if` If statements in Go are very much like other programming languages. #### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#declaring-variables) Declaring variables We're declaring some variables with the syntax `varName := value`, which lets us re-use some values in our test for readability. #### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#t.errorf) `t.Errorf` We are calling the `Errorf` _method_ on our `t` which will print out a message and fail the test. The `f` stands for format which allows us to build a string with values inserted into the placeholder values `%q`. When you made the test fail it should be clear how it works. You can read more about the placeholder strings in the [fmt go doc](https://golang.org/pkg/fmt/#hdr-Printing) . For tests `%q` is very useful as it wraps your values in double quotes. We will later explore the difference between methods and functions. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#go-doc) Go doc Another quality of life feature of Go is the documentation. You can launch the docs locally by running `godoc -http :8000`. If you go to [localhost:8000/pkg](http://localhost:8000/pkg) you will see all the packages installed on your system. The vast majority of the standard library has excellent documentation with examples. Navigating to [http://localhost:8000/pkg/testing/](http://localhost:8000/pkg/testing/) would be worthwhile to see what's available to you. If you don't have `godoc` command, then maybe you are using the newer version of Go (1.14 or later) which is [no longer including `godoc`](https://golang.org/doc/go1.14#godoc) . You can manually install it with `go get golang.org/x/tools/cmd/godoc`. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#hello-you) Hello, YOU Now that we have a test we can iterate on our software safely. In the last example we wrote the test _after_ the code had been written just so you could get an example of how to write a test and declare a function. From this point on we will be _writing tests first_. Our next requirement is to let us specify the recipient of the greeting. Let's start by capturing these requirements in a test. This is basic test driven development and allows us to make sure our test is _actually_ testing what we want. When you retrospectively write tests there is the risk that your test may continue to pass even if the code doesn't work as intended. Now run `go test`, you should have a compilation error When using a statically typed language like Go it is important to _listen to the compiler_. The compiler understands how your code should snap together and work so you don't have to. In this case the compiler is telling you what you need to do to continue. We have to change our function `Hello` to accept an argument. Edit the `Hello` function to accept an argument of type string If you try and run your tests again your `main.go` will fail to compile because you're not passing an argument. Send in "world" to make it pass. Now when you run your tests you should see something like We finally have a compiling program but it is not meeting our requirements according to the test. Let's make the test pass by using the name argument and concatenate it with `Hello,` When you run the tests they should now pass. Normally as part of the TDD cycle we should now _refactor_. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#a-note-on-source-control) A note on source control At this point, if you are using source control (which you should!) I would `commit` the code as it is. We have working software backed by a test. I _wouldn't_ push to master though, because I plan to refactor next. It is nice to commit at this point in case you somehow get into a mess with refactoring - you can always go back to the working version. There's not a lot to refactor here, but we can introduce another language feature, _constants_. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#constants) Constants Constants are defined like so We can now refactor our code After refactoring, re-run your tests to make sure you haven't broken anything. Constants should improve performance of your application as it saves you creating the `"Hello, "` string instance every time `Hello` is called. To be clear, the performance boost is incredibly negligible for this example! But it's worth thinking about creating constants to capture the meaning of values and sometimes to aid performance. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#hello-world...-again) Hello, world... again ---------------------------------------------------------------------------------------------------------------------------------- The next requirement is when our function is called with an empty string it defaults to printing "Hello, World", rather than "Hello, ". Start by writing a new failing test Here we are introducing another tool in our testing arsenal, subtests. Sometimes it is useful to group tests around a "thing" and then have subtests describing different scenarios. A benefit of this approach is you can set up shared code that can be used in the other tests. There is repeated code when we check if the message is what we expect. Refactoring is not _just_ for the production code! It is important that your tests _are clear specifications_ of what the code needs to do. We can and should refactor our tests. What have we done here? We've refactored our assertion into a function. This reduces duplication and improves readability of our tests. In Go you can declare functions inside other functions and assign them to variables. You can then call them, just like normal functions. We need to pass in `t *testing.T` so that we can tell the test code to fail when we need to. `t.Helper()` is needed to tell the test suite that this method is a helper. By doing this when it fails the line number reported will be in our _function call_ rather than inside our test helper. This will help other developers track down problems easier. If you still don't understand, comment it out, make a test fail and observe the test output. Now that we have a well-written failing test, let's fix the code, using an `if`. If we run our tests we should see it satisfies the new requirement and we haven't accidentally broken the other functionality. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#back-to-source-control) Back to source control Now we are happy with the code I would amend the previous commit so we only check in the lovely version of our code with its test. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#discipline) Discipline Let's go over the cycle again * Write a test * Make the compiler pass * Run the test, see that it fails and check the error message is meaningful * Write enough code to make the test pass * Refactor On the face of it this may seem tedious but sticking to the feedback loop is important. Not only does it ensure that you have _relevant tests_, it helps ensure _you design good software_ by refactoring with the safety of tests. Seeing the test fail is an important check because it also lets you see what the error message looks like. As a developer it can be very hard to work with a codebase when failing tests do not give a clear idea as to what the problem is. By ensuring your tests are _fast_ and setting up your tools so that running tests is simple you can get in to a state of flow when writing your code. By not writing tests you are committing to manually checking your code by running your software which breaks your state of flow and you won't be saving yourself any time, especially in the long run. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#keep-going-more-requirements) Keep going! More requirements -------------------------------------------------------------------------------------------------------------------------------------------------- Goodness me, we have more requirements. We now need to support a second parameter, specifying the language of the greeting. If a language is passed in that we do not recognise, just default to English. We should be confident that we can use TDD to flesh out this functionality easily! Write a test for a user passing in Spanish. Add it to the existing suite. Remember not to cheat! _Test first_. When you try and run the test, the compiler _should_ complain because you are calling `Hello` with two arguments rather than one. Fix the compilation problems by adding another string argument to `Hello` When you try and run the test again it will complain about not passing through enough arguments to `Hello` in your other tests and in `hello.go` Fix them by passing through empty strings. Now all your tests should compile _and_ pass, apart from our new scenario We can use `if` here to check the language is equal to "Spanish" and if so change the message 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. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#french) French * Write a test asserting that if you pass in `"French"` you get `"Bonjour, "` * See it fail, check the error message is easy to read * Do the smallest reasonable change in the code You may have written something that looks roughly like this [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#switch) `switch` ------------------------------------------------------------------------------------------------------- When you have lots of `if` statements checking a particular value it is common to use a `switch` statement instead. We can use `switch` to refactor the code to make it easier to read and more extensible if we wish to add more language support later Write a test to now include a greeting in the language of your choice and you should see how simple it is to extend our _amazing_ function. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#one...last...refactor) one...last...refactor? You could argue that maybe our function is getting a little big. The simplest refactor for this would be to extract out some functionality into another function. A few new concepts: * In our function signature we have made a _named return value_ `(prefix string)`. * This will create a variable called `prefix` in your function. * It will be assigned the "zero" value. This depends on the type, for example `int`s are 0 and for strings it is `""`. * You can return whatever it's set to by just calling `return` rather than `return prefix`. * This will display in the Go Doc for your function so it can make the intent of your code clearer. * `default` in the switch case will be branched to if none of the other `case` statements match. * The function name starts with a lowercase letter. In Go public functions start with a capital letter and private ones start with a lowercase. We don't want the internals of our algorithm to be exposed to the world, so we made this function private. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#wrapping-up) Wrapping up --------------------------------------------------------------------------------------------------------------- Who knew you could get so much out of `Hello, world`? By now you should have some understanding of: ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#some-of-gos-syntax-around) Some of Go's syntax around * Writing tests * Declaring functions, with arguments and return types * `if`, `const` and `switch` * Declaring variables and constants ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#the-tdd-process-and-why-the-steps-are-important) The TDD process and _why_ the steps are important * _Write a failing test and see it fail_ so we know we have written a _relevant_ test for our requirements and seen that it produces an _easy to understand description of the failure_ * Writing the smallest amount of code to make it pass so we know we have working software * _Then_ refactor, backed with the safety of our tests to ensure we have well-crafted code that is easy to work with In our case we've gone from `Hello()` to `Hello("name")`, to `Hello("name", "French")` in small, easy to understand steps. This is of course trivial compared to "real world" software but the principles still stand. TDD is a skill that needs practice to develop but by being able to break problems down into smaller components that you can test you will have a much easier time writing software. [PreviousInstall Go](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/install-go) [NextIntegers](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/integers) Last updated 6 years ago * [How it works](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#how-it-works) * [How to test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#how-to-test) * [Writing tests](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#writing-tests) * [Go doc](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#go-doc) * [Hello, YOU](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#hello-you) * [A note on source control](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#a-note-on-source-control) * [Constants](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#constants) * [Hello, world... again](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#hello-world...-again) * [Back to source control](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#back-to-source-control) * [Discipline](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#discipline) * [Keep going! More requirements](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#keep-going-more-requirements) * [French](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#french) * [switch](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#switch) * [one...last...refactor?](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#one...last...refactor) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#wrapping-up) * [Some of Go's syntax around](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#some-of-gos-syntax-around) * [The TDD process and why the steps are important](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/hello-world#the-tdd-process-and-why-the-steps-are-important) Copy package main import "fmt" func Hello() string { return "Hello, world" } func main() { fmt.Println(Hello()) } Copy package main import "testing" func TestHello(t *testing.T) { got := Hello() want := "Hello, world" if got != want { t.Errorf("got %q want %q", got, want) } } Copy package main import "testing" func TestHello(t *testing.T) { got := Hello("Chris") want := "Hello, Chris" if got != want { t.Errorf("got %q want %q", got, want) } } Copy ./hello_test.go:6:18: too many arguments in call to Hello have (string) want () Copy func Hello(name string) string { return "Hello, world" } Copy func main() { fmt.Println(Hello("world")) } Copy hello_test.go:10: got 'Hello, world' want 'Hello, Chris'' Copy func Hello(name string) string { return "Hello, " + name } Copy const englishHelloPrefix = "Hello, " Copy const englishHelloPrefix = "Hello, " func Hello(name string) string { return englishHelloPrefix + name } Copy func TestHello(t *testing.T) { t.Run("saying hello to people", func(t *testing.T) { got := Hello("Chris") want := "Hello, Chris" if got != want { t.Errorf("got %q want %q", got, want) } }) t.Run("say 'Hello, World' when an empty string is supplied", func(t *testing.T) { got := Hello("") want := "Hello, World" if got != want { t.Errorf("got %q want %q", got, want) } }) } Copy func TestHello(t *testing.T) { assertCorrectMessage := func(t *testing.T, got, want string) { t.Helper() if got != want { t.Errorf("got %q want %q", got, want) } } t.Run("saying hello to people", func(t *testing.T) { got := Hello("Chris") want := "Hello, Chris" assertCorrectMessage(t, got, want) }) t.Run("empty string defaults to 'World'", func(t *testing.T) { got := Hello("") want := "Hello, World" assertCorrectMessage(t, got, want) }) } Copy const englishHelloPrefix = "Hello, " func Hello(name string) string { if name == "" { name = "World" } return englishHelloPrefix + name } Copy t.Run("in Spanish", func(t *testing.T) { got := Hello("Elodie", "Spanish") want := "Hola, Elodie" assertCorrectMessage(t, got, want) }) Copy ./hello_test.go:27:19: too many arguments in call to Hello have (string, string) want (string) Copy func Hello(name string, language string) string { if name == "" { name = "World" } return englishHelloPrefix + name } Copy ./hello.go:15:19: not enough arguments in call to Hello have (string) want (string, string) Copy hello_test.go:29: got 'Hello, Elodie' want 'Hola, Elodie' Copy func Hello(name string, language string) string { if name == "" { name = "World" } if language == "Spanish" { return "Hola, " + name } return englishHelloPrefix + name } Copy const spanish = "Spanish" const englishHelloPrefix = "Hello, " const spanishHelloPrefix = "Hola, " func Hello(name string, language string) string { if name == "" { name = "World" } if language == spanish { return spanishHelloPrefix + name } return englishHelloPrefix + name } Copy func Hello(name string, language string) string { if name == "" { name = "World" } if language == spanish { return spanishHelloPrefix + name } if language == french { return frenchHelloPrefix + name } return englishHelloPrefix + name } Copy func Hello(name string, language string) string { if name == "" { name = "World" } prefix := englishHelloPrefix switch language { case french: prefix = frenchHelloPrefix case spanish: prefix = spanishHelloPrefix } return prefix + name } Copy func Hello(name string, language string) string { if name == "" { name = "World" } return greetingPrefix(language) + name } func greetingPrefix(language string) (prefix string) { switch language { case french: prefix = frenchHelloPrefix case spanish: prefix = spanishHelloPrefix default: prefix = englishHelloPrefix } return } --- # Iteration | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/iteration.md) . [**You can find all the code for this chapter here**](https://github.com/quii/learn-go-with-tests/tree/master/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! Let's write a test for a function that repeats a character 5 times. There's nothing new so far, so try and write it yourself for practice. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/iteration#write-the-test-first) Write the test first ------------------------------------------------------------------------------------------------------------------------------- Copy package iteration import "testing" func TestRepeat(t *testing.T) { repeated := Repeat("a") expected := "aaaaa" if repeated != expected { t.Errorf("expected %q but got %q", expected, repeated) } } [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/iteration#try-and-run-the-test) Try and run the test ------------------------------------------------------------------------------------------------------------------------------- `./repeat_test.go:6:14: undefined: Repeat` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/iteration#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- _Keep the discipline!_ You don't need to know anything new right now to make the test fail properly. All you need to do right now is enough to make it compile so you can check your test is written well. Isn't it nice to know you already know enough Go to write tests for some basic problems? This means you can now play with the production code as much as you like and know it's behaving as you'd hope. `repeat_test.go:10: expected 'aaaaa' but got ''` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/iteration#write-enough-code-to-make-it-pass) Write enough code to make it pass --------------------------------------------------------------------------------------------------------------------------------------------------------- The `for` syntax is very unremarkable and follows most C-like languages. Unlike other languages like C, Java, or JavaScript there are no parentheses surrounding the three components of the for statement and the braces `{ }` are always required. You might wonder what is happening in the row as we've been using `:=` so far to declare and initializing variables. However, `:=` is simply [short hand for both steps](https://gobyexample.com/variables) . Here we are declaring a `string` variable only. Hence, the explicit version. We can also use `var` to declare functions, as we'll see later on. Run the test and it should pass. Additional variants of the for loop are described [here](https://gobyexample.com/for) . [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/iteration#refactor) Refactor ------------------------------------------------------------------------------------------------------- Now it's time to refactor and introduce another construct `+=` assignment operator. `+=` called _"the Add AND assignment operator"_, adds the right operand to the left operand and assigns the result to left operand. It works with other types like integers. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/iteration#benchmarking) Benchmarking Writing [benchmarks](https://golang.org/pkg/testing/#hdr-Benchmarks) in Go is another first-class feature of the language and it is very similar to writing tests. You'll see the code is very similar to a test. The `testing.B` gives you access to the cryptically named `b.N`. When the benchmark code is executed, it runs `b.N` times and measures how long it takes. The amount of times the code is run shouldn't matter to you, the framework will determine what is a "good" value for that to let you have some decent results. To run the benchmarks do `go test -bench=.` (or if you're in Windows Powershell `go test -bench="."`) What `136 ns/op` means is our function takes on average 136 nanoseconds to run (on my computer). Which is pretty ok! To test this it ran it 10000000 times. _NOTE_ by default Benchmarks are run sequentially. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/iteration#practice-exercises) Practice exercises --------------------------------------------------------------------------------------------------------------------------- * Change the test so a caller can specify how many times the character is repeated and then fix the code * Write `ExampleRepeat` to document your function * Have a look through the [strings](https://golang.org/pkg/strings) package. Find functions you think could be useful and experiment with them by writing tests like we have here. Investing time learning the standard library will really pay off over time. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/iteration#wrapping-up) Wrapping up ------------------------------------------------------------------------------------------------------------- * More TDD practice * Learned `for` * Learned how to write benchmarks [PreviousIntegers](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/integers) [NextArrays and slices](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices) Last updated 6 years ago * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/iteration#write-the-test-first) * [Try and run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/iteration#try-and-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/iteration#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/iteration#write-enough-code-to-make-it-pass) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/iteration#refactor) * [Benchmarking](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/iteration#benchmarking) * [Practice exercises](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/iteration#practice-exercises) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/iteration#wrapping-up) Copy package iteration func Repeat(character string) string { return "" } Copy func Repeat(character string) string { var repeated string for i := 0; i < 5; i++ { repeated = repeated + character } return repeated } Copy var repeated string Copy const repeatCount = 5 func Repeat(character string) string { var repeated string for i := 0; i < repeatCount; i++ { repeated += character } return repeated } Copy func BenchmarkRepeat(b *testing.B) { for i := 0; i < b.N; i++ { Repeat("a") } } Copy goos: darwin goarch: amd64 pkg: github.com/quii/learn-go-with-tests/for/v4 10000000 136 ns/op PASS --- # Arrays and slices | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices.md) . [**You can find all the code for this chapter here**](https://github.com/quii/learn-go-with-tests/tree/master/arrays) Arrays allow you to store multiple elements of the same type in a variable in a particular order. When you have an array, it is very common to have to iterate over them. So let's use [our new-found knowledge of `for`](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/iteration) to make a `Sum` function. `Sum` will take an array of numbers and return the total. Let's use our TDD skills [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-the-test-first) Write the test first --------------------------------------------------------------------------------------------------------------------------------------- In `sum_test.go` Copy package main import "testing" func TestSum(t *testing.T) { numbers := [5]int{1, 2, 3, 4, 5} got := Sum(numbers) want := 15 if got != want { t.Errorf("got %d want %d given, %v", got, want, numbers) } } Arrays have a _fixed capacity_ which you define when you declare the variable. We can initialize an array in two ways: * \[N\]type{value1, value2, ..., valueN} e.g. `numbers := [5]int{1, 2, 3, 4, 5}` * \[...\]type{value1, value2, ..., valueN} e.g. `numbers := [...]int{1, 2, 3, 4, 5}` It is sometimes useful to also print the inputs to the function in the error message and we are using the `%v` placeholder which is the "default" format, which works well for arrays. [Read more about the format strings](https://golang.org/pkg/fmt/) [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#try-to-run-the-test) Try to run the test ------------------------------------------------------------------------------------------------------------------------------------- By running `go test` the compiler will fail with `./sum_test.go:10:15: undefined: Sum` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- In `sum.go` Your test should now fail with _a clear error message_ `sum_test.go:13: got 0 want 15 given, [1 2 3 4 5]` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-enough-code-to-make-it-pass) Write enough code to make it pass ----------------------------------------------------------------------------------------------------------------------------------------------------------------- To get the value out of an array at a particular index, just use `array[index]` syntax. In this case, we are using `for` to iterate 5 times to work through the array and add each item onto `sum`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#refactor) Refactor --------------------------------------------------------------------------------------------------------------- Let's introduce [`range`](https://gobyexample.com/range) to help clean up our code `range` lets you iterate over an array. Every time it is called it returns two values, the index and the value. We are choosing to ignore the index value by using `_` [blank identifier](https://golang.org/doc/effective_go.html#blank) . ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#arrays-and-their-type) Arrays and their type An interesting property of arrays is that the size is encoded in its type. If you try to pass an `[4]int` into a function that expects `[5]int`, it won't compile. They are different types so it's just the same as trying to pass a `string` into a function that wants an `int`. You may be thinking it's quite cumbersome that arrays have a fixed length, and most of the time you probably won't be using them! Go has _slices_ which do not encode the size of the collection and instead can have any size. The next requirement will be to sum collections of varying sizes. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-the-test-first-1) Write the test first ----------------------------------------------------------------------------------------------------------------------------------------- We will now use the [slice type](https://golang.org/doc/effective_go.html#slices) which allows us to have collections of any size. The syntax is very similar to arrays, you just omit the size when declaring them `mySlice := []int{1,2,3}` rather than `myArray := [3]int{1,2,3}` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#try-and-run-the-test) Try and run the test --------------------------------------------------------------------------------------------------------------------------------------- This does not compile `./sum_test.go:22:13: cannot use numbers (type []int) as type [5]int in argument to Sum` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) Write the minimal amount of code for the test to run and check the failing test output ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- The problem here is we can either * Break the existing API by changing the argument to `Sum` to be a slice rather than an array. When we do this we will know we have potentially ruined someone's day because our _other_ test will not compile! * Create a new function In our case, no-one else is using our function so rather than having two functions to maintain let's just have one. If you try to run the tests they will still not compile, you will have to change the first test to pass in a slice rather than an array. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-enough-code-to-make-it-pass-1) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------------- It turns out that fixing the compiler problems were all we need to do here and the tests pass! [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#refactor-1) Refactor ----------------------------------------------------------------------------------------------------------------- We had already refactored `Sum` and all we've done is changing from arrays to slices, so there's not a lot to do here. Remember that we must not neglect our test code in the refactoring stage and we have some to do here. It is important to question the value of your tests. It should not be a goal to have as many tests as possible, but rather to have as much _confidence_ as possible in your code base. Having too many tests can turn in to a real problem and it just adds more overhead in maintenance. **Every test has a cost**. In our case, you can see that having two tests for this function is redundant. If it works for a slice of one size it's very likely it'll work for a slice of any size (within reason). Go's built-in testing toolkit features a [coverage tool](https://blog.golang.org/cover) , which can help identify areas of your code you have not covered. I do want to stress that having 100% coverage should not be your goal, it's just a tool to give you an idea of your coverage. If you have been strict with TDD, it's quite likely you'll have close to 100% coverage anyway. Try running `go test -cover` You should see Now delete one of the tests and check the coverage again. Now that we are happy we have a well-tested function you should commit your great work before taking on the next challenge. We need a new function called `SumAll` which will take a varying number of slices, returning a new slice containing the totals for each slice passed in. For example `SumAll([]int{1,2}, []int{0,9})` would return `[]int{3, 9}` or `SumAll([]int{1,1,1})` would return `[]int{3}` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-the-test-first-2) Write the test first ----------------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#try-and-run-the-test-1) Try and run the test ----------------------------------------------------------------------------------------------------------------------------------------- `./sum_test.go:23:9: undefined: SumAll` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-2) Write the minimal amount of code for the test to run and check the failing test output ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- We need to define SumAll according to what our test wants. Go can let you write [_variadic functions_](https://gobyexample.com/variadic-functions) that can take a variable number of arguments. Try to compile but our tests still don't compile! `./sum_test.go:26:9: invalid operation: got != want (slice can only be compared to nil)` Go does not let you use equality operators with slices. You _could_ write a function to iterate over each `got` and `want` slice and check their values but for convenience sake, we can use [`reflect.DeepEqual`](https://golang.org/pkg/reflect/#DeepEqual) which is useful for seeing if _any_ two variables are the same. (make sure you `import reflect` in the top of your file to have access to `DeepEqual`) It's important to note that `reflect.DeepEqual` is not "type safe", the code will compile even if you did something a bit silly. To see this in action, temporarily change the test to: What we have done here is try to compare a `slice` with a `string`. Which makes no sense, but the test compiles! So while using `reflect.DeepEqual` is a convenient way of comparing slices (and other things) you must be careful when using it. Change the test back again and run it, you should have test output looking like this `sum_test.go:30: got [] want [3 9]` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-enough-code-to-make-it-pass-2) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------------- What we need to do is iterate over the varargs, calculate the sum using our `Sum` function from before and then add it to the slice we will return Lots of new things to learn! There's a new way to create a slice. `make` allows you to create a slice with a starting capacity of the `len` of the `numbersToSum` we need to work through. You can index slices like arrays with `mySlice[N]` to get the value out or assign it a new value with `=` The tests should now pass [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#refactor-2) Refactor ----------------------------------------------------------------------------------------------------------------- As mentioned, slices have a capacity. If you have a slice with a capacity of 2 and try to do `mySlice[10] = 1` you will get a _runtime_ error. However, you can use the `append` function which takes a slice and a new value, returning a new slice with all the items in it. In this implementation, we are worrying less about capacity. We start with an empty slice `sums` and append to it the result of `Sum` as we work through the varargs. Our next requirement is to change `SumAll` to `SumAllTails`, where it now calculates the totals of the "tails" of each slice. The tail of a collection is all the items apart from the first one (the "head") [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-the-test-first-3) Write the test first ----------------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#try-and-run-the-test-2) Try and run the test ----------------------------------------------------------------------------------------------------------------------------------------- `./sum_test.go:26:9: undefined: SumAllTails` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-3) Write the minimal amount of code for the test to run and check the failing test output ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Rename the function to `SumAllTails` and re-run the test `sum_test.go:30: got [3 9] want [2 9]` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-enough-code-to-make-it-pass-3) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------------- Slices can be sliced! The syntax is `slice[low:high]` If you omit the value on one of the sides of the `:` it captures everything to the side of it. In our case, we are saying "take from 1 to the end" with `numbers[1:]`. You might want to invest some time in writing other tests around slices and experimenting with the slice operator so you can be familiar with it. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#refactor-3) Refactor ----------------------------------------------------------------------------------------------------------------- Not a lot to refactor this time. What do you think would happen if you passed in an empty slice into our function? What is the "tail" of an empty slice? What happens when you tell Go to capture all elements from `myEmptySlice[1:]`? [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-the-test-first-4) Write the test first ----------------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#try-and-run-the-test-3) Try and run the test ----------------------------------------------------------------------------------------------------------------------------------------- Oh no! It's important to note the test _has compiled_, it is a runtime error. Compile time errors are our friend because they help us write software that works, runtime errors are our enemies because they affect our users. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-enough-code-to-make-it-pass-4) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#refactor-4) Refactor ----------------------------------------------------------------------------------------------------------------- Our tests have some repeated code around assertion again, let's extract that into a function A handy side-effect of this is this adds a little type-safety to our code. If a silly developer adds a new test with `checkSums(t, got, "dave")` the compiler will stop them in their tracks. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#wrapping-up) Wrapping up --------------------------------------------------------------------------------------------------------------------- We have covered * Arrays * Slices * The various ways to make them * How they have a _fixed_ capacity but you can create new slices from old ones using `append` * How to slice, slices! * `len` to get the length of an array or slice * Test coverage tool * `reflect.DeepEqual` and why it's useful but can reduce the type-safety of your code We've used slices and arrays with integers but they work with any other type too, including arrays/slices themselves. So you can declare a variable of `[][]string` if you need to. [Check out the Go blog post on slices](https://blog.golang.org/go-slices-usage-and-internals) for an in-depth look into slices. Try writing more tests to demonstrate what you learn from reading it. 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 will not affect the original array. [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. [PreviousIteration](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/iteration) [NextStructs, methods & interfaces](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces) Last updated 6 years ago * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-the-test-first) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#try-to-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-enough-code-to-make-it-pass) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#refactor) * [Arrays and their type](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#arrays-and-their-type) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-the-test-first-1) * [Try and run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#try-and-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-enough-code-to-make-it-pass-1) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#refactor-1) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-the-test-first-2) * [Try and run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#try-and-run-the-test-1) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-2) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-enough-code-to-make-it-pass-2) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#refactor-2) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-the-test-first-3) * [Try and run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#try-and-run-the-test-2) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-3) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-enough-code-to-make-it-pass-3) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#refactor-3) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-the-test-first-4) * [Try and run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#try-and-run-the-test-3) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#write-enough-code-to-make-it-pass-4) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#refactor-4) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices#wrapping-up) Copy package main func Sum(numbers [5]int) int { return 0 } Copy func Sum(numbers [5]int) int { sum := 0 for i := 0; i < 5; i++ { sum += numbers[i] } return sum } Copy func Sum(numbers [5]int) int { sum := 0 for _, number := range numbers { sum += number } return sum } Copy func TestSum(t *testing.T) { t.Run("collection of 5 numbers", func(t *testing.T) { numbers := [5]int{1, 2, 3, 4, 5} got := Sum(numbers) want := 15 if got != want { t.Errorf("got %d want %d given, %v", got, want, numbers) } }) t.Run("collection of any size", func(t *testing.T) { numbers := []int{1, 2, 3} got := Sum(numbers) want := 6 if got != want { t.Errorf("got %d want %d given, %v", got, want, numbers) } }) } Copy func Sum(numbers []int) int { sum := 0 for _, number := range numbers { sum += number } return sum } Copy func TestSum(t *testing.T) { t.Run("collection of 5 numbers", func(t *testing.T) { numbers := []int{1, 2, 3, 4, 5} got := Sum(numbers) want := 15 if got != want { t.Errorf("got %d want %d given, %v", got, want, numbers) } }) t.Run("collection of any size", func(t *testing.T) { numbers := []int{1, 2, 3} got := Sum(numbers) want := 6 if got != want { t.Errorf("got %d want %d given, %v", got, want, numbers) } }) } Copy PASS coverage: 100.0% of statements Copy func TestSumAll(t *testing.T) { got := SumAll([]int{1, 2}, []int{0, 9}) want := []int{3, 9} if got != want { t.Errorf("got %v want %v", got, want) } } Copy func SumAll(numbersToSum ...[]int) (sums []int) { return } Copy func TestSumAll(t *testing.T) { got := SumAll([]int{1, 2}, []int{0, 9}) want := []int{3, 9} if !reflect.DeepEqual(got, want) { t.Errorf("got %v want %v", got, want) } } Copy func TestSumAll(t *testing.T) { got := SumAll([]int{1, 2}, []int{0, 9}) want := "bob" if !reflect.DeepEqual(got, want) { t.Errorf("got %v want %v", got, want) } } Copy func SumAll(numbersToSum ...[]int) []int { lengthOfNumbers := len(numbersToSum) sums := make([]int, lengthOfNumbers) for i, numbers := range numbersToSum { sums[i] = Sum(numbers) } return sums } Copy func SumAll(numbersToSum ...[]int) []int { var sums []int for _, numbers := range numbersToSum { sums = append(sums, Sum(numbers)) } return sums } Copy func TestSumAllTails(t *testing.T) { got := SumAllTails([]int{1, 2}, []int{0, 9}) want := []int{2, 9} if !reflect.DeepEqual(got, want) { t.Errorf("got %v want %v", got, want) } } Copy func SumAllTails(numbersToSum ...[]int) []int { var sums []int for _, numbers := range numbersToSum { tail := numbers[1:] sums = append(sums, Sum(tail)) } return sums } Copy func TestSumAllTails(t *testing.T) { t.Run("make the sums of some slices", func(t *testing.T) { got := SumAllTails([]int{1, 2}, []int{0, 9}) want := []int{2, 9} if !reflect.DeepEqual(got, want) { t.Errorf("got %v want %v", got, want) } }) t.Run("safely sum empty slices", func(t *testing.T) { got := SumAllTails([]int{}, []int{3, 4, 5}) want := []int{0, 9} if !reflect.DeepEqual(got, want) { t.Errorf("got %v want %v", got, want) } }) } Copy panic: runtime error: slice bounds out of range [recovered] panic: runtime error: slice bounds out of range Copy func SumAllTails(numbersToSum ...[]int) []int { var sums []int for _, numbers := range numbersToSum { if len(numbers) == 0 { sums = append(sums, 0) } else { tail := numbers[1:] sums = append(sums, Sum(tail)) } } return sums } Copy func TestSumAllTails(t *testing.T) { checkSums := func(t *testing.T, got, want []int) { t.Helper() if !reflect.DeepEqual(got, want) { t.Errorf("got %v want %v", got, want) } } t.Run("make the sums of tails of", func(t *testing.T) { got := SumAllTails([]int{1, 2}, []int{0, 9}) want := []int{2, 9} checkSums(t, got, want) }) t.Run("safely sum empty slices", func(t *testing.T) { got := SumAllTails([]int{}, []int{3, 4, 5}) want := []int{0, 9} checkSums(t, got, want) }) } Copy $ go test ./sum_test.go:52:21: cannot use "dave" (type string) as type []int in argument to checkSums --- # Structs, methods & interfaces | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces.md) . [**You can find all the code for this chapter here**](https://github.com/quii/learn-go-with-tests/tree/master/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`. The TDD cycle should be pretty familiar to you by now. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#write-the-test-first) Write the test first ---------------------------------------------------------------------------------------------------------------------------------------------------- Copy func TestPerimeter(t *testing.T) { got := Perimeter(10.0, 10.0) want := 40.0 if got != want { t.Errorf("got %.2f want %.2f", got, want) } } Notice the new format string? The `f` is for our `float64` and the `.2` means print 2 decimal places. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#try-to-run-the-test) Try to run the test -------------------------------------------------------------------------------------------------------------------------------------------------- `./shapes_test.go:6:9: undefined: Perimeter` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Results in `shapes_test.go:10: got 0.00 want 40.00`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#write-enough-code-to-make-it-pass) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ So far, so easy. Now let's create a function called `Area(width, height float64)` which returns the area of a rectangle. Try to do it yourself, following the TDD cycle. You should end up with tests like this And code like this [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#refactor) Refactor ---------------------------------------------------------------------------------------------------------------------------- Our code does the job, but it doesn't contain anything explicit about rectangles. An unwary developer might try to supply the width and height of a triangle to these functions without realising they will return the wrong answer. We could just give the functions more specific names like `RectangleArea`. A neater solution is to define our own _type_ called `Rectangle` which encapsulates this concept for us. We can create a simple type using a **struct**. [A struct](https://golang.org/ref/spec#Struct_types) is just a named collection of fields where you can store data. Declare a struct like this Now let's refactor the tests to use `Rectangle` instead of plain `float64`s. Remember to run your tests before attempting to fix, you should get a helpful error like You can access the fields of a struct with the syntax of `myStruct.field`. Change the two functions to fix the test. I hope you'll agree that passing a `Rectangle` to a function conveys our intent more clearly but there are more benefits of using structs that we will get on to. Our next requirement is to write an `Area` function for circles. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#write-the-test-first-1) Write the test first ------------------------------------------------------------------------------------------------------------------------------------------------------ As you can see, the 'f' has been replaced by 'g', using 'f' it could be difficult to know the exact decimal number, with 'g' we get a complete decimal number in the error message ([fmt options](https://golang.org/pkg/fmt/) ). [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#try-to-run-the-test-1) Try to run the test ---------------------------------------------------------------------------------------------------------------------------------------------------- `./shapes_test.go:28:13: undefined: Circle` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) Write the minimal amount of code for the test to run and check the failing test output ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ We need to define our `Circle` type. Now try to run the tests again `./shapes_test.go:29:14: cannot use circle (type Circle) as type Rectangle in argument to Area` Some programming languages allow you to do something like this: But you cannot in Go `./shapes.go:20:32: Area redeclared in this block` We have two choices: * You can have functions with the same name declared in different _packages_. So we could create our `Area(Circle)` in a new package, but that feels overkill here. * We can define [_methods_](https://golang.org/ref/spec#Method_declarations) on our newly defined types instead. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#what-are-methods) What are methods? So far we have only been writing _functions_ but we have been using some methods. When we call `t.Errorf` we are calling the method `Errorf` on the instance of our `t` (`testing.T`). A method is a function with a receiver. A method declaration binds an identifier, the method name, to a method, and associates the method with the receiver's base type. Methods are very similar to functions but they are called by invoking them on an instance of a particular type. Where you can just call functions wherever you like, such as `Area(rectangle)` you can only call methods on "things". An example will help so let's change our tests first to call methods instead and then fix the code. If we try to run the tests, we get > type Circle has no field or method Area I would like to reiterate how great the compiler is here. It is so important to take the time to slowly read the error messages you get, it will help you in the long run. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-2) Write the minimal amount of code for the test to run and check the failing test output ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Let's add some methods to our types The syntax for declaring methods is almost the same as functions and that's because they're so similar. The only difference is the syntax of the method receiver `func (receiverName ReceiverType) MethodName(args)`. When your method is called on a variable of that type, you get your reference to its data via the `receiverName` variable. In many other programming languages this is done implicitly and you access the receiver via `this`. It is a convention in Go to have the receiver variable be the first letter of the type. If you try to re-run the tests they should now compile and give you some failing output. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#write-enough-code-to-make-it-pass-1) Write enough code to make it pass -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Now let's make our rectangle tests pass by fixing our new method If you re-run the tests the rectangle tests should be passing but circle should still be failing. To make circle's `Area` function pass we will borrow the `Pi` constant from the `math` package (remember to import it). [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#refactor-1) Refactor ------------------------------------------------------------------------------------------------------------------------------ There is some duplication in our tests. All we want to do is take a collection of _shapes_, call the `Area()` method on them and then check the result. We want to be able to write some kind of `checkArea` function that we can pass both `Rectangle`s and `Circle`s to, but fail to compile if we try to pass in something that isn't a shape. With Go, we can codify this intent with **interfaces**. [Interfaces](https://golang.org/ref/spec#Interface_types) are a very powerful concept in statically typed languages like Go because they allow you to make functions that can be used with different types and create highly-decoupled code whilst still maintaining type-safety. Let's introduce this by refactoring our tests. We are creating a helper function like we have in other exercises but this time we are asking for a `Shape` to be passed in. If we try to call this with something that isn't a shape, then it will not compile. How does something become a shape? We just tell Go what a `Shape` is using an interface declaration We're creating a new `type` just like we did with `Rectangle` and `Circle` but this time it is an `interface` rather than a `struct`. Once you add this to the code, the tests will pass. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#wait-what) Wait, what? This is quite different to interfaces in most other programming languages. Normally you have to write code to say `My type Foo implements interface Bar`. But in our case * `Rectangle` has a method called `Area` that returns a `float64` so it satisfies the `Shape` interface * `Circle` has a method called `Area` that returns a `float64` so it satisfies the `Shape` interface * `string` does not have such a method, so it doesn't satisfy the interface * etc. In Go **interface resolution is implicit**. If the type you pass in matches what the interface is asking for, it will compile. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#decoupling) Decoupling Notice how our helper does not need to concern itself with whether the shape is a `Rectangle` or a `Circle` or a `Triangle`. By declaring an interface the helper is _decoupled_ from the concrete types and just has the method it needs to do its job. This kind of approach of using interfaces to declare **only what you need** is very important in software design and will be covered in more detail in later sections. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#further-refactoring) Further refactoring -------------------------------------------------------------------------------------------------------------------------------------------------- Now that you have some understanding of structs we can introduce "table driven tests". [Table driven tests](https://github.com/golang/go/wiki/TableDrivenTests) are useful when you want to build a list of test cases that can be tested in the same manner. The only new syntax here is creating an "anonymous struct", areaTests. We are declaring a slice of structs by using `[]struct` with two fields, the `shape` and the `want`. Then we fill the slice with cases. We then iterate over them just like we do any other slice, using the struct fields to run our tests. You can see how it would be very easy for a developer to introduce a new shape, implement `Area` and then add it to the test cases. In addition, if a bug is found with `Area` it is very easy to add a new test case to exercise it before fixing it. Table based tests can be a great item in your toolbox but be sure that you have a need for the extra noise in the tests. If you wish to test various implementations of an interface, or if the data being passed in to a function has lots of different requirements that need testing then they are a great fit. Let's demonstrate all this by adding another shape and testing it; a triangle. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#write-the-test-first-2) Write the test first ------------------------------------------------------------------------------------------------------------------------------------------------------ Adding a new test for our new shape is very easy. Just add `{Triangle{12, 6}, 36.0},` to our list. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#try-to-run-the-test-2) Try to run the test ---------------------------------------------------------------------------------------------------------------------------------------------------- Remember, keep trying to run the test and let the compiler guide you toward a solution. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-3) Write the minimal amount of code for the test to run and check the failing test output ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ `./shapes_test.go:25:4: undefined: Triangle` We have not defined Triangle yet Try again It's telling us we cannot use a Triangle as a shape because it does not have an `Area()` method, so add an empty implementation to get the test working Finally the code compiles and we get our error `shapes_test.go:31: got 0.00 want 36.00` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#write-enough-code-to-make-it-pass-2) Write enough code to make it pass -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- And our tests pass! [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#refactor-2) Refactor ------------------------------------------------------------------------------------------------------------------------------ Again, the implementation is fine but our tests could do with some improvement. When you scan this It's not immediately clear what all the numbers represent and you should be aiming for your tests to be easily understood. So far you've only been shown syntax for creating instances of structs `MyStruct{val1, val2}` but you can optionally name the fields. Let's see what it looks like In [Test-Driven Development by Example](https://g.co/kgs/yCzDLF) Kent Beck refactors some tests to a point and asserts: > The test speaks to us more clearly, as if it were an assertion of truth, **not a sequence of operations** (emphasis mine) Now our tests (at least the list of cases) make assertions of truth about shapes and their areas. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#make-sure-your-test-output-is-helpful) Make sure your test output is helpful -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Remember earlier when we were implementing `Triangle` and we had the failing test? It printed `shapes_test.go:31: got 0.00 want 36.00`. We knew this was in relation to `Triangle` because we were just working with it, but what if a bug slipped in to the system in one of 20 cases in the table? How would a developer know which case failed? This is not a great experience for the developer, they will have to manually look through the cases to find out which case actually failed. We can change our error message into `%#v got %.2f want %.2f`. The `%#v` format string will print out our struct with the values in its field, so the developer can see at a glance the properties that are being tested. To increase the readability of our test cases further we can rename the `want` field into something more descriptive like `hasArea`. One final tip with table driven tests is to use `t.Run` and to name the test cases. By wrapping each case in a `t.Run` you will have clearer test output on failures as it will print the name of the case And you can run specific tests within your table with `go test -run TestArea/Rectangle`. Here is our final test code which captures this [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#wrapping-up) Wrapping up ---------------------------------------------------------------------------------------------------------------------------------- This was more TDD practice, iterating over our solutions to basic mathematic problems and learning new language features motivated by our tests. * Declaring structs to create your own data types which lets you bundle related data together and make the intent of your code clearer * Declaring interfaces so you can define functions that can be used by different types ([parametric polymorphism](https://en.wikipedia.org/wiki/Parametric_polymorphism) ) * Adding methods so you can add functionality to your data types and so you can implement interfaces * Table based tests to make your assertions clearer and your suites easier to extend & maintain This was an important chapter because we are now starting to define our own types. In statically typed languages like Go, being able to design your own types is essential for building software that is easy to understand, to piece together and to test. Interfaces are a great tool for hiding complexity away from other parts of the system. In our case our test helper _code_ did not need to know the exact shape it was asserting on, only how to "ask" for it's area. As you become more familiar with Go you start to see the real strength of interfaces and the standard library. You'll learn about interfaces defined in the standard library that are used _everywhere_ and by implementing them against your own types you can very quickly re-use a lot of great functionality. [PreviousArrays and slices](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices) [NextPointers & errors](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors) Last updated 6 years ago * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#write-the-test-first) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#try-to-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#write-enough-code-to-make-it-pass) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#refactor) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#write-the-test-first-1) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#try-to-run-the-test-1) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) * [What are methods?](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#what-are-methods) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-2) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#write-enough-code-to-make-it-pass-1) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#refactor-1) * [Wait, what?](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#wait-what) * [Decoupling](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#decoupling) * [Further refactoring](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#further-refactoring) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#write-the-test-first-2) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#try-to-run-the-test-2) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-3) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#write-enough-code-to-make-it-pass-2) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#refactor-2) * [Make sure your test output is helpful](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#make-sure-your-test-output-is-helpful) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces#wrapping-up) Copy func Perimeter(width float64, height float64) float64 { return 0 } Copy func Perimeter(width float64, height float64) float64 { return 2 * (width + height) } Copy func TestPerimeter(t *testing.T) { got := Perimeter(10.0, 10.0) want := 40.0 if got != want { t.Errorf("got %.2f want %.2f", got, want) } } func TestArea(t *testing.T) { got := Area(12.0, 6.0) want := 72.0 if got != want { t.Errorf("got %.2f want %.2f", got, want) } } Copy func Perimeter(width float64, height float64) float64 { return 2 * (width + height) } func Area(width float64, height float64) float64 { return width * height } Copy type Rectangle struct { Width float64 Height float64 } Copy func TestPerimeter(t *testing.T) { rectangle := Rectangle{10.0, 10.0} got := Perimeter(rectangle) want := 40.0 if got != want { t.Errorf("got %.2f want %.2f", got, want) } } func TestArea(t *testing.T) { rectangle := Rectangle{12.0, 6.0} got := Area(rectangle) want := 72.0 if got != want { t.Errorf("got %.2f want %.2f", got, want) } } Copy ./shapes_test.go:7:18: not enough arguments in call to Perimeter have (Rectangle) want (float64, float64) Copy func Perimeter(rectangle Rectangle) float64 { return 2 * (rectangle.Width + rectangle.Height) } func Area(rectangle Rectangle) float64 { return rectangle.Width * rectangle.Height } Copy func TestArea(t *testing.T) { t.Run("rectangles", func(t *testing.T) { rectangle := Rectangle{12, 6} got := Area(rectangle) want := 72.0 if got != want { t.Errorf("got %g want %g", got, want) } }) t.Run("circles", func(t *testing.T) { circle := Circle{10} got := Area(circle) want := 314.1592653589793 if got != want { t.Errorf("got %g want %g", got, want) } }) } Copy type Circle struct { Radius float64 } Copy func Area(circle Circle) float64 { ... } func Area(rectangle Rectangle) float64 { ... } Copy func TestArea(t *testing.T) { t.Run("rectangles", func(t *testing.T) { rectangle := Rectangle{12, 6} got := rectangle.Area() want := 72.0 if got != want { t.Errorf("got %g want %g", got, want) } }) t.Run("circles", func(t *testing.T) { circle := Circle{10} got := circle.Area() want := 314.1592653589793 if got != want { t.Errorf("got %g want %g", got, want) } }) } Copy ./shapes_test.go:19:19: rectangle.Area undefined (type Rectangle has no field or method Area) ./shapes_test.go:29:16: circle.Area undefined (type Circle has no field or method Area) Copy type Rectangle struct { Width float64 Height float64 } func (r Rectangle) Area() float64 { return 0 } type Circle struct { Radius float64 } func (c Circle) Area() float64 { return 0 } Copy r Rectangle Copy func (r Rectangle) Area() float64 { return r.Width * r.Height } Copy func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius } Copy func TestArea(t *testing.T) { checkArea := func(t *testing.T, shape Shape, want float64) { t.Helper() got := shape.Area() if got != want { t.Errorf("got %g want %g", got, want) } } t.Run("rectangles", func(t *testing.T) { rectangle := Rectangle{12, 6} checkArea(t, rectangle, 72.0) }) t.Run("circles", func(t *testing.T) { circle := Circle{10} checkArea(t, circle, 314.1592653589793) }) } Copy type Shape interface { Area() float64 } Copy func TestArea(t *testing.T) { areaTests := []struct { shape Shape want float64 }{ {Rectangle{12, 6}, 72.0}, {Circle{10}, 314.1592653589793}, } for _, tt := range areaTests { got := tt.shape.Area() if got != tt.want { t.Errorf("got %g want %g", got, tt.want) } } } Copy func TestArea(t *testing.T) { areaTests := []struct { shape Shape want float64 }{ {Rectangle{12, 6}, 72.0}, {Circle{10}, 314.1592653589793}, {Triangle{12, 6}, 36.0}, } for _, tt := range areaTests { got := tt.shape.Area() if got != tt.want { t.Errorf("got %g want %g", got, tt.want) } } } Copy type Triangle struct { Base float64 Height float64 } Copy ./shapes_test.go:25:8: cannot use Triangle literal (type Triangle) as type Shape in field value: Triangle does not implement Shape (missing Area method) Copy func (t Triangle) Area() float64 { return 0 } Copy func (t Triangle) Area() float64 { return (t.Base * t.Height) * 0.5 } Copy {Rectangle{12, 6}, 72.0}, {Circle{10}, 314.1592653589793}, {Triangle{12, 6}, 36.0}, Copy {shape: Rectangle{Width: 12, Height: 6}, want: 72.0}, {shape: Circle{Radius: 10}, want: 314.1592653589793}, {shape: Triangle{Base: 12, Height: 6}, want: 36.0}, Copy --- FAIL: TestArea (0.00s) --- FAIL: TestArea/Rectangle (0.00s) shapes_test.go:33: main.Rectangle{Width:12, Height:6} got 72.00 want 72.10 Copy func TestArea(t *testing.T) { areaTests := []struct { name string shape Shape hasArea float64 }{ {name: "Rectangle", shape: Rectangle{Width: 12, Height: 6}, hasArea: 72.0}, {name: "Circle", shape: Circle{Radius: 10}, hasArea: 314.1592653589793}, {name: "Triangle", shape: Triangle{Base: 12, Height: 6}, hasArea: 36.0}, } for _, tt := range areaTests { // using tt.name from the case to use it as the `t.Run` test name t.Run(tt.name, func(t *testing.T) { got := tt.shape.Area() if got != tt.hasArea { t.Errorf("%#v got %g want %g", tt.shape, got, tt.hasArea) } }) } } --- # Intro | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/app-intro.md) . Now that you have hopefully digested the _Go Fundamentals_ section you have a solid grounding of a majority of Go's language features and how to do TDD. This next section will involve building an application. Each chapter will iterate on the previous one, expanding the application's functionality as our product owner dictates. New concepts will be introduced to help facilitate writing great code but most of the new material will be learning what can be accomplished from Go's standard library. By the end of this you should have a strong grasp as to how to iteratively write an application in Go, backed by tests. * [HTTP server](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server) - We will create an application which listens to HTTP requests and responds to them. * [JSON, routing and embedding](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json) - We will make our endpoints return JSON, explore how to do routing and learn about type embedding. * [IO](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io) \- We will persist and read our data from disk and we'll cover sorting data. * [Command line](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line) - We will create a new program leveraging the code we've made so far to make a command line interface. This will involve us restructuring our project to support multiple binaries * [Time](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time) - We will schedule some activities that happen at different times depending on user input. [PreviousMaths](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/math) [NextHTTP server](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server) Last updated 8 years ago --- # Pointers & errors | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors.md) . [**You can find all the code for this chapter here**](https://github.com/quii/learn-go-with-tests/tree/master/pointers) We learned about structs in the last section which let us capture a number of values related around a concept. At some point you may wish to use structs to manage state, exposing methods to let users change the state in a way that you can control. **Fintech loves Go** and uhhh bitcoins? So let's show what an amazing banking system we can make. Let's make a `Wallet` struct which let's us deposit `Bitcoin`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#write-the-test-first) Write the test first ----------------------------------------------------------------------------------------------------------------------------------------- Copy func TestWallet(t *testing.T) { wallet := Wallet{} wallet.Deposit(10) got := wallet.Balance() want := 10 if got != want { t.Errorf("got %d want %d", got, want) } } In the [previous example](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces) we accessed fields directly with the field name, however in our _very secure wallet_ we don't want to expose our inner state to the rest of the world. We want to control access via methods. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#try-to-run-the-test) Try to run the test --------------------------------------------------------------------------------------------------------------------------------------- `./wallet_test.go:7:12: undefined: Wallet` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- The compiler doesn't know what a `Wallet` is so let's tell it. Now we've made our wallet, try and run the test again We need to define these methods. Remember to only do enough to make the tests run. We need to make sure our test fails correctly with a clear error message. If this syntax is unfamiliar go back and read the structs section. The tests should now compile and run `wallet_test.go:15: got 0 want 10` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#write-enough-code-to-make-it-pass) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------------- We will need some kind of _balance_ variable in our struct to store the state In Go if a symbol (so variables, types, functions et al) starts with a lowercase symbol then it is private _outside the package it's defined in_. In our case we want our methods to be able to manipulate this value but no one else. Remember we can access the internal `balance` field in the struct using the "receiver" variable. With our career in fintech secured, run our tests and bask in the passing test `wallet_test.go:15: got 0 want 10` ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#undefined) ???? Well this is confusing, our code looks like it should work, we add the new amount onto our balance and then the balance method should return the current state of it. In Go, **when you call a function or a method the arguments are** _**copied**_. When calling `func (w Wallet) Deposit(amount int)` the `w` is a copy of whatever we called the method from. Without getting too computer-sciency, when you create a value - like a wallet, it is stored somewhere in memory. You can find out what the _address_ of that bit of memory with `&myVal`. Experiment by adding some prints to your code The `\n` escape character, prints new line after outputting the memory address. We get the pointer to a thing with the address of symbol; `&`. Now re-run the test You can see that the addresses of the two balances are different. So when we change the value of the balance inside the code, we are working on a copy of what came from the test. Therefore the balance in the test is unchanged. We can fix this with _pointers_. [Pointers](https://gobyexample.com/pointers) let us _point_ to some values and then let us change them. So rather than taking a copy of the Wallet, we take a pointer to the wallet so we can change it. The difference is the receiver type is `*Wallet` rather than `Wallet` which you can read as "a pointer to a wallet". Try and re-run the tests and they should pass. Now you might wonder, why did they pass? We didn't dereference the pointer in the function, like so: and seemingly addressed the object directly. In fact, the code above using `(*w)` is absolutely valid. However, the makers of Go deemed this notation cumbersome, so the language permits us to write `w.balance`, without explicit dereference. These pointers to structs even have their own name: _struct pointers_ and they are [automatically dereferenced](https://golang.org/ref/spec#Method_values) . Technically you do not need to change `Balance` to use a pointer receiver as taking a copy of the balance is fine. However by convention you should keep your method receiver types to be the same for consistency. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#refactor) Refactor ----------------------------------------------------------------------------------------------------------------- We said we were making a Bitcoin wallet but we have not mentioned them so far. We've been using `int` because they're a good type for counting things! It seems a bit overkill to create a `struct` for this. `int` is fine in terms of the way it works but it's not descriptive. Go lets you create new types from existing ones. The syntax is `type MyName OriginalType` To make `Bitcoin` you just use the syntax `Bitcoin(999)`. By doing this we're making a new type and we can declare _methods_ on them. This can be very useful when you want to add some domain specific functionality on top of existing types. Let's implement [Stringer](https://golang.org/pkg/fmt/#Stringer) on Bitcoin This interface is defined in the `fmt` package and lets you define how your type is printed when used with the `%s` format string in prints. As you can see, the syntax for creating a method on a type alias is the same as it is on a struct. Next we need to update our test format strings so they will use `String()` instead. To see this in action, deliberately break the test so we can see it `wallet_test.go:18: got 10 BTC want 20 BTC` This makes it clearer what's going on in our test. The next requirement is for a `Withdraw` function. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#write-the-test-first-1) Write the test first ------------------------------------------------------------------------------------------------------------------------------------------- Pretty much the opposite of `Deposit()` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#try-to-run-the-test-1) Try to run the test ----------------------------------------------------------------------------------------------------------------------------------------- `./wallet_test.go:26:9: wallet.Withdraw undefined (type Wallet has no field or method Withdraw)` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) Write the minimal amount of code for the test to run and check the failing test output ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- `wallet_test.go:33: got 20 BTC want 10 BTC` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#write-enough-code-to-make-it-pass-1) Write enough code to make it pass --------------------------------------------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#refactor-1) Refactor ------------------------------------------------------------------------------------------------------------------- There's some duplication in our tests, lets refactor that out. What should happen if you try to `Withdraw` more than is left in the account? For now, our requirement is to assume there is not an overdraft facility. How do we signal a problem when using `Withdraw` ? In Go, if you want to indicate an error it is idiomatic for your function to return an `err` for the caller to check and act on. Let's try this out in a test. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#write-the-test-first-2) Write the test first ------------------------------------------------------------------------------------------------------------------------------------------- We want `Withdraw` to return an error _if_ you try to take out more than you have and the balance should stay the same. We then check an error has returned by failing the test if it is `nil`. `nil` is synonymous with `null` from other programming languages. Errors can be `nil` because the return type of `Withdraw` will be `error`, which is an interface. If you see a function that takes arguments or returns values that are interfaces, they can be nillable. Like `null` if you try to access a value that is `nil` it will throw a **runtime panic**. This is bad! You should make sure that you check for nils. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#try-and-run-the-test) Try and run the test ----------------------------------------------------------------------------------------------------------------------------------------- `./wallet_test.go:31:25: wallet.Withdraw(Bitcoin(100)) used as value` The wording is perhaps a little unclear, but our previous intent with `Withdraw` was just to call it, it will never return a value. To make this compile we will need to change it so it has a return type. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-2) Write the minimal amount of code for the test to run and check the failing test output ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Again, it is very important to just write enough code to satisfy the compiler. We correct our `Withdraw` method to return `error` and for now we have to return _something_ so let's just return `nil`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#write-enough-code-to-make-it-pass-2) Write enough code to make it pass --------------------------------------------------------------------------------------------------------------------------------------------------------------------- Remember to import `errors` into your code. `errors.New` creates a new `error` with a message of your choosing. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#refactor-2) Refactor ------------------------------------------------------------------------------------------------------------------- Let's make a quick test helper for our error check just to help our test read clearer And in our test Hopefully when returning an error of "oh no" you were thinking that we _might_ iterate on that because it doesn't seem that useful to return. Assuming that the error ultimately gets returned to the user, let's update our test to assert on some kind of error message rather than just the existence of an error. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#write-the-test-first-3) Write the test first ------------------------------------------------------------------------------------------------------------------------------------------- Update our helper for a `string` to compare against. And then update the caller We've introduced `t.Fatal` which will stop the test if it is called. This is because we don't want to make any more assertions on the error returned if there isn't one around. Without this the test would carry on to the next step and panic because of a nil pointer. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#try-to-run-the-test-2) Try to run the test ----------------------------------------------------------------------------------------------------------------------------------------- `wallet_test.go:61: got err 'oh no' want 'cannot withdraw, insufficient funds'` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#write-enough-code-to-make-it-pass-3) Write enough code to make it pass --------------------------------------------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#refactor-3) Refactor ------------------------------------------------------------------------------------------------------------------- We have duplication of the error message in both the test code and the `Withdraw` code. It would be really annoying for the test to fail if someone wanted to re-word the error and it's just too much detail for our test. We don't _really_ care what the exact wording is, just that some kind of meaningful error around withdrawing is returned given a certain condition. In Go, errors are values, so we can refactor it out into a variable and have a single source of truth for it. The `var` keyword allows us to define values global to the package. This is a positive change in itself because now our `Withdraw` function looks very clear. Next we can refactor our test code to use this value instead of specific strings. And now the test is easier to follow too. I have moved the helpers out of the main test function just so when someone opens up a file they can start reading our assertions first, rather than some helpers. Another useful property of tests is that they help us understand the _real_ usage of our code so we can make sympathetic code. We can see here that a developer can simply call our code and do an equals check to `ErrInsufficientFunds` and act accordingly. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#unchecked-errors) Unchecked errors Whilst the Go compiler helps you a lot, sometimes there are things you can still miss and error handling can sometimes be tricky. There is one scenario we have not tested. To find it, run the following in a terminal to install `errcheck`, one of many linters available for Go. `go get -u github.com/kisielk/errcheck` Then, inside the directory with your code run `errcheck .` You should get something like `wallet_test.go:17:18: wallet.Withdraw(Bitcoin(10))` What this is telling us is that we have not checked the error being returned on that line of code. That line of code on my computer corresponds to our normal withdraw scenario because we have not checked that if the `Withdraw` is successful that an error is _not_ returned. Here is the final test code that accounts for this. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#wrapping-up) Wrapping up ----------------------------------------------------------------------------------------------------------------------- ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#pointers) Pointers * Go copies values when you pass them to functions/methods so if you're writing a function that needs to mutate state you'll need it to take a pointer to the thing you want to change. * The fact that Go takes a copy of values is useful a lot of the time but sometimes you won't want your system to make a copy of something, in which case you need to pass a reference. Examples could be very large data or perhaps things you intend only to have one instance of (like database connection pools). ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#nil) nil * Pointers can be nil * When a function returns a pointer to something, you need to make sure you check if it's nil or you might raise a runtime exception, the compiler won't help you here. * Useful for when you want to describe a value that could be missing ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#errors) Errors * Errors are the way to signify failure when calling a function/method. * By listening to our tests we concluded that checking for a string in an error would result in a flaky test. So we refactored to use a meaningful value instead and this resulted in easier to test code and concluded this would be easier for users of our API too. * This is not the end of the story with error handling, you can do more sophisticated things but this is just an intro. Later sections will cover more strategies. * [Don’t just check errors, handle them gracefully](https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully) ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#create-new-types-from-existing-ones) Create new types from existing ones * Useful for adding more domain specific meaning to values * Can let you implement interfaces Pointers and errors are a big part of writing Go that you need to get comfortable with. Thankfully the compiler will _usually_ help you out if you do something wrong, just take your time and read the error. [PreviousStructs, methods & interfaces](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces) [NextMaps](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps) Last updated 6 years ago * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#write-the-test-first) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#try-to-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#write-enough-code-to-make-it-pass) * [????](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#undefined) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#refactor) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#write-the-test-first-1) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#try-to-run-the-test-1) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#write-enough-code-to-make-it-pass-1) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#refactor-1) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#write-the-test-first-2) * [Try and run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#try-and-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-2) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#write-enough-code-to-make-it-pass-2) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#refactor-2) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#write-the-test-first-3) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#try-to-run-the-test-2) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#write-enough-code-to-make-it-pass-3) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#refactor-3) * [Unchecked errors](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#unchecked-errors) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#wrapping-up) * [Pointers](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#pointers) * [nil](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#nil) * [Errors](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#errors) * [Create new types from existing ones](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors#create-new-types-from-existing-ones) Copy type Wallet struct { } Copy ./wallet_test.go:9:8: wallet.Deposit undefined (type Wallet has no field or method Deposit) ./wallet_test.go:11:15: wallet.Balance undefined (type Wallet has no field or method Balance) Copy func (w Wallet) Deposit(amount int) { } func (w Wallet) Balance() int { return 0 } Copy type Wallet struct { balance int } Copy func (w Wallet) Deposit(amount int) { w.balance += amount } func (w Wallet) Balance() int { return w.balance } Copy func TestWallet(t *testing.T) { wallet := Wallet{} wallet.Deposit(10) got := wallet.Balance() fmt.Printf("address of balance in test is %v \n", &wallet.balance) want := 10 if got != want { t.Errorf("got %d want %d", got, want) } } Copy func (w Wallet) Deposit(amount int) { fmt.Printf("address of balance in Deposit is %v \n", &w.balance) w.balance += amount } Copy address of balance in Deposit is 0xc420012268 address of balance in test is 0xc420012260 Copy func (w *Wallet) Deposit(amount int) { w.balance += amount } func (w *Wallet) Balance() int { return w.balance } Copy func (w *Wallet) Balance() int { return (*w).balance } Copy type Bitcoin int type Wallet struct { balance Bitcoin } func (w *Wallet) Deposit(amount Bitcoin) { w.balance += amount } func (w *Wallet) Balance() Bitcoin { return w.balance } Copy func TestWallet(t *testing.T) { wallet := Wallet{} wallet.Deposit(Bitcoin(10)) got := wallet.Balance() want := Bitcoin(10) if got != want { t.Errorf("got %d want %d", got, want) } } Copy type Stringer interface { String() string } Copy func (b Bitcoin) String() string { return fmt.Sprintf("%d BTC", b) } Copy if got != want { t.Errorf("got %s want %s", got, want) } Copy func TestWallet(t *testing.T) { t.Run("Deposit", func(t *testing.T) { wallet := Wallet{} wallet.Deposit(Bitcoin(10)) got := wallet.Balance() want := Bitcoin(10) if got != want { t.Errorf("got %s want %s", got, want) } }) t.Run("Withdraw", func(t *testing.T) { wallet := Wallet{balance: Bitcoin(20)} wallet.Withdraw(Bitcoin(10)) got := wallet.Balance() want := Bitcoin(10) if got != want { t.Errorf("got %s want %s", got, want) } }) } Copy func (w *Wallet) Withdraw(amount Bitcoin) { } Copy func (w *Wallet) Withdraw(amount Bitcoin) { w.balance -= amount } Copy func TestWallet(t *testing.T) { assertBalance := func(t *testing.T, wallet Wallet, want Bitcoin) { t.Helper() got := wallet.Balance() if got != want { t.Errorf("got %s want %s", got, want) } } t.Run("Deposit", func(t *testing.T) { wallet := Wallet{} wallet.Deposit(Bitcoin(10)) assertBalance(t, wallet, Bitcoin(10)) }) t.Run("Withdraw", func(t *testing.T) { wallet := Wallet{balance: Bitcoin(20)} wallet.Withdraw(Bitcoin(10)) assertBalance(t, wallet, Bitcoin(10)) }) } Copy t.Run("Withdraw insufficient funds", func(t *testing.T) { startingBalance := Bitcoin(20) wallet := Wallet{startingBalance} err := wallet.Withdraw(Bitcoin(100)) assertBalance(t, wallet, startingBalance) if err == nil { t.Error("wanted an error but didn't get one") } }) Copy func (w *Wallet) Withdraw(amount Bitcoin) error { w.balance -= amount return nil } Copy func (w *Wallet) Withdraw(amount Bitcoin) error { if amount > w.balance { return errors.New("oh no") } w.balance -= amount return nil } Copy assertError := func(t *testing.T, err error) { t.Helper() if err == nil { t.Error("wanted an error but didn't get one") } } Copy t.Run("Withdraw insufficient funds", func(t *testing.T) { wallet := Wallet{Bitcoin(20)} err := wallet.Withdraw(Bitcoin(100)) assertBalance(t, wallet, Bitcoin(20)) assertError(t, err) }) Copy assertError := func(t *testing.T, got error, want string) { t.Helper() if got == nil { t.Fatal("didn't get an error but wanted one") } if got.Error() != want { t.Errorf("got %q, want %q", got, want) } } Copy t.Run("Withdraw insufficient funds", func(t *testing.T) { startingBalance := Bitcoin(20) wallet := Wallet{startingBalance} err := wallet.Withdraw(Bitcoin(100)) assertBalance(t, wallet, startingBalance) assertError(t, err, "cannot withdraw, insufficient funds") }) Copy func (w *Wallet) Withdraw(amount Bitcoin) error { if amount > w.balance { return errors.New("cannot withdraw, insufficient funds") } w.balance -= amount return nil } Copy var ErrInsufficientFunds = errors.New("cannot withdraw, insufficient funds") func (w *Wallet) Withdraw(amount Bitcoin) error { if amount > w.balance { return ErrInsufficientFunds } w.balance -= amount return nil } Copy func TestWallet(t *testing.T) { t.Run("Deposit", func(t *testing.T) { wallet := Wallet{} wallet.Deposit(Bitcoin(10)) assertBalance(t, wallet, Bitcoin(10)) }) t.Run("Withdraw with funds", func(t *testing.T) { wallet := Wallet{Bitcoin(20)} wallet.Withdraw(Bitcoin(10)) assertBalance(t, wallet, Bitcoin(10)) }) t.Run("Withdraw insufficient funds", func(t *testing.T) { wallet := Wallet{Bitcoin(20)} err := wallet.Withdraw(Bitcoin(100)) assertBalance(t, wallet, Bitcoin(20)) assertError(t, err, ErrInsufficientFunds) }) } func assertBalance(t *testing.T, wallet Wallet, want Bitcoin) { t.Helper() got := wallet.Balance() if got != want { t.Errorf("got %q want %q", got, want) } } func assertError(t *testing.T, got error, want error) { t.Helper() if got == nil { t.Fatal("didn't get an error but wanted one") } if got != want { t.Errorf("got %q, want %q", got, want) } } Copy func TestWallet(t *testing.T) { t.Run("Deposit", func(t *testing.T) { wallet := Wallet{} wallet.Deposit(Bitcoin(10)) assertBalance(t, wallet, Bitcoin(10)) }) t.Run("Withdraw with funds", func(t *testing.T) { wallet := Wallet{Bitcoin(20)} err := wallet.Withdraw(Bitcoin(10)) assertBalance(t, wallet, Bitcoin(10)) assertNoError(t, err) }) t.Run("Withdraw insufficient funds", func(t *testing.T) { wallet := Wallet{Bitcoin(20)} err := wallet.Withdraw(Bitcoin(100)) assertBalance(t, wallet, Bitcoin(20)) assertError(t, err, ErrInsufficientFunds) }) } func assertBalance(t *testing.T, wallet Wallet, want Bitcoin) { t.Helper() got := wallet.Balance() if got != want { t.Errorf("got %s want %s", got, want) } } func assertNoError(t *testing.T, got error) { t.Helper() if got != nil { t.Fatal("got an error but didn't want one") } } func assertError(t *testing.T, got error, want error) { t.Helper() if got == nil { t.Fatal("didn't get an error but wanted one") } if got != want { t.Errorf("got %s, want %s", got, want) } } --- # Maps | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps.md) . [**You can find all the code for this chapter here**](https://github.com/quii/learn-go-with-tests/tree/master/maps) In [arrays & slices](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/arrays-and-slices) , 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. Maps allow you to store items in a manner similar to a dictionary. You can think of the `key` as the word and the `value` as the definition. And what better way is there to learn about Maps than to build our own dictionary? First, assuming we already have some words with their definitions in the dictionary, if we search for a word, it should return the definition of it. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-test-first) Write the test first -------------------------------------------------------------------------------------------------------------------------- In `dictionary_test.go` Copy package main import "testing" func TestSearch(t *testing.T) { dictionary := map[string]string{"test": "this is just a test"} got := Search(dictionary, "test") want := "this is just a test" if got != want { t.Errorf("got %q want %q given, %q", got, want, "test") } } Declaring a Map is somewhat similar to an array. Except, it starts with the `map` keyword and requires two types. The first is the key type, which is written inside the `[]`. The second is the value type, which goes right after the `[]`. The key type is special. It can only be a comparable type because without the ability to tell if 2 keys are equal, we have no way to ensure that we are getting the correct value. Comparable types are explained in depth in the [language spec](https://golang.org/ref/spec#Comparison_operators) . The value type, on the other hand, can be any type you want. It can even be another map. Everything else in this test should be familiar. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#try-to-run-the-test) Try to run the test ------------------------------------------------------------------------------------------------------------------------ By running `go test` the compiler will fail with `./dictionary_test.go:8:9: undefined: Search`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-output) Write the minimal amount of code for the test to run and check the output ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ In `dictionary.go` Your test should now fail with a _clear error message_ `dictionary_test.go:12: got '' want 'this is just a test' given, 'test'`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-enough-code-to-make-it-pass) Write enough code to make it pass ---------------------------------------------------------------------------------------------------------------------------------------------------- Getting a value out of a Map is the same as getting a value out of Array `map[key]`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#refactor) Refactor -------------------------------------------------------------------------------------------------- I decided to create an `assertStrings` helper to make the implementation more general. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#using-a-custom-type) Using a custom type We can improve our dictionary's usage by creating a new type around map and making `Search` a method. In `dictionary_test.go`: We started using the `Dictionary` type, which we have not defined yet. Then called `Search` on the `Dictionary` instance. We did not need to change `assertStrings`. In `dictionary.go`: Here we created a `Dictionary` type which acts as a thin wrapper around `map`. With the custom type defined, we can create the `Search` method. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-test-first-1) Write the test first ---------------------------------------------------------------------------------------------------------------------------- The basic search was very easy to implement, but what will happen if we supply a word that's not in our dictionary? We actually get nothing back. This is good because the program can continue to run, but there is a better approach. The function can report that the word is not in the dictionary. This way, the user isn't left wondering if the word doesn't exist or if there is just no definition (this might not seem very useful for a dictionary. However, it's a scenario that could be key in other usecases). The way to handle this scenario in Go is to return a second argument which is an `Error` type. `Error`s can be converted to a string with the `.Error()` method, which we do when passing it to the assertion. We are also protecting `assertStrings` with `if` to ensure we don't call `.Error()` on `nil`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#try-and-run-the-test) Try and run the test -------------------------------------------------------------------------------------------------------------------------- This does not compile [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-output-1) Write the minimal amount of code for the test to run and check the output -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Your test should now fail with a much clearer error message. `dictionary_test.go:22: expected to get an error.` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-enough-code-to-make-it-pass-1) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------ In order to make this pass, we are using an interesting property of the map lookup. It can return 2 values. The second value is a boolean which indicates if the key was found successfully. This property allows us to differentiate between a word that doesn't exist and a word that just doesn't have a definition. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#refactor-1) Refactor ---------------------------------------------------------------------------------------------------- We can get rid of the magic error in our `Search` function by extracting it into a variable. This will also allow us to have a better test. By creating a new helper we were able to simplify our test, and start using our `ErrNotFound` variable so our test doesn't fail if we change the error text in the future. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-test-first-2) Write the test first ---------------------------------------------------------------------------------------------------------------------------- We have a great way to search the dictionary. However, we have no way to add new words to our dictionary. In this test, we are utilizing our `Search` function to make the validation of the dictionary a little easier. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-output) Write the minimal amount of code for the test to run and check output ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- In `dictionary.go` Your test should now fail [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-enough-code-to-make-it-pass-2) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------ Adding to a map is also similar to an array. You just need to specify a key and set it equal to a value. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#reference-types) Reference Types An interesting property of maps is that you can modify them without passing them as a pointer. This is because `map` is a reference type. Meaning it holds a reference to the underlying data structure, much like a pointer. The underlying data structure is a `hash table`, or `hash map`, and you can read more about `hash tables` [here](https://en.wikipedia.org/wiki/Hash_table) . Maps being a reference is really good, because no matter how big a map gets there will only be one copy. A gotcha that reference types introduce is that maps can be a `nil` value. A `nil` map behaves like an empty map when reading, but attempts to write to a `nil` map will cause a runtime panic. You can read more about maps [here](https://blog.golang.org/go-maps-in-action) . Therefore, you should never initialize an empty map variable: Instead, you can initialize an empty map like we were doing above, or use the `make` keyword to create a map for you: Both approaches create an empty `hash map` and point `dictionary` at it. Which ensures that you will never get a runtime panic. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#refactor-2) Refactor ---------------------------------------------------------------------------------------------------- There isn't much to refactor in our implementation but the test could use a little simplification. We made variables for word and definition, and moved the definition assertion into its own helper function. Our `Add` is looking good. Except, we didn't consider what happens when the value we are trying to add already exists! Map will not throw an error if the value already exists. Instead, they will go ahead and overwrite the value with the newly provided value. This can be convenient in practice, but makes our function name less than accurate. `Add` should not modify existing values. It should only add new words to our dictionary. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-test-first-3) Write the test first ---------------------------------------------------------------------------------------------------------------------------- For this test, we modified `Add` to return an error, which we are validating against a new error variable, `ErrWordExists`. We also modified the previous test to check for a `nil` error, as well as the `assertError` function. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#try-to-run-test) Try to run test ---------------------------------------------------------------------------------------------------------------- The compiler will fail because we are not returning a value for `Add`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-output-2) Write the minimal amount of code for the test to run and check the output -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- In `dictionary.go` Now we get two more errors. We are still modifying the value, and returning a `nil` error. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-enough-code-to-make-it-pass-3) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------ Here we are using a `switch` statement to match on the error. Having a `switch` like this provides an extra safety net, in case `Search` returns an error other than `ErrNotFound`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#refactor-3) Refactor ---------------------------------------------------------------------------------------------------- We don't have too much to refactor, but as our error usage grows we can make a few modifications. We made the errors constant; this required us to create our own `DictionaryErr` type which implements the `error` interface. You can read more about the details in [this excellent article by Dave Cheney](https://dave.cheney.net/2016/04/07/constant-errors) . Simply put, it makes the errors more reusable and immutable. Next, let's create a function to `Update` the definition of a word. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-test-first-4) Write the test first ---------------------------------------------------------------------------------------------------------------------------- `Update` is very closely related to `Add` and will be our next implementation. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#try-and-run-the-test-1) Try and run the test ---------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write minimal amount of code for the test to run and check the failing test output ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ We already know how to deal with an error like this. We need to define our function. With that in place, we are able to see that we need to change the definition of the word. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-enough-code-to-make-it-pass-4) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------ We already saw how to do this when we fixed the issue with `Add`. So let's implement something really similar to `Add`. There is no refactoring we need to do on this since it was a simple change. However, we now have the same issue as with `Add`. If we pass in a new word, `Update` will add it to the dictionary. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-test-first-5) Write the test first ---------------------------------------------------------------------------------------------------------------------------- We added yet another error type for when the word does not exist. We also modified `Update` to return an `error` value. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#try-and-run-the-test-2) Try and run the test ---------------------------------------------------------------------------------------------------------------------------- We get 3 errors this time, but we know how to deal with these. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- We added our own error type and are returning a `nil` error. With these changes, we now get a very clear error: [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-enough-code-to-make-it-pass-5) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------ This function looks almost identical to `Add` except we switched when we update the `dictionary` and when we return an error. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#note-on-declaring-a-new-error-for-update) Note on declaring a new error for Update We could reuse `ErrNotFound` and not add a new error. However, it is often better to have a precise error for when an update fails. Having specific errors gives you more information about what went wrong. Here is an example in a web app: > You can redirect the user when `ErrNotFound` is encountered, but display an error message when `ErrWordDoesNotExist` is encountered. Next, let's create a function to `Delete` a word in the dictionary. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-test-first-6) Write the test first ---------------------------------------------------------------------------------------------------------------------------- Our test creates a `Dictionary` with a word and then checks if the word has been removed. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#try-to-run-the-test-1) Try to run the test -------------------------------------------------------------------------------------------------------------------------- By running `go test` we get: [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) Write the minimal amount of code for the test to run and check the failing test output ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- After we add this, the test tells us we are not deleting the word. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-enough-code-to-make-it-pass-6) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------ Go has a built-in function `delete` that works on maps. It takes two arguments. The first is the map and the second is the key to be removed. The `delete` function returns nothing, and we based our `Delete` method on the same notion. Since deleting a value that's not there has no effect, unlike our `Update` and `Add` methods, we don't need to complicate the API with errors. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#wrapping-up) Wrapping up -------------------------------------------------------------------------------------------------------- In this section, we covered a lot. We made a full CRUD (Create, Read, Update and Delete) API for our dictionary. Throughout the process we learned how to: * Create maps * Search for items in maps * Add new items to maps * Update items in maps * Delete items from a map * Learned more about errors * How to create errors that are constants * Writing error wrappers [PreviousPointers & errors](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/pointers-and-errors) [NextDependency Injection](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection) Last updated 6 years ago * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-test-first) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#try-to-run-the-test) * [Write the minimal amount of code for the test to run and check the output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-enough-code-to-make-it-pass) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#refactor) * [Using a custom type](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#using-a-custom-type) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-test-first-1) * [Try and run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#try-and-run-the-test) * [Write the minimal amount of code for the test to run and check the output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-output-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-enough-code-to-make-it-pass-1) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#refactor-1) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-test-first-2) * [Write the minimal amount of code for the test to run and check output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-enough-code-to-make-it-pass-2) * [Reference Types](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#reference-types) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#refactor-2) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-test-first-3) * [Try to run test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#try-to-run-test) * [Write the minimal amount of code for the test to run and check the output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-output-2) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-enough-code-to-make-it-pass-3) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#refactor-3) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-test-first-4) * [Try and run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#try-and-run-the-test-1) * [Write minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-enough-code-to-make-it-pass-4) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-test-first-5) * [Try and run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#try-and-run-the-test-2) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-enough-code-to-make-it-pass-5) * [Note on declaring a new error for Update](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#note-on-declaring-a-new-error-for-update) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-test-first-6) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#try-to-run-the-test-1) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#write-enough-code-to-make-it-pass-6) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps#wrapping-up) Copy package main func Search(dictionary map[string]string, word string) string { return "" } Copy func Search(dictionary map[string]string, word string) string { return dictionary[word] } Copy func TestSearch(t *testing.T) { dictionary := map[string]string{"test": "this is just a test"} got := Search(dictionary, "test") want := "this is just a test" assertStrings(t, got, want) } func assertStrings(t *testing.T, got, want string) { t.Helper() if got != want { t.Errorf("got %q want %q", got, want) } } Copy func TestSearch(t *testing.T) { dictionary := Dictionary{"test": "this is just a test"} got := dictionary.Search("test") want := "this is just a test" assertStrings(t, got, want) } Copy type Dictionary map[string]string func (d Dictionary) Search(word string) string { return d[word] } Copy func TestSearch(t *testing.T) { dictionary := Dictionary{"test": "this is just a test"} t.Run("known word", func(t *testing.T) { got, _ := dictionary.Search("test") want := "this is just a test" assertStrings(t, got, want) }) t.Run("unknown word", func(t *testing.T) { _, err := dictionary.Search("unknown") want := "could not find the word you were looking for" if err == nil { t.Fatal("expected to get an error.") } assertStrings(t, err.Error(), want) }) } Copy ./dictionary_test.go:18:10: assignment mismatch: 2 variables but 1 values Copy func (d Dictionary) Search(word string) (string, error) { return d[word], nil } Copy func (d Dictionary) Search(word string) (string, error) { definition, ok := d[word] if !ok { return "", errors.New("could not find the word you were looking for") } return definition, nil } Copy var ErrNotFound = errors.New("could not find the word you were looking for") func (d Dictionary) Search(word string) (string, error) { definition, ok := d[word] if !ok { return "", ErrNotFound } return definition, nil } Copy t.Run("unknown word", func(t *testing.T) { _, got := dictionary.Search("unknown") assertError(t, got, ErrNotFound) }) } func assertError(t *testing.T, got, want error) { t.Helper() if got != want { t.Errorf("got error %q want %q", got, want) } } Copy func TestAdd(t *testing.T) { dictionary := Dictionary{} dictionary.Add("test", "this is just a test") want := "this is just a test" got, err := dictionary.Search("test") if err != nil { t.Fatal("should find added word:", err) } if got != want { t.Errorf("got %q want %q", got, want) } } Copy func (d Dictionary) Add(word, definition string) { } Copy dictionary_test.go:31: should find added word: could not find the word you were looking for Copy func (d Dictionary) Add(word, definition string) { d[word] = definition } Copy var m map[string]string Copy var dictionary = map[string]string{} // OR var dictionary = make(map[string]string) Copy func TestAdd(t *testing.T) { dictionary := Dictionary{} word := "test" definition := "this is just a test" dictionary.Add(word, definition) assertDefinition(t, dictionary, word, definition) } func assertDefinition(t *testing.T, dictionary Dictionary, word, definition string) { t.Helper() got, err := dictionary.Search(word) if err != nil { t.Fatal("should find added word:", err) } if definition != got { t.Errorf("got %q want %q", got, definition) } } Copy func TestAdd(t *testing.T) { t.Run("new word", func(t *testing.T) { dictionary := Dictionary{} word := "test" definition := "this is just a test" err := dictionary.Add(word, definition) assertError(t, err, nil) assertDefinition(t, dictionary, word, definition) }) t.Run("existing word", func(t *testing.T) { word := "test" definition := "this is just a test" dictionary := Dictionary{word: definition} err := dictionary.Add(word, "new test") assertError(t, err, ErrWordExists) assertDefinition(t, dictionary, word, definition) }) } ... func assertError(t *testing.T, got, want error) { t.Helper() if got != want { t.Errorf("got %q want %q", got, want) } if got == nil { if want == nil { return } t.Fatal("expected to get an error.") } } Copy ./dictionary_test.go:30:13: dictionary.Add(word, definition) used as value ./dictionary_test.go:41:13: dictionary.Add(word, "new test") used as value Copy var ( ErrNotFound = errors.New("could not find the word you were looking for") ErrWordExists = errors.New("cannot add word because it already exists") ) func (d Dictionary) Add(word, definition string) error { d[word] = definition return nil } Copy dictionary_test.go:43: got error '%!q()' want 'cannot add word because it already exists' dictionary_test.go:44: got 'new test' want 'this is just a test' Copy func (d Dictionary) Add(word, definition string) error { _, err := d.Search(word) switch err { case ErrNotFound: d[word] = definition case nil: return ErrWordExists default: return err } return nil } Copy const ( ErrNotFound = DictionaryErr("could not find the word you were looking for") ErrWordExists = DictionaryErr("cannot add word because it already exists") ) type DictionaryErr string func (e DictionaryErr) Error() string { return string(e) } Copy func TestUpdate(t *testing.T) { word := "test" definition := "this is just a test" dictionary := Dictionary{word: definition} newDefinition := "new definition" dictionary.Update(word, newDefinition) assertDefinition(t, dictionary, word, newDefinition) } Copy ./dictionary_test.go:53:2: dictionary.Update undefined (type Dictionary has no field or method Update) Copy func (d Dictionary) Update(word, definition string) {} Copy dictionary_test.go:55: got 'this is just a test' want 'new definition' Copy func (d Dictionary) Update(word, definition string) { d[word] = definition } Copy t.Run("existing word", func(t *testing.T) { word := "test" definition := "this is just a test" newDefinition := "new definition" dictionary := Dictionary{word: definition} err := dictionary.Update(word, newDefinition) assertError(t, err, nil) assertDefinition(t, dictionary, word, newDefinition) }) t.Run("new word", func(t *testing.T) { word := "test" definition := "this is just a test" dictionary := Dictionary{} err := dictionary.Update(word, definition) assertError(t, err, ErrWordDoesNotExist) }) Copy ./dictionary_test.go:53:16: dictionary.Update(word, "new test") used as value ./dictionary_test.go:64:16: dictionary.Update(word, definition) used as value ./dictionary_test.go:66:23: undefined: ErrWordDoesNotExist Copy const ( ErrNotFound = DictionaryErr("could not find the word you were looking for") ErrWordExists = DictionaryErr("cannot add word because it already exists") ErrWordDoesNotExist = DictionaryErr("cannot update word because it does not exist") ) func (d Dictionary) Update(word, definition string) error { d[word] = definition return nil } Copy dictionary_test.go:66: got error '%!q()' want 'cannot update word because it does not exist' Copy func (d Dictionary) Update(word, definition string) error { _, err := d.Search(word) switch err { case ErrNotFound: return ErrWordDoesNotExist case nil: d[word] = definition default: return err } return nil } Copy func TestDelete(t *testing.T) { word := "test" dictionary := Dictionary{word: "test definition"} dictionary.Delete(word) _, err := dictionary.Search(word) if err != ErrNotFound { t.Errorf("Expected %q to be deleted", word) } } Copy ./dictionary_test.go:74:6: dictionary.Delete undefined (type Dictionary has no field or method Delete) Copy func (d Dictionary) Delete(word string) { } Copy dictionary_test.go:78: Expected 'test' to be deleted Copy func (d Dictionary) Delete(word string) { delete(d, word) } --- # Dependency Injection | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection.md) . [**You can find all the code for this chapter here**](https://github.com/quii/learn-go-with-tests/tree/master/di) It is assumed that you have read the structs section before as some understanding of interfaces will be needed for this. There is _a lot_ of misunderstandings around dependency injection around the programming community. Hopefully, this guide will show you how * You don't need a framework * It does not overcomplicate your design * It facilitates testing * It allows you to write great, general-purpose functions. We want to write a function that greets someone, just like we did in the hello-world chapter but this time we are going to be testing the _actual printing_. Just to recap, here is what that function could look like Copy func Greet(name string) { fmt.Printf("Hello, %s", name) } But how can we test this? Calling `fmt.Printf` prints to stdout, which is pretty hard for us to capture using the testing framework. What we need to do is to be able to **inject** (which is just a fancy word for pass in) the dependency of printing. **Our function doesn't need to care** _**where**_ **or** _**how**_ **the printing happens, so we should accept an** _**interface**_ **rather than a concrete type.** If we do that, we can then change the implementation to print to something we control so that we can test it. In "real life" you would inject in something that writes to stdout. If you look at the source code of `fmt.Printf` you can see a way for us to hook in Copy // It returns the number of bytes written and any write error encountered. func Printf(format string, a ...interface{}) (n int, err error) { return Fprintf(os.Stdout, format, a...) } Interesting! Under the hood `Printf` just calls `Fprintf` passing in `os.Stdout`. What exactly _is_ an `os.Stdout`? What does `Fprintf` expect to get passed to it for the 1st argument? An `io.Writer` As you write more Go code you will find this interface popping up a lot because it's a great general purpose interface for "put this data somewhere". So we know under the covers we're ultimately using `Writer` to send our greeting somewhere. Let's use this existing abstraction to make our code testable and more reusable. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection#write-the-test-first) Write the test first ------------------------------------------------------------------------------------------------------------------------------------------ The `buffer` type from the `bytes` package implements the `Writer` interface. So we'll use it in our test to send in as our `Writer` and then we can check what was written to it after we invoke `Greet` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection#try-and-run-the-test) Try and run the test ------------------------------------------------------------------------------------------------------------------------------------------ The test will not compile [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ _Listen to the compiler_ and fix the problem. `Hello, Chris di_test.go:16: got '' want 'Hello, Chris'` The test fails. Notice that the name is getting printed out, but it's going to stdout. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection#write-enough-code-to-make-it-pass) Write enough code to make it pass -------------------------------------------------------------------------------------------------------------------------------------------------------------------- Use the writer to send the greeting to the buffer in our test. Remember `fmt.Fprintf` is like `fmt.Printf` but instead takes a `Writer` to send the string to, whereas `fmt.Printf` defaults to stdout. The test now passes. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection#refactor) Refactor ------------------------------------------------------------------------------------------------------------------ Earlier the compiler told us to pass in a pointer to a `bytes.Buffer`. This is technically correct but not very useful. To demonstrate this, try wiring up the `Greet` function into a Go application where we want it to print to stdout. `./di.go:14:7: cannot use os.Stdout (type *os.File) as type *bytes.Buffer in argument to Greet` As discussed earlier `fmt.Fprintf` allows you to pass in an `io.Writer` which we know both `os.Stdout` and `bytes.Buffer` implement. If we change our code to use the more general purpose interface we can now use it in both tests and in our application. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection#more-on-io.writer) More on io.Writer ------------------------------------------------------------------------------------------------------------------------------------ What other places can we write data to using `io.Writer`? Just how general purpose is our `Greet` function? ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection#the-internet) The internet Run the following Run the program and go to [http://localhost:5000](http://localhost:5000/) . You'll see your greeting function being used. HTTP servers will be covered in a later chapter so don't worry too much about the details. When you write an HTTP handler, you are given an `http.ResponseWriter` and the `http.Request` that was used to make the request. When you implement your server you _write_ your response using the writer. You can probably guess that `http.ResponseWriter` also implements `io.Writer` so this is why we could re-use our `Greet` function inside our handler. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection#wrapping-up) Wrapping up ------------------------------------------------------------------------------------------------------------------------ Our first round of code was not easy to test because it wrote data to somewhere we couldn't control. _Motivated by our tests_ we refactored the code so we could control _where_ the data was written by **injecting a dependency** which allowed us to: * **Test our code** If you can't test a function _easily_, it's usually because of dependencies hard-wired into a function _or_ global state. If you have a global database connection pool for instance that is used by some kind of service layer, it is likely going to be difficult to test and they will be slow to run. DI will motivate you to inject in a database dependency (via an interface) which you can then mock out with something you can control in your tests. * **Separate our concerns**, decoupling _where the data goes_ from _how to generate it_. If you ever feel like a method/function has too many responsibilities (generating data _and_ writing to a db? handling HTTP requests _and_ doing domain level logic?) DI is probably going to be the tool you need. * **Allow our code to be re-used in different contexts** The first "new" context our code can be used in is inside tests. But further on if someone wants to try something new with your function they can inject their own dependencies. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection#what-about-mocking-i-hear-you-need-that-for-di-and-also-its-evil) What about mocking? I hear you need that for DI and also it's evil Mocking will be covered in detail later (and it's not evil). You use mocking to replace real things you inject with a pretend version that you can control and inspect in your tests. In our case though, the standard library had something ready for us to use. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection#the-go-standard-library-is-really-good-take-time-to-study-it) The Go standard library is really good, take time to study it By having some familiarity with the `io.Writer` interface we are able to use `bytes.Buffer` in our test as our `Writer` and then we can use other `Writer`s from the standard library to use our function in a command line app or in web server. The more familiar you are with the standard library the more you'll see these general purpose interfaces which you can then re-use in your own code to make your software reusable in a number of contexts. This example is heavily influenced by a chapter in [The Go Programming language](https://www.amazon.co.uk/Programming-Language-Addison-Wesley-Professional-Computing/dp/0134190440) , so if you enjoyed this, go buy it! [PreviousMaps](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/maps) [NextMocking](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking) Last updated 6 years ago * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection#write-the-test-first) * [Try and run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection#try-and-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection#write-enough-code-to-make-it-pass) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection#refactor) * [More on io.Writer](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection#more-on-io.writer) * [The internet](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection#the-internet) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection#wrapping-up) * [What about mocking? I hear you need that for DI and also it's evil](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection#what-about-mocking-i-hear-you-need-that-for-di-and-also-its-evil) * [The Go standard library is really good, take time to study it](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection#the-go-standard-library-is-really-good-take-time-to-study-it) Copy func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { p := newPrinter() p.doPrintf(format, a) n, err = w.Write(p.buf) p.free() return } Copy type Writer interface { Write(p []byte) (n int, err error) } Copy func TestGreet(t *testing.T) { buffer := bytes.Buffer{} Greet(&buffer, "Chris") got := buffer.String() want := "Hello, Chris" if got != want { t.Errorf("got %q want %q", got, want) } } Copy ./di_test.go:10:7: too many arguments in call to Greet have (*bytes.Buffer, string) want (string) Copy func Greet(writer *bytes.Buffer, name string) { fmt.Printf("Hello, %s", name) } Copy func Greet(writer *bytes.Buffer, name string) { fmt.Fprintf(writer, "Hello, %s", name) } Copy func main() { Greet(os.Stdout, "Elodie") } Copy package main import ( "fmt" "os" "io" ) func Greet(writer io.Writer, name string) { fmt.Fprintf(writer, "Hello, %s", name) } func main() { Greet(os.Stdout, "Elodie") } Copy package main import ( "fmt" "io" "net/http" ) func Greet(writer io.Writer, name string) { fmt.Fprintf(writer, "Hello, %s", name) } func MyGreeterHandler(w http.ResponseWriter, r *http.Request) { Greet(w, "world") } func main() { http.ListenAndServe(":5000", http.HandlerFunc(MyGreeterHandler)) } --- # Sync | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync.md) . [**You can find all the code for this chapter here**](https://github.com/quii/learn-go-with-tests/tree/master/sync) 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. Then we'll exercise it's unsafeness with multiple goroutines trying to use it via a test and fix it. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#write-the-test-first) Write the test first -------------------------------------------------------------------------------------------------------------------------- We want our API to give us a method to increment the counter and then retrieve its value. Copy func TestCounter(t *testing.T) { t.Run("incrementing the counter 3 times leaves it at 3", func(t *testing.T) { counter := Counter{} counter.Inc() counter.Inc() counter.Inc() if counter.Value() != 3 { t.Errorf("got %d, want %d", counter.Value(), 3) } }) } [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#try-to-run-the-test) Try to run the test ------------------------------------------------------------------------------------------------------------------------ [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Let's define `Counter`. Try again and it fails with the following So to finally make the test run we can define those methods It should now run and fail [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#write-enough-code-to-make-it-pass) Write enough code to make it pass ---------------------------------------------------------------------------------------------------------------------------------------------------- This should be trivial for Go experts like us. We need to keep some state for the counter in our datatype and then increment it on every `Inc` call [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#refactor) Refactor -------------------------------------------------------------------------------------------------- There's not a lot to refactor but given we're going to write more tests around `Counter` we'll write a small assertion function `assertCount` so the test reads a bit clearer. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#next-steps) 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. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#write-the-test-first-1) Write the test first ---------------------------------------------------------------------------------------------------------------------------- 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. > A WaitGroup waits for a collection of goroutines to finish. The main goroutine calls Add to set the number of goroutines to wait for. Then each of the goroutines runs and calls Done when finished. At the same time, Wait can be used to block until all goroutines have finished. By waiting for `wg.Wait()` to finish before making our assertions we can be sure all of our goroutines have attempted to `Inc` the `Counter`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#try-to-run-the-test-1) Try to run the test -------------------------------------------------------------------------------------------------------------------------- The test will _probably_ fail with a different number, but nonetheless it demonstrates it does not work when multiple goroutines are trying to mutate the value of the counter at the same time. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#write-enough-code-to-make-it-pass-1) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------ A simple solution is to add a lock to our `Counter`, a [`Mutex`](https://golang.org/pkg/sync/#Mutex) > A Mutex is a mutual exclusion lock. The zero value for a Mutex is an unlocked mutex. 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. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#ive-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 It can be argued that it can make the code a bit more elegant. 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. 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. ![](https://quii.gitbook.io/learn-go-with-tests/~gitbook/image?url=https%3A%2F%2Fi.imgur.com%2FSWYNpwm.png&width=768&dpr=3&quality=100&sign=b1b75cda&sv=2) Showing how a user of this API can wrongly change the state of the lock _This seems like a really bad idea_ [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#copying-mutexes) Copying mutexes ---------------------------------------------------------------------------------------------------------------- Our test passes but our code is still a bit dangerous If you run `go vet` on your code you should get an error like the following A look at the documentation of [`sync.Mutex`](https://golang.org/pkg/sync/#Mutex) tells us why > 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. To solve this we should pass in a pointer to our `Counter` instead, so change the signature of `assertCounter` Our tests will no longer compile because we are trying to pass in a `Counter` rather than a `*Counter`. To solve this I prefer to create a constructor which shows readers of your API that it would be better to not initialise the type yourself. Use this function in your tests when initialising `Counter`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#wrapping-up) Wrapping up -------------------------------------------------------------------------------------------------------- We've covered a few things from the [sync package](https://golang.org/pkg/sync/) * `Mutex` allows us to add locks to our data * `Waitgroup` is a means of waiting for goroutines to finish jobs ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#when-to-use-locks-over-channels-and-goroutines) When to use locks over channels and goroutines? [We've previously covered goroutines in the first concurrency chapter](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/concurrency) 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 mutexes for managing state** ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#go-vet) go vet Remember to use go vet in your build scripts as it can alert you to some subtle bugs in your code before they hit your poor users. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#dont-use-embedding-because-its-convenient) Don't use embedding because it's convenient * Think about the effect embedding has on your public API. * Do you _really_ want to expose these methods and have people coupling their own code to them? * With respect to mutexes, this could be potentially disastrous in very unpredictable and weird ways, imagine some nefarious code unlocking a mutex when it shouldn't be; this would cause some very strange bugs that will be hard to track down. [PreviousReflection](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection) [NextContext](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context) Last updated 6 years ago * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#write-the-test-first) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#try-to-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#write-enough-code-to-make-it-pass) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#refactor) * [Next steps](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#next-steps) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#write-the-test-first-1) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#try-to-run-the-test-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#write-enough-code-to-make-it-pass-1) * [I've seen other examples where the sync.Mutex is embedded into the struct.](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#ive-seen-other-examples-where-the-sync.mutex-is-embedded-into-the-struct) * [Copying mutexes](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#copying-mutexes) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#wrapping-up) * [When to use locks over channels and goroutines?](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#when-to-use-locks-over-channels-and-goroutines) * [go vet](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#go-vet) * [Don't use embedding because it's convenient](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync#dont-use-embedding-because-its-convenient) Copy ./sync_test.go:9:14: undefined: Counter Copy type Counter struct { } Copy ./sync_test.go:14:10: counter.Inc undefined (type Counter has no field or method Inc) ./sync_test.go:18:13: counter.Value undefined (type Counter has no field or method Value) Copy func (c *Counter) Inc() { } func (c *Counter) Value() int { return 0 } Copy === RUN TestCounter === RUN TestCounter/incrementing_the_counter_3_times_leaves_it_at_3 --- FAIL: TestCounter (0.00s) --- FAIL: TestCounter/incrementing_the_counter_3_times_leaves_it_at_3 (0.00s) sync_test.go:27: got 0, want 3 Copy type Counter struct { value int } func (c *Counter) Inc() { c.value++ } func (c *Counter) Value() int { return c.value } Copy t.Run("incrementing the counter 3 times leaves it at 3", func(t *testing.T) { counter := Counter{} counter.Inc() counter.Inc() counter.Inc() assertCounter(t, counter, 3) }) func assertCounter(t *testing.T, got Counter, want int) { t.Helper() if got.Value() != want { t.Errorf("got %d, want %d", got.Value(), want) } } Copy t.Run("it runs safely concurrently", func(t *testing.T) { wantedCount := 1000 counter := Counter{} var wg sync.WaitGroup wg.Add(wantedCount) for i := 0; i < wantedCount; i++ { go func(w *sync.WaitGroup) { counter.Inc() w.Done() }(&wg) } wg.Wait() assertCounter(t, counter, wantedCount) }) Copy === RUN TestCounter/it_runs_safely_in_a_concurrent_envionment --- FAIL: TestCounter (0.00s) --- FAIL: TestCounter/it_runs_safely_in_a_concurrent_envionment (0.00s) sync_test.go:26: got 939, want 1000 FAIL Copy type Counter struct { mu sync.Mutex value int } func (c *Counter) Inc() { c.mu.Lock() defer c.mu.Unlock() c.value++ } Copy type Counter struct { sync.Mutex value int } Copy func (c *Counter) Inc() { c.Lock() defer c.Unlock() c.value++ } Copy sync/v2/sync_test.go:16: call of assertCounter copies lock value: v1.Counter contains sync.Mutex sync/v2/sync_test.go:39: assertCounter passes lock by value: v1.Counter contains sync.Mutex Copy func assertCounter(t *testing.T, got *Counter, want int) Copy func NewCounter() *Counter { return &Counter{} } --- # Contributing | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/meta/contributing.md) . Contributions are very welcome. I hope for this to become a great home for guides of how to learn Go by writing tests. Consider submitting a PR or creating an issue which you can do [here](https://github.com/quii/learn-go-with-tests/issues) . [](https://quii.gitbook.io/learn-go-with-tests/master/meta/contributing#what-were-looking-for) What we're looking for -------------------------------------------------------------------------------------------------------------------------- * Teaching Go features (e.g things like `if`, `select`, structs, methods, etc). * Showcase interesting functionality within the standard library. Show off how easy it is to TDD a HTTP server for instance. * Show how Go's tooling, like benchmarking, race detectors, etc can help you arrive at great software. If you don't feel confident to submit your own guide, submitting an issue for something you want to learn is still a valuable contribution. ### [](https://quii.gitbook.io/learn-go-with-tests/master/meta/contributing#get-feedback-quickly-for-new-content) ⚠️ Get feedback quickly for new content ⚠️ * TDD teaches us to work iteratively and get feedback and I strongly suggest you do the same if you wish to contribute * Open a PR with your first test and implementation, discuss your approach so I can offer feedback and course correct * This is of course open-source but I do have strong opinions on the content. The sooner you talk to me the better. [](https://quii.gitbook.io/learn-go-with-tests/master/meta/contributing#style-guide) Style guide ----------------------------------------------------------------------------------------------------- * Always be reinforcing the TDD cycle. Take a look at the [Chapter Template](https://quii.gitbook.io/learn-go-with-tests/master/meta/template) . * Emphasis on iterating over functionality driven by tests. The Hello, world example works well because we gradually make it more sophisticated and learning new techniques _driven_ by the tests. For example: * `Hello()` <- how to write functions, return types. * `Hello(name string)` <- arguments, constants. * `Hello(name string)` <- default to "world" using `if`. * `Hello(name, language string)` <- `switch`. * Try and minimise the surface area of required knowledge. * Thinking of examples that showcase what you're trying to teach without confusing the reader with other features is important. * For example you can learn about `struct`s without understanding pointers. * Brevity is king. * Follow the [Code Review Comments style guide](https://github.com/golang/go/wiki/CodeReviewComments) . It's important for a consistent style across all the sections. * Your section should have a runnable application at the end (e.g `package main` with a `main` func) so users can see it in action and play with it. * All tests should pass. * Run `./build.sh` before raising PR. [PreviousWhy unit tests and how to make them work for you](https://quii.gitbook.io/learn-go-with-tests/master/meta/why) [NextChapter Template](https://quii.gitbook.io/learn-go-with-tests/master/meta/template) Last updated 6 years ago * [What we're looking for](https://quii.gitbook.io/learn-go-with-tests/master/meta/contributing#what-were-looking-for) * [⚠️ Get feedback quickly for new content ⚠️](https://quii.gitbook.io/learn-go-with-tests/master/meta/contributing#get-feedback-quickly-for-new-content) * [Style guide](https://quii.gitbook.io/learn-go-with-tests/master/meta/contributing#style-guide) --- # Chapter Template | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/meta/template.md) . Some intro [](https://quii.gitbook.io/learn-go-with-tests/master/meta/template#write-the-test-first) Write the test first ------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/meta/template#try-to-run-the-test) Try to run the test ----------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/meta/template#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/meta/template#write-enough-code-to-make-it-pass) Write enough code to make it pass --------------------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/meta/template#refactor) Refactor ------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/meta/template#repeat-for-new-requirements) Repeat for new requirements --------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/meta/template#wrapping-up) Wrapping up ------------------------------------------------------------------------------------------------- [PreviousContributing](https://quii.gitbook.io/learn-go-with-tests/master/meta/contributing) Last updated 8 years ago * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/meta/template#write-the-test-first) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/meta/template#try-to-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/meta/template#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/meta/template#write-enough-code-to-make-it-pass) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/meta/template#refactor) * [Repeat for new requirements](https://quii.gitbook.io/learn-go-with-tests/master/meta/template#repeat-for-new-requirements) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/meta/template#wrapping-up) --- # Concurrency | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/concurrency.md) . [**You can find all the code for this chapter here**](https://github.com/quii/learn-go-with-tests/tree/master/concurrency) Here's the setup: a colleague has written a function, `CheckWebsites`, that checks the status of a list of URLs. Copy package concurrency type WebsiteChecker func(string) bool func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { results := make(map[string]bool) for _, url := range urls { results[url] = wc(url) } return results } It returns a map of each URL checked to a boolean value - `true` for a good response, `false` for a bad response. You also have to pass in a `WebsiteChecker` which takes a single URL and returns a boolean. This is used by the function to check all the websites. Using [dependency injection](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection) has allowed them to test the function without making real HTTP calls, making it reliable and fast. Here's the test they've written: The function is in production and being used to check hundreds of websites. But your colleague has started to get complaints that it's slow, so they've asked you to help speed it up. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/concurrency#write-a-test) Write a test ----------------------------------------------------------------------------------------------------------------- Let's use a benchmark to test the speed of `CheckWebsites` so that we can see the effect of our changes. The benchmark tests `CheckWebsites` using a slice of one hundred urls and uses a new fake implementation of `WebsiteChecker`. `slowStubWebsiteChecker` is deliberately slow. It uses `time.Sleep` to wait exactly twenty milliseconds and then it returns true. When we run the benchmark using `go test -bench=.` (or if you're in Windows Powershell `go test -bench="."`): `CheckWebsites` has been benchmarked at 2249228637 nanoseconds - about two and a quarter seconds. Let's try and make this faster. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/concurrency#write-enough-code-to-make-it-pass) Write enough code to make it pass Now we can finally talk about concurrency which, for the purposes of the following, means 'having more than one thing in progress'. This is something that we do naturally everyday. For instance, this morning I made a cup of tea. I put the kettle on and then, while I was waiting for it to boil, I got the milk out of the fridge, got the tea out of the cupboard, found my favourite mug, put the teabag into the cup and then, when the kettle had boiled, I put the water in the cup. What I _didn't_ do was put the kettle on and then stand there blankly staring at the kettle until it boiled, then do everything else once the kettle had boiled. If you can understand why it's faster to make tea the first way, then you can understand how we will make `CheckWebsites` faster. Instead of waiting for a website to respond before sending a request to the next website, we will tell our computer to make the next request while it is waiting. Normally in Go when we call a function `doSomething()` we wait for it to return (even if it has no value to return, we still wait for it to finish). We say that this operation is _blocking_ - it makes us wait for it to finish. An operation that does not block in Go will run in a separate _process_ called a _goroutine_. Think of a process as reading down the page of Go code from top to bottom, going 'inside' each function when it gets called to read what it does. When a separate process starts it's like another reader begins reading inside the function, leaving the original reader to carry on going down the page. To tell Go to start a new goroutine we turn a function call into a `go` statement by putting the keyword `go` in front of it: `go doSomething()`. Because the only way to start a goroutine is to put `go` in front of a function call, we often use _anonymous functions_ when we want to start a goroutine. An anonymous function literal looks just the same as a normal function declaration, but without a name (unsurprisingly). You can see one above in the body of the `for` loop. Anonymous functions have a number of features which make them useful, two of which we're using above. Firstly, they can be executed at the same time that the're declared - this is what the `()` at the end of the anonymous function is doing. Secondly they maintain access to the lexical scope they are defined in - all the variables that are available at the point when you declare the anonymous function are also available in the body of the function. The body of the anonymous function above is just the same as the loop body was before. The only difference is that each iteration of the loop will start a new goroutine, concurrent with the current process (the `WebsiteChecker` function) each of which will add its result to the results map. But when we run `go test`: ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/concurrency#a-quick-aside-into-a-parallel-ism-universe) A quick aside into a parallel(ism) universe... You might not get this result. You might get a panic message that we're going to talk about in a bit. Don't worry if you got that, just keep running the test until you _do_ get the result above. Or pretend that you did. Up to you. Welcome to concurrency: when it's not handled correctly it's hard to predict what's going to happen. Don't worry - that's why we're writing tests, to help us know when we're handling concurrency predictably. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/concurrency#and-were-back) ... and we're back. We are caught by the original tests `CheckWebsites` is now returning an empty map. What went wrong? None of the goroutines that our `for` loop started had enough time to add their result to the `results` map; the `WebsiteChecker` function is too fast for them, and it returns the still empty map. To fix this we can just wait while all the goroutines do their work, and then return. Two seconds ought to do it, right? Now when we run the tests you get (or don't get - see above): This isn't great - why only one result? We might try and fix this by increasing the time we wait - try it if you like. It won't work. The problem here is that the variable `url` is reused for each iteration of the `for` loop - it takes a new value from `urls` each time. But each of our goroutines have a reference to the `url` variable - they don't have their own independent copy. So they're _all_ writing the value that `url` has at the end of the iteration - the last url. Which is why the one result we have is the last url. To fix this: By giving each anonymous function a parameter for the url - `u` - and then calling the anonymous function with the `url` as the argument, we make sure that the value of `u` is fixed as the value of `url` for the iteration of the loop that we're launching the goroutine in. `u` is a copy of the value of `url`, and so can't be changed. Now if you're lucky you'll get: But if you're unlucky (this is more likely if you run them with the benchmark as you'll get more tries) This is long and scary, but all we need to do is take a breath and read the stacktrace: `fatal error: concurrent map writes`. Sometimes, when we run our tests, two of the goroutines write to the results map at exactly the same time. Maps in Go don't like it when more than one thing tries to write to them at once, and so `fatal error`. This is a _race condition_, a bug that occurs when the output of our software is dependent on the timing and sequence of events that we have no control over. Because we cannot control exactly when each goroutine writes to the results map, we are vulnerable to two goroutines writing to it at the same time. Go can help us to spot race conditions with its built in [_race detector_](https://blog.golang.org/race-detector) . To enable this feature, run the tests with the `race` flag: `go test -race`. You should get some output that looks like this: The details are, again, hard to read - but `WARNING: DATA RACE` is pretty unambiguous. Reading into the body of the error we can see two different goroutines performing writes on a map: `Write at 0x00c420084d20 by goroutine 8:` is writing to the same block of memory as `Previous write at 0x00c420084d20 by goroutine 7:` On top of that we can see the line of code where the write is happening: `/Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:12` and the line of code where goroutines 7 an 8 are started: `/Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:11` Everything you need to know is printed to your terminal - all you have to do is be patient enough to read it. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/concurrency#channels) Channels We can solve this data race by coordinating our goroutines using _channels_. Channels are a Go data structure that can both receive and send values. These operations, along with their details, allow communication between different processes. In this case we want to think about the communication between the parent process and each of the goroutines that it makes to do the work of running the `WebsiteChecker` function with the url. Alongside the `results` map we now have a `resultChannel`, which we `make` in the same way. `chan result` is the type of the channel - a channel of `result`. The new type, `result` has been made to associate the return value of the `WebsiteChecker` with the url being checked - it's a struct of `string` and `bool`. As we don't need either value to be named, each of them is anonymous within the struct; this can be useful in when it's hard to know what to name a value. Now when we iterate over the urls, instead of writing to the `map` directly we're sending a `result` struct for each call to `wc` to the `resultChannel` with a _send statement_. This uses the `<-` operator, taking a channel on the left and a value on the right: The next `for` loop iterates once for each of the urls. Inside we're using a _receive expression_, which assigns a value received from a channel to a variable. This also uses the `<-` operator, but with the two operands now reversed: the channel is now on the right and the variable that we're assigning to is on the left: We then use the `result` received to update the map. By sending the results into a channel, we can control the timing of each write into the results map, ensuring that it happens one at a time. Although each of the calls of `wc`, and each send to the result channel, is happening in parallel inside its own process, each of the results is being dealt with one at a time as we take values out of the result channel with the receive expression. We have parallelized the part of the code that we wanted to make faster, while making sure that the part that cannot happen in parallel still happens linearly. And we have communicated across the multiple processes involved by using channels. When we run the benchmark: 23406615 nanoseconds - 0.023 seconds, about one hundred times as fast as original function. A great success. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/concurrency#wrapping-up) Wrapping up --------------------------------------------------------------------------------------------------------------- This exercise has been a little lighter on the TDD than usual. In a way we've been taking part in one long refactoring of the `CheckWebsites` function; the inputs and outputs never changed, it just got faster. But the tests we had in place, as well as the benchmark we wrote, allowed us to refactor `CheckWebsites` in a way that maintained confidence that the software was still working, while demonstrating that it had actually become faster. In making it faster we learned about * _goroutines_, the basic unit of concurrency in Go, which let us check more than one website at the same time. * _anonymous functions_, which we used to start each of the concurrent processes that check websites. * _channels_, to help organize and control the communication between the different processes, allowing us to avoid a _race condition_ bug. * _the race detector_ which helped us debug problems with concurrent code ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/concurrency#make-it-fast) Make it fast One formulation of an agile way of building software, often misattributed to Kent Beck, is: > [Make it work, make it right, make it fast](http://wiki.c2.com/?MakeItWorkMakeItRightMakeItFast) Where 'work' is making the tests pass, 'right' is refactoring the code, and 'fast' is optimizing the code to make it, for example, run quickly. We can only 'make it fast' once we've made it work and made it right. We were lucky that the code we were given was already demonstrated to be working, and didn't need to be refactored. We should never try to 'make it fast' before the other two steps have been performed because > [Premature optimization is the root of all evil](http://wiki.c2.com/?PrematureOptimization) > -- Donald Knuth [PreviousMocking](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking) [NextSelect](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select) Last updated 6 years ago * [Write a test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/concurrency#write-a-test) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/concurrency#write-enough-code-to-make-it-pass) * [A quick aside into a parallel(ism) universe...](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/concurrency#a-quick-aside-into-a-parallel-ism-universe) * [... and we're back.](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/concurrency#and-were-back) * [Channels](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/concurrency#channels) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/concurrency#wrapping-up) * [Make it fast](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/concurrency#make-it-fast) Copy package concurrency import ( "reflect" "testing" ) func mockWebsiteChecker(url string) bool { if url == "waat://furhurterwe.geds" { return false } return true } func TestCheckWebsites(t *testing.T) { websites := []string{ "http://google.com", "http://blog.gypsydave5.com", "waat://furhurterwe.geds", } want := map[string]bool{ "http://google.com": true, "http://blog.gypsydave5.com": true, "waat://furhurterwe.geds": false, } got := CheckWebsites(mockWebsiteChecker, websites) if !reflect.DeepEqual(want, got) { t.Fatalf("Wanted %v, got %v", want, got) } } Copy package concurrency import ( "testing" "time" ) func slowStubWebsiteChecker(_ string) bool { time.Sleep(20 * time.Millisecond) return true } func BenchmarkCheckWebsites(b *testing.B) { urls := make([]string, 100) for i := 0; i < len(urls); i++ { urls[i] = "a url" } for i := 0; i < b.N; i++ { CheckWebsites(slowStubWebsiteChecker, urls) } } Copy pkg: github.com/gypsydave5/learn-go-with-tests/concurrency/v0 BenchmarkCheckWebsites-4 1 2249228637 ns/op PASS ok github.com/gypsydave5/learn-go-with-tests/concurrency/v0 2.268s Copy package concurrency type WebsiteChecker func(string) bool func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { results := make(map[string]bool) for _, url := range urls { go func() { results[url] = wc(url) }() } return results } Copy --- FAIL: TestCheckWebsites (0.00s) CheckWebsites_test.go:31: Wanted map[http://google.com:true http://blog.gypsydave5.com:true waat://furhurterwe.geds:false], got map[] FAIL exit status 1 FAIL github.com/gypsydave5/learn-go-with-tests/concurrency/v1 0.010s Copy package concurrency import "time" type WebsiteChecker func(string) bool func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { results := make(map[string]bool) for _, url := range urls { go func() { results[url] = wc(url) }() } time.Sleep(2 * time.Second) return results } Copy --- FAIL: TestCheckWebsites (0.00s) CheckWebsites_test.go:31: Wanted map[http://google.com:true http://blog.gypsydave5.com:true waat://furhurterwe.geds:false], got map[waat://furhurterwe.geds:false] FAIL exit status 1 FAIL github.com/gypsydave5/learn-go-with-tests/concurrency/v1 0.010s Copy package concurrency import ( "time" ) type WebsiteChecker func(string) bool func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { results := make(map[string]bool) for _, url := range urls { go func(u string) { results[u] = wc(u) }(url) } time.Sleep(2 * time.Second) return results } Copy PASS ok github.com/gypsydave5/learn-go-with-tests/concurrency/v1 2.012s Copy fatal error: concurrent map writes goroutine 8 [running]: runtime.throw(0x12c5895, 0x15) /usr/local/Cellar/go/1.9.3/libexec/src/runtime/panic.go:605 +0x95 fp=0xc420037700 sp=0xc4200376e0 pc=0x102d395 runtime.mapassign_faststr(0x1271d80, 0xc42007acf0, 0x12c6634, 0x17, 0x0) /usr/local/Cellar/go/1.9.3/libexec/src/runtime/hashmap_fast.go:783 +0x4f5 fp=0xc420037780 sp=0xc420037700 pc=0x100eb65 github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker.func1(0xc42007acf0, 0x12d3938, 0x12c6634, 0x17) /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:12 +0x71 fp=0xc4200377c0 sp=0xc420037780 pc=0x12308f1 runtime.goexit() /usr/local/Cellar/go/1.9.3/libexec/src/runtime/asm_amd64.s:2337 +0x1 fp=0xc4200377c8 sp=0xc4200377c0 pc=0x105cf01 created by github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:11 +0xa1 ... many more scary lines of text ... Copy ================== WARNING: DATA RACE Write at 0x00c420084d20 by goroutine 8: runtime.mapassign_faststr() /usr/local/Cellar/go/1.9.3/libexec/src/runtime/hashmap_fast.go:774 +0x0 github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker.func1() /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:12 +0x82 Previous write at 0x00c420084d20 by goroutine 7: runtime.mapassign_faststr() /usr/local/Cellar/go/1.9.3/libexec/src/runtime/hashmap_fast.go:774 +0x0 github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker.func1() /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:12 +0x82 Goroutine 8 (running) created at: github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker() /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:11 +0xc4 github.com/gypsydave5/learn-go-with-tests/concurrency/v3.TestWebsiteChecker() /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker_test.go:27 +0xad testing.tRunner() /usr/local/Cellar/go/1.9.3/libexec/src/testing/testing.go:746 +0x16c Goroutine 7 (finished) created at: github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker() /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:11 +0xc4 github.com/gypsydave5/learn-go-with-tests/concurrency/v3.TestWebsiteChecker() /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker_test.go:27 +0xad testing.tRunner() /usr/local/Cellar/go/1.9.3/libexec/src/testing/testing.go:746 +0x16c ================== Copy package concurrency type WebsiteChecker func(string) bool type result struct { string bool } func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { results := make(map[string]bool) resultChannel := make(chan result) for _, url := range urls { go func(u string) { resultChannel <- result{u, wc(u)} }(url) } for i := 0; i < len(urls); i++ { result := <-resultChannel results[result.string] = result.bool } return results } Copy // Send statement resultChannel <- result{u, wc(u)} Copy // Receive expression result := <-resultChannel Copy pkg: github.com/gypsydave5/learn-go-with-tests/concurrency/v2 BenchmarkCheckWebsites-8 100 23406615 ns/op PASS ok github.com/gypsydave5/learn-go-with-tests/concurrency/v2 2.377s --- # Context-aware Reader | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader.md) . [**You can find all the code here**](https://github.com/quii/learn-go-with-tests/tree/master/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) . [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader#context-aware-reader) Context aware reader? ------------------------------------------------------------------------------------------------------------------------------------------------- First of all, a quick primer on `io.Reader`. If you've read other chapters in this book you will have ran into `io.Reader` when we've opened files, encoded JSON and various other common tasks. It's a simple abstraction over reading data from _something_ Copy type Reader interface { Read(p []byte) (n int, err error) } By using `io.Reader` you can gain a lot of re-use from the standard library, it's a very commonly used abstraction (along with its counterpart `io.Writer`) ### [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader#context-aware) Context aware? [In a previous chapter](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context) we discussed how we can use `context` to provide cancellation. This is especially useful if you're performing tasks which may be computationally expensive and you want to be able to stop them. When you're using an `io.Reader` you have no guarantees over speed, it could take 1 nanosecond or hundreds of hours. You might find it useful to be able to cancel these kind of tasks in your own application and that's what Mat and David wrote about. They combined two simple abstractions (`context.Context` and `io.Reader`) to solve this problem. Let's try and TDD some functionality so that we can wrap an `io.Reader` so it can be cancelled. Testing this poses an interesting challenge. Normally when using an `io.Reader` you're usually supplying it to some other function and you dont really concern yourself with the details; such as `json.NewDecoder` or `ioutil.ReadAll`. What we want to demonstrate is something like > Given an `io.Reader` with "ABCDEF", when I send a cancel signal half-way through I when I try to continue to read I get nothing else so all I get is "ABC" Let's look at the interface again. The `Reader`'s `Read` method will read the contents it has into a `[]byte` that we supply. So rather than reading everything, we could: * Supply a fixed-size byte array that doesnt fit all the contents * Send a cancel signal * Try and read again and this should return an error with 0 bytes read For now, let's just write a "happy path" test where there is no cancellation, just so we can get familiar with the problem without having to write any production code yet. * Make an `io.Reader` from a string with some data * A byte array to read into which is smaller than the contents of the reader * Call read, check the contents, repeat. From this we can imagine sending some kind of cancel signal before the second read to change behaviour. Now we've seen how it works we'll TDD the rest of the functionality. [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader#write-the-test-first) Write the test first ------------------------------------------------------------------------------------------------------------------------------------------------ We want to be able to compose an `io.Reader` with a `context.Context`. With TDD it's best to start with imagining your desired API and write a test for it. From there let the compiler and failing test output can guide us to a solution [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader#try-to-run-the-test) Try to run the test ---------------------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ We'll need to define this function and it should return an `io.Reader` If you try and run it As expected [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader#write-enough-code-to-make-it-pass) Write enough code to make it pass -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- For now, we'll just return the `io.Reader` we pass in The test should now pass. I know, I know, this seems silly and pedantic but before charging in to the fancy work it is important that we have _some_ verification that we haven't broken the "normal" behaviour of an `io.Reader` and this test will give us confidence as we move forward. [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader#write-the-test-first-1) Write the test first -------------------------------------------------------------------------------------------------------------------------------------------------- Next we need to try and cancel. We can more or less copy the first test but now we're: * Creating a `context.Context` with cancellation so we can `cancel` after the first read * For our code to work we'll need to pass `ctx` to our function * We then assert that post-`cancel` nothing was read [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader#try-to-run-the-test-1) Try to run the test ------------------------------------------------------------------------------------------------------------------------------------------------ [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) Write the minimal amount of code for the test to run and check the failing test output -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- The compiler is telling us what to do; update our signature to accept a context (You'll need to update the first test to pass in `context.Background` too) You should now see a very clear failing test output [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader#write-enough-code-to-make-it-pass-1) Write enough code to make it pass ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- At this point, it's copy and paste from the original post by Mat and David but we'll still take it slowly and iteratively. We know we need to have a type that encapsulates the `io.Reader` that we read from and the `context.Context` so let's create that and try and return it from our function instead of the original `io.Reader` As I have stressed many times in this book, go slowly and let the compiler help you The abstraction feels right, but it doesn't implement the interface we need (`io.Reader`) so let's add the method. Run the tests and they should _compile_ but panic. This is still progress. Let's make the first test pass by just _delegating_ the call to our underlying `io.Reader` At this point we have our happy path test passing again and it feels like we have our stuff abstracted nicely To make our second test pass we need to check the `context.Context` to see if it has been cancelled. All tests should now pass. You'll notice how we return the error from the `context.Context`. This allows callers of the code to inspect the various reasons cancellation has occurred and this is covered more in the original post. [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader#wrapping-up) Wrapping up ------------------------------------------------------------------------------------------------------------------------------ * Small interfaces are good and are easily composed * When you're trying to augment one thing (e.g `io.Reader`) with another you usually want to reach for the [delegation pattern](https://en.wikipedia.org/wiki/Delegation_pattern) > In software engineering, the delegation pattern is an object-oriented design pattern that allows object composition to achieve the same code reuse as inheritance. * An easy way to start this kind of work is to wrap your delegate and write a test that asserts it behaves how the delegate normally does before you start composing other parts to change behaviour. This will help you to keep things working correctly as you code toward your goal [PreviousError types](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/error-types) [NextRevisiting HTTP Handlers](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/http-handlers-revisited) Last updated 6 years ago * [Context aware reader?](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader#context-aware-reader) * [Context aware?](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader#context-aware) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader#write-the-test-first) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader#try-to-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader#write-enough-code-to-make-it-pass) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader#write-the-test-first-1) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader#try-to-run-the-test-1) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader#write-enough-code-to-make-it-pass-1) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader#wrapping-up) Copy type Reader interface { Read(p []byte) (n int, err error) } Copy func TestContextAwareReader(t *testing.T) { t.Run("lets just see how a normal reader works", func(t *testing.T) { rdr := strings.NewReader("123456") got := make([]byte, 3) _, err := rdr.Read(got) if err != nil { t.Fatal(err) } assertBufferHas(t, got, "123") _, err = rdr.Read(got) if err != nil { t.Fatal(err) } assertBufferHas(t, got, "456") }) } func assertBufferHas(t *testing.T, buf []byte, want string) { t.Helper() got := string(buf) if got != want { t.Errorf("got %q, want %q", got, want) } } Copy t.Run("behaves like a normal reader", func(t *testing.T) { rdr := NewCancellableReader(strings.NewReader("123456")) got := make([]byte, 3) _, err := rdr.Read(got) if err != nil { t.Fatal(err) } assertBufferHas(t, got, "123") _, err = rdr.Read(got) if err != nil { t.Fatal(err) } assertBufferHas(t, got, "456") }) Copy ./cancel_readers_test.go:12:10: undefined: NewCancellableReader Copy func NewCancellableReader(rdr io.Reader) io.Reader { return nil } Copy === RUN TestCancelReaders === RUN TestCancelReaders/behaves_like_a_normal_reader panic: runtime error: invalid memory address or nil pointer dereference [recovered] panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x10f8fb5] Copy func NewCancellableReader(rdr io.Reader) io.Reader { return rdr } Copy t.Run("stops reading when cancelled", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) rdr := NewCancellableReader(ctx, strings.NewReader("123456")) got := make([]byte, 3) _, err := rdr.Read(got) if err != nil { t.Fatal(err) } assertBufferHas(t, got, "123") cancel() n, err := rdr.Read(got) if err == nil { t.Error("expected an error after cancellation but didnt get one") } if n > 0 { t.Errorf("expected 0 bytes to be read after cancellation but %d were read", n) } }) Copy ./cancel_readers_test.go:33:30: too many arguments in call to NewCancellableReader have (context.Context, *strings.Reader) want (io.Reader) Copy func NewCancellableReader(ctx context.Context, rdr io.Reader) io.Reader { return rdr } Copy === RUN TestCancelReaders === RUN TestCancelReaders/stops_reading_when_cancelled --- FAIL: TestCancelReaders (0.00s) --- FAIL: TestCancelReaders/stops_reading_when_cancelled (0.00s) cancel_readers_test.go:48: expected an error but didnt get one cancel_readers_test.go:52: expected 0 bytes to be read after cancellation but 3 were read Copy func NewCancellableReader(ctx context.Context, rdr io.Reader) io.Reader { return &readerCtx{ ctx: ctx, delegate: rdr, } } type readerCtx struct { ctx context.Context delegate io.Reader } Copy ./cancel_readers_test.go:60:3: cannot use &readerCtx literal (type *readerCtx) as type io.Reader in return argument: *readerCtx does not implement io.Reader (missing Read method) Copy func (r *readerCtx) Read(p []byte) (n int, err error) { panic("implement me") } Copy func (r readerCtx) Read(p []byte) (n int, err error) { return r.delegate.Read(p) } Copy func (r readerCtx) Read(p []byte) (n int, err error) { if err := r.ctx.Err(); err != nil { return 0, err } return r.delegate.Read(p) } --- # Select | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select.md) . [**You can find all the code for this chapter here**](https://github.com/quii/learn-go-with-tests/tree/master/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`. For this, we will be using * `net/http` to make the HTTP calls. * `net/http/httptest` to help us test them. * goroutines. * `select` to synchronise processes. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#write-the-test-first) Write the test first ---------------------------------------------------------------------------------------------------------------------------- Let's start with something naive to get us going. Copy func TestRacer(t *testing.T) { slowURL := "http://www.facebook.com" fastURL := "http://www.quii.co.uk" want := fastURL got := Racer(slowURL, fastURL) if got != want { t.Errorf("got %q, want %q", got, want) } } We know this isn't perfect and has problems but it will get us going. It's important not to get too hung-up on getting things perfect first time. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#try-to-run-the-test) Try to run the test -------------------------------------------------------------------------------------------------------------------------- `./racer_test.go:14:9: undefined: Racer` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- `racer_test.go:25: got '', want 'http://www.quii.co.uk'` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#write-enough-code-to-make-it-pass) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------ For each URL: 1. We use `time.Now()` to record just before we try and get the `URL`. 2. Then we use [`http.Get`](https://golang.org/pkg/net/http/#Client.Get) to try and get the contents of the `URL`. This function returns an [`http.Response`](https://golang.org/pkg/net/http/#Response) and an `error` but so far we are not interested in these values. 3. `time.Since` takes the start time and returns a `time.Duration` of the difference. Once we have done this we simply compare the durations to see which is the quickest. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#problems) Problems This may or may not make the test pass for you. The problem is we're reaching out to real websites to test our own logic. Testing code that uses HTTP is so common that Go has tools in the standard library to help you test it. In the mocking and dependency injection chapters, we covered how ideally we don't want to be relying on external services to test our code because they can be * Slow * Flaky * Can't test edge cases In the standard library, there is a package called [`net/http/httptest`](https://golang.org/pkg/net/http/httptest/) where you can easily create a mock HTTP server. Let's change our tests to use mocks so we have reliable servers to test against that we can control. The syntax may look a bit busy but just take your time. `httptest.NewServer` takes an `http.HandlerFunc` which we are sending in via an _anonymous function_. `http.HandlerFunc` is a type that looks like this: `type HandlerFunc func(ResponseWriter, *Request)`. All it's really saying is it needs a function that takes a `ResponseWriter` and a `Request`, which is not too surprising for an HTTP server. It turns out there's really no extra magic here, **this is also how you would write a** _**real**_ **HTTP server in Go**. The only difference is we are wrapping it in an `httptest.NewServer` which makes it easier to use with testing, as it finds an open port to listen on and then you can close it when you're done with your test. Inside our two servers, we make the slow one have a short `time.Sleep` when we get a request to make it slower than the other one. Both servers then write an `OK` response with `w.WriteHeader(http.StatusOK)` back to the caller. If you re-run the test it will definitely pass now and should be faster. Play with these sleeps to deliberately break the test. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#refactor) Refactor ---------------------------------------------------------------------------------------------------- We have some duplication in both our production code and test code. This DRY-ing up makes our `Racer` code a lot easier to read. We've refactored creating our fake servers into a function called `makeDelayedServer` to move some uninteresting code out of the test and reduce repetition. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#defer) `defer` By prefixing a function call with `defer` it will now call that function _at the end of the containing function_. Sometimes you will need to cleanup resources, such as closing a file or in our case closing a server so that it does not continue to listen to a port. You want this to execute at the end of the function, but keep the instruction near where you created the server for the benefit of future readers of the code. Our refactoring is an improvement and is a reasonable solution given the Go features covered so far, but we can make the solution simpler. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#synchronising-processes) Synchronising processes * Why are we testing the speeds of the websites one after another when Go is great at concurrency? We should be able to check both at the same time. * We don't really care about _the exact response times_ of the requests, we just want to know which one comes back first. To do this, we're going to introduce a new construct called `select` which helps us synchronise processes really easily and clearly. #### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#ping) `ping` 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! Why `struct{}` and not another type like a `bool`? Well, a `chan struct{}` is the smallest data type available from a memory perspective so we get no allocation versus a `bool`. Since we are closing and not sending anything on the chan, why allocate anything? Inside the same function, we start a goroutine which will send a signal into that channel once we have completed `http.Get(url)`. **Always make channels** Notice how we have to use `make` when creating a channel; rather than say `var ch chan struct{}`. When you use `var` the variable will be initialised with the "zero" value of the type. So for `string` it is `""`, `int` it is 0, etc. For channels the zero value is `nil` and if you try and send to it with `<-` it will block forever because you cannot send to `nil` channels [You can see this in action in The Go Playground](https://play.golang.org/p/IIbeAox5jKA) #### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#select) `select` If you recall from the concurrency chapter, you can wait for values to be sent to a channel with `myVar := <-ch`. This is a _blocking_ call, as you're waiting for a value. What `select` lets you do is wait on _multiple_ channels. The first one to send a value "wins" and the code underneath the `case` is executed. We use `ping` in our `select` to set up two channels for each of our `URL`s. Whichever one writes to its channel first will have its code executed in the `select`, which results in its `URL` being returned (and being the winner). After these changes, the intent behind our code is very clear and the implementation is actually simpler. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#timeouts) Timeouts Our final requirement was to return an error if `Racer` takes longer than 10 seconds. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#write-the-test-first-1) Write the test first ------------------------------------------------------------------------------------------------------------------------------ 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`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#try-to-run-the-test-1) Try to run the test ---------------------------------------------------------------------------------------------------------------------------- `./racer_test.go:37:10: assignment mismatch: 2 variables but 1 values` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) Write the minimal amount of code for the test to run and check the failing test output ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Change the signature of `Racer` to return the winner and an `error`. Return `nil` for our happy cases. The compiler will complain about your _first test_ only looking for one value so change that line to `got, _ := Racer(slowURL, fastURL)`, knowing that we should check we _don't_ get an error in our happy scenario. If you run it now after 11 seconds it will fail. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#write-enough-code-to-make-it-pass-1) Write enough code to make it pass -------------------------------------------------------------------------------------------------------------------------------------------------------- `time.After` is a very handy function when using `select`. Although it didn't happen in our case you can potentially write code that blocks forever if the channels you're listening on never return a value. `time.After` returns a `chan` (like `ping`) and will send a signal down it after the amount of time you define. For us this is perfect; if `a` or `b` manage to return they win, but if we get to 10 seconds then our `time.After` will send a signal and we'll return an `error`. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#slow-tests) Slow tests The problem we have is that this test takes 10 seconds to run. For such a simple bit of logic, this doesn't feel great. What we can do is make the timeout configurable. So in our test, we can have a very short timeout and then when the code is used in the real world it can be set to 10 seconds. Our tests now won't compile because we're not supplying a timeout. Before rushing in to add this default value to both our tests let's _listen to them_. * Do we care about the timeout in the "happy" test? * The requirements were explicit about the timeout. Given this knowledge, let's do a little refactoring to be sympathetic to both our tests and the users of our code. Our users and our first test can use `Racer` (which uses `ConfigurableRacer` under the hood) and our sad path test can use `ConfigurableRacer`. I added one final check on the first test to verify we don't get an `error`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#wrapping-up) Wrapping up ---------------------------------------------------------------------------------------------------------- ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#select-1) `select` * Helps you wait on multiple channels. * Sometimes you'll want to include `time.After` in one of your `cases` to prevent your system blocking forever. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#httptest) `httptest` * A convenient way of creating test servers so you can have reliable and controllable tests. * Using the same interfaces as the "real" `net/http` servers which is consistent and less for you to learn. [PreviousConcurrency](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/concurrency) [NextReflection](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection) Last updated 5 years ago * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#write-the-test-first) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#try-to-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#write-enough-code-to-make-it-pass) * [Problems](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#problems) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#refactor) * [defer](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#defer) * [Synchronising processes](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#synchronising-processes) * [Timeouts](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#timeouts) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#write-the-test-first-1) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#try-to-run-the-test-1) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#write-enough-code-to-make-it-pass-1) * [Slow tests](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#slow-tests) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#wrapping-up) * [select](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#select-1) * [httptest](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select#httptest) Copy func Racer(a, b string) (winner string) { return } Copy func Racer(a, b string) (winner string) { startA := time.Now() http.Get(a) aDuration := time.Since(startA) startB := time.Now() http.Get(b) bDuration := time.Since(startB) if aDuration < bDuration { return a } return b } Copy func TestRacer(t *testing.T) { slowServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(20 * time.Millisecond) w.WriteHeader(http.StatusOK) })) fastServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) slowURL := slowServer.URL fastURL := fastServer.URL want := fastURL got := Racer(slowURL, fastURL) if got != want { t.Errorf("got %q, want %q", got, want) } slowServer.Close() fastServer.Close() } Copy func Racer(a, b string) (winner string) { aDuration := measureResponseTime(a) bDuration := measureResponseTime(b) if aDuration < bDuration { return a } return b } func measureResponseTime(url string) time.Duration { start := time.Now() http.Get(url) return time.Since(start) } Copy func TestRacer(t *testing.T) { slowServer := makeDelayedServer(20 * time.Millisecond) fastServer := makeDelayedServer(0 * time.Millisecond) defer slowServer.Close() defer fastServer.Close() slowURL := slowServer.URL fastURL := fastServer.URL want := fastURL got := Racer(slowURL, fastURL) if got != want { t.Errorf("got %q, want %q", got, want) } } func makeDelayedServer(delay time.Duration) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(delay) w.WriteHeader(http.StatusOK) })) } Copy func Racer(a, b string) (winner string) { select { case <-ping(a): return a case <-ping(b): return b } } func ping(url string) chan struct{} { ch := make(chan struct{}) go func() { http.Get(url) close(ch) }() return ch } Copy t.Run("returns an error if a server doesn't respond within 10s", func(t *testing.T) { serverA := makeDelayedServer(11 * time.Second) serverB := makeDelayedServer(12 * time.Second) defer serverA.Close() defer serverB.Close() _, err := Racer(serverA.URL, serverB.URL) if err == nil { t.Error("expected an error but didn't get one") } }) Copy func Racer(a, b string) (winner string, error error) { select { case <-ping(a): return a, nil case <-ping(b): return b, nil } } Copy --- FAIL: TestRacer (12.00s) --- FAIL: TestRacer/returns_an_error_if_a_server_doesn't_respond_within_10s (12.00s) racer_test.go:40: expected an error but didn't get one Copy func Racer(a, b string) (winner string, error error) { select { case <-ping(a): return a, nil case <-ping(b): return b, nil case <-time.After(10 * time.Second): return "", fmt.Errorf("timed out waiting for %s and %s", a, b) } } Copy func Racer(a, b string, timeout time.Duration) (winner string, error error) { select { case <-ping(a): return a, nil case <-ping(b): return b, nil case <-time.After(timeout): return "", fmt.Errorf("timed out waiting for %s and %s", a, b) } } Copy var tenSecondTimeout = 10 * time.Second func Racer(a, b string) (winner string, error error) { return ConfigurableRacer(a, b, tenSecondTimeout) } func ConfigurableRacer(a, b string, timeout time.Duration) (winner string, error error) { select { case <-ping(a): return a, nil case <-ping(b): return b, nil case <-time.After(timeout): return "", fmt.Errorf("timed out waiting for %s and %s", a, b) } } Copy 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) defer slowServer.Close() defer fastServer.Close() slowURL := slowServer.URL fastURL := fastServer.URL want := fastURL got, err := Racer(slowURL, fastURL) if err != nil { t.Fatalf("did not expect an error but got one %v", err) } if got != want { t.Errorf("got %q, want %q", got, want) } }) t.Run("returns an error if a server doesn't respond within the specified time", func(t *testing.T) { server := makeDelayedServer(25 * time.Millisecond) defer server.Close() _, err := ConfigurableRacer(server.URL, server.URL, 20*time.Millisecond) if err == nil { t.Error("expected an error but didn't get one") } }) } --- # OS Exec | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/os-exec.md) . [**You can find all the code here**](https://github.com/quii/learn-go-with-tests/tree/master/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/) > I am executing a command using os/exec.Command() which generated XML data. The command will be executed in a function called GetData(). > > In order to test GetData(), I have some testdata which I created. > > In my \_test.go I have a TestGetData which calls GetData() but that will use os.exec, instead I would like for it to use my testdata. > > What is a good way to achieve this? When calling GetData should I have a "test" flag mode so it will read a file ie GetData(mode string)? 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](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection) so that you can model your dependencies and separate concerns. I have taken the liberty of guessing what the code might look like Copy type Payload struct { Message string `xml:"message"` } func GetData() string { cmd := exec.Command("cat", "msg.xml") out, _ := cmd.StdoutPipe() var payload Payload decoder := xml.NewDecoder(out) // these 3 can return errors but I'm ignoring for brevity cmd.Start() decoder.Decode(&payload) cmd.Wait() return strings.ToUpper(payload.Message) } * 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) . * 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` I wrote a simple test to show it in action [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/os-exec#testable-code) Testable code --------------------------------------------------------------------------------------------------------------------- Testable code is decoupled and single purpose. To me it feels like there are two main concerns for this code 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 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. **The problem with GetData is the business logic is coupled with the means of getting the XML. To make our design better we need to decouple them** Our `TestGetData` can act as our integration test between our two concerns so we'll keep hold of that to make sure it keeps working. Here is what the newly separated code looks like Now that `GetData` takes its input from just an `io.Reader` we have made it testable and it is no longer concerned how the data is retrieved; people can re-use the function with anything that returns an `io.Reader` (which is extremely common). For example we could start fetching the XML from a URL instead of the command line. 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. [PreviousWebSockets](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets) [NextError types](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/error-types) Last updated 6 years ago Copy Happy New Year! Copy func TestGetData(t *testing.T) { got := GetData() want := "HAPPY NEW YEAR!" if got != want { t.Errorf("got %q, want %q", got, want) } } Copy type Payload struct { Message string `xml:"message"` } func GetData(data io.Reader) string { var payload Payload xml.NewDecoder(data).Decode(&payload) return strings.ToUpper(payload.Message) } func getXMLFromCommand() io.Reader { cmd := exec.Command("cat", "msg.xml") out, _ := cmd.StdoutPipe() cmd.Start() data, _ := ioutil.ReadAll(out) cmd.Wait() return bytes.NewReader(data) } func TestGetDataIntegration(t *testing.T) { got := GetData(getXMLFromCommand()) want := "HAPPY NEW YEAR!" if got != want { t.Errorf("got %q, want %q", got, want) } } Copy func TestGetData(t *testing.T) { input := strings.NewReader(` Cats are the best animal `) got := GetData(input) want := "CATS ARE THE BEST ANIMAL" if got != want { t.Errorf("got %q, want %q", got, want) } } --- # Mocking | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking.md) . [**You can find all the code for this chapter here**](https://github.com/quii/learn-go-with-tests/tree/master/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. Copy 3 2 1 Go! We'll tackle this by writing a function called `Countdown` which we will then put inside a `main` program so it looks something like this: Copy package main func main() { Countdown() } While this is a pretty trivial program, to test it fully we will need as always to take an _iterative_, _test-driven_ approach. What do I mean by iterative? We make sure we take the smallest steps we can to have _useful software_. We don't want to spend a long time with code that will theoretically work after some hacking because that's often how developers fall down rabbit holes. **It's an important skill to be able to slice up requirements as small as you can so you can have** _**working software**_**.** Here's how we can divide our work up and iterate on it: * Print 3 * Print 3, 2, 1 and Go! * Wait a second between each line [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#write-the-test-first) Write the test first ----------------------------------------------------------------------------------------------------------------------------- Our software needs to print to stdout and we saw how we could use DI to facilitate testing this in the DI section. If anything like `buffer` is unfamiliar to you, re-read [the previous section](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection) . We know we want our `Countdown` function to write data somewhere and `io.Writer` is the de-facto way of capturing that as an interface in Go. * In `main` we will send to `os.Stdout` so our users see the countdown printed to the terminal. * In test we will send to `bytes.Buffer` so our tests can capture what data is being generated. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#try-and-run-the-test) Try and run the test ----------------------------------------------------------------------------------------------------------------------------- `./countdown_test.go:11:2: undefined: Countdown` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Define `Countdown` Try again The compiler is telling you what your function signature could be, so update it. `countdown_test.go:17: got '' want '3'` Perfect! [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#write-enough-code-to-make-it-pass) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------- We're using `fmt.Fprint` which takes an `io.Writer` (like `*bytes.Buffer`) and sends a `string` to it. The test should pass. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#refactor) Refactor ----------------------------------------------------------------------------------------------------- We know that while `*bytes.Buffer` works, it would be better to use a general purpose interface instead. Re-run the tests and they should be passing. To complete matters, let's now wire up our function into a `main` so we have some working software to reassure ourselves we're making progress. Try and run the program and be amazed at your handywork. Yes this seems trivial but this approach is what I would recommend for any project. **Take a thin slice of functionality and make it work end-to-end, backed by tests.** Next we can make it print 2,1 and then "Go!". [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#write-the-test-first-1) Write the test first ------------------------------------------------------------------------------------------------------------------------------- By investing in getting the overall plumbing working right, we can iterate on our solution safely and easily. We will no longer need to stop and re-run the program to be confident of it working as all the logic is tested. The backtick syntax is another way of creating a `string` but lets you put things like newlines which is perfect for our test. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#try-and-run-the-test-1) Try and run the test ------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#write-enough-code-to-make-it-pass-1) Write enough code to make it pass --------------------------------------------------------------------------------------------------------------------------------------------------------- Use a `for` loop counting backwards with `i--` and use `fmt.Fprintln` to print to `out` with our number followed by a newline character. Finally use `fmt.Fprint` to send "Go!" aftward. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#refactor-1) Refactor ------------------------------------------------------------------------------------------------------- There's not much to refactor other than refactoring some magic values into named constants. If you run the program now, you should get the desired output but we don't have it as a dramatic countdown with the 1 second pauses. Go lets you achieve this with `time.Sleep`. Try adding it in to our code. If you run the program it works as we want it to. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#mocking) Mocking --------------------------------------------------------------------------------------------------- The tests still pass and the software works as intended but we have some problems: * Our tests take 4 seconds to run. * Every forward thinking post about software development emphasises the importance of quick feedback loops. * **Slow tests ruin developer productivity**. * Imagine if the requirements get more sophisticated warranting more tests. Are we happy with 4s added to the test run for every new test of `Countdown`? * We have not tested an important property of our function. We have a dependency on `Sleep`ing which we need to extract so we can then control it in our tests. If we can _mock_ `time.Sleep` we can use _dependency injection_ to use it instead of a "real" `time.Sleep` and then we can **spy on the calls** to make assertions on them. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#write-the-test-first-2) Write the test first ------------------------------------------------------------------------------------------------------------------------------- Let's define our dependency as an interface. This lets us then use a _real_ Sleeper in `main` and a _spy sleeper_ in our tests. By using an interface our `Countdown` function is oblivious to this and adds some flexibility for the caller. I made a design decision that our `Countdown` function would not be responsible for how long the sleep is. This simplifies our code a little for now at least and means a user of our function can configure that sleepiness however they like. Now we need to make a _mock_ of it for our tests to use. _Spies_ are a kind of _mock_ which can record how a dependency is used. They can record the arguments sent in, how many times it has been called, etc. In our case, we're keeping track of how many times `Sleep()` is called so we can check it in our test. Update the tests to inject a dependency on our Spy and assert that the sleep has been called 4 times. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#try-and-run-the-test-2) Try and run the test ------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) Write the minimal amount of code for the test to run and check the failing test output ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- We need to update `Countdown` to accept our `Sleeper` If you try again, your `main` will no longer compile for the same reason Let's create a _real_ sleeper which implements the interface we need We can then use it in our real application like so [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#write-enough-code-to-make-it-pass-2) Write enough code to make it pass --------------------------------------------------------------------------------------------------------------------------------------------------------- The test is now compiling but not passing because we're still calling the `time.Sleep` rather than the injected in dependency. Let's fix that. The test should pass and no longer taking 4 seconds. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#still-some-problems) Still some problems There's still another important property we haven't tested. `Countdown` should sleep before each print, e.g: * `Sleep` * `Print N` * `Sleep` * `Print N-1` * `Sleep` * `Print Go!` * etc Our latest change only asserts that it has slept 4 times, but those sleeps could occur out of sequence. When writing tests if you're not confident that your tests are giving you sufficient confidence, just break it! (make sure you have committed your changes to source control first though). Change the code to the following If you run your tests they should still be passing even though the implementation is wrong. Let's use spying again with a new test to check the order of operations is correct. We have two different dependencies and we want to record all of their operations into one list. So we'll create _one spy for them both_. Our `CountdownOperationsSpy` implements both `io.Writer` and `Sleeper`, recording every call into one slice. In this test we're only concerned about the order of operations, so just recording them as list of named operations is sufficient. We can now add a sub-test into our test suite which verifies our sleeps and prints operate in the order we hope This test should now fail. Revert `Countdown` back to how it was to fix the test. We now have two tests spying on the `Sleeper` so we can now refactor our test so one is testing what is being printed and the other one is ensuring we're sleeping in between the prints. Finally we can delete our first spy as it's not used anymore. We now have our function and its 2 important properties properly tested. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#extending-sleeper-to-be-configurable) Extending Sleeper to be configurable ------------------------------------------------------------------------------------------------------------------------------------------------------------- A nice feature would be for the `Sleeper` to be configurable. This means that we can adjust the sleep time in our main program. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#write-the-test-first-3) Write the test first Let's first create a new type for `ConfigurableSleeper` that accepts what we need for configuration and testing. We are using `duration` to configure the time slept and `sleep` as a way to pass in a sleep function. The signature of `sleep` is the same as for `time.Sleep` allowing us to use `time.Sleep` in our real implementation and the following spy in our tests: With our spy in place, we can create a new test for the configurable sleeper. There should be nothing new in this test and it is setup very similar to the previous mock tests. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#try-and-run-the-test-3) Try and run the test You should see a very clear error message indicating that we do not have a `Sleep` method created on our `ConfigurableSleeper`. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-failing-test-output) Write the minimal amount of code for the test to run and check failing test output With our new `Sleep` function implemented we have a failing test. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#write-enough-code-to-make-it-pass-3) Write enough code to make it pass All we need to do now is implement the `Sleep` function for `ConfigurableSleeper`. With this change all of the tests should be passing again and you might wonder why all the hassle as the main program didn't change at all. Hopefully it becomes clear after the following section. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#cleanup-and-refactor) Cleanup and refactor The last thing we need to do is to actually use our `ConfigurableSleeper` in the main function. If we run the tests and the program manually, we can see that all the behavior remains the same. Since we are using the `ConfigurableSleeper`, it is now safe to delete the `DefaultSleeper` implementation. Wrapping up our program and having a more [generic](https://stackoverflow.com/questions/19291776/whats-the-difference-between-abstraction-and-generalization) Sleeper with arbitrary long countdowns. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#but-isnt-mocking-evil) But isn't mocking evil? --------------------------------------------------------------------------------------------------------------------------------- You may have heard mocking is evil. Just like anything in software development it can be used for evil, just like [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) . People normally get in to a bad state when they don't _listen to their tests_ and are _not respecting the refactoring stage_. If your mocking code is becoming complicated or you are having to mock out lots of things to test something, you should _listen_ to that bad feeling and think about your code. Usually it is a sign of * The thing you are testing is having to do too many things (because it has too many dependencies to mock) * Break the module apart so it does less * Its dependencies are too fine-grained * Think about how you can consolidate some of these dependencies into one meaningful module * Your test is too concerned with implementation details * Favour testing expected behaviour rather than the implementation Normally a lot of mocking points to _bad abstraction_ in your code. **What people see here is a weakness in TDD but it is actually a strength**, more often than not poor test code is a result of bad design or put more nicely, well-designed code is easy to test. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#but-mocks-and-tests-are-still-making-my-life-hard) But mocks and tests are still making my life hard! Ever run into this situation? * You want to do some refactoring * To do this you end up changing lots of tests * You question TDD and make a post on Medium titled "Mocking considered harmful" This is usually a sign of you testing too much _implementation detail_. Try to make it so your tests are testing _useful behaviour_ unless the implementation is really important to how the system runs. It is sometimes hard to know _what level_ to test exactly but here are some thought processes and rules I try to follow: * **The definition of refactoring is that the code changes but the behaviour stays the same**. If you have decided to do some refactoring in theory you should be able to do make the commit without any test changes. So when writing a test ask yourself * Am I testing the behaviour I want or the implementation details? * If I were to refactor this code, would I have to make lots of changes to the tests? * Although Go lets you test private functions, I would avoid it as private functions are to do with implementation. * I feel like if a test is working with **more than 3 mocks then it is a red flag** - time for a rethink on the design * Use spies with caution. Spies let you see the insides of the algorithm you are writing which can be very useful but that means a tighter coupling between your test code and the implementation. **Be sure you actually care about these details if you're going to spy on them** As always, rules in software development aren't really rules and there can be exceptions. [Uncle Bob's article of "When to mock"](https://8thlight.com/blog/uncle-bob/2014/05/10/WhenToMock.html) has some excellent pointers. #### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#cant-i-just-use-a-mocking-framework) Can't I just use a mocking framework? Mocking requires no magic and is relatively simple; using a framework can make mocking seem more complicated than it is. We don't use automocking in this chapter so that we get: * a better understanding of how to mock * practise implementing interfaces In collaborative projects there is value auto-generating mocks. In a team, a mock generation tool codifies consistency around the test doubles. This will avoid inconsistently written test doubles which can translate to inconsistently written tests. You should only use a mock generator that generates test doubles against an interface. Any tool that overly dictates how tests are written, or that use lots of 'magic', can get in the sea. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#wrapping-up) Wrapping up ----------------------------------------------------------------------------------------------------------- ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#more-on-tdd-approach) More on TDD approach * When faced with less trivial examples, break the problem down into "thin vertical slices". Try to get to a point where you have _working software backed by tests_ as soon as you can, to avoid getting in rabbit holes and taking a "big bang" approach. * Once you have some working software it should be easier to _iterate with small steps_ until you arrive at the software you need. > "When to use iterative development? You should use iterative development only on projects that you want to succeed." Martin Fowler. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#mocking-1) Mocking * **Without mocking important areas of your code will be untested**. In our case we would not be able to test that our code paused between each print but there are countless other examples. Calling a service that _can_ fail? Wanting to test your system in a particular state? It is very hard to test these scenarios without mocking. * Without mocks you may have to set up databases and other third parties things just to test simple business rules. You're likely to have slow tests, resulting in **slow feedback loops**. * By having to spin up a database or a webservice to test something you're likely to have **fragile tests** due to the unreliability of such services. Once a developer learns about mocking it becomes very easy to over-test every single facet of a system in terms of the _way it works_ rather than _what it does_. Always be mindful about **the value of your tests** and what impact they would have in future refactoring. In this post about mocking we have only covered **Spies** which are a kind of mock. There are different kind of mocks. [Uncle Bob explains the types in a very easy to read article](https://8thlight.com/blog/uncle-bob/2014/05/14/TheLittleMocker.html) . In later chapters we will need to write code that depends on others for data, which is where we will show **Stubs** in action. [PreviousDependency Injection](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection) [NextConcurrency](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/concurrency) Last updated 6 years ago * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#write-the-test-first) * [Try and run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#try-and-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#write-enough-code-to-make-it-pass) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#refactor) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#write-the-test-first-1) * [Try and run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#try-and-run-the-test-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#write-enough-code-to-make-it-pass-1) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#refactor-1) * [Mocking](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#mocking) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#write-the-test-first-2) * [Try and run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#try-and-run-the-test-2) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#write-enough-code-to-make-it-pass-2) * [Still some problems](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#still-some-problems) * [Extending Sleeper to be configurable](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#extending-sleeper-to-be-configurable) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#write-the-test-first-3) * [Try and run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#try-and-run-the-test-3) * [Write the minimal amount of code for the test to run and check failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#write-enough-code-to-make-it-pass-3) * [Cleanup and refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#cleanup-and-refactor) * [But isn't mocking evil?](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#but-isnt-mocking-evil) * [But mocks and tests are still making my life hard!](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#but-mocks-and-tests-are-still-making-my-life-hard) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#wrapping-up) * [More on TDD approach](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#more-on-tdd-approach) * [Mocking](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/mocking#mocking-1) Copy func TestCountdown(t *testing.T) { buffer := &bytes.Buffer{} Countdown(buffer) got := buffer.String() want := "3" if got != want { t.Errorf("got %q want %q", got, want) } } Copy func Countdown() {} Copy ./countdown_test.go:11:11: too many arguments in call to Countdown have (*bytes.Buffer) want () Copy func Countdown(out *bytes.Buffer) {} Copy func Countdown(out *bytes.Buffer) { fmt.Fprint(out, "3") } Copy func Countdown(out io.Writer) { fmt.Fprint(out, "3") } Copy package main import ( "fmt" "io" "os" ) func Countdown(out io.Writer) { fmt.Fprint(out, "3") } func main() { Countdown(os.Stdout) } Copy func TestCountdown(t *testing.T) { buffer := &bytes.Buffer{} Countdown(buffer) got := buffer.String() want := `3 2 1 Go!` if got != want { t.Errorf("got %q want %q", got, want) } } Copy countdown_test.go:21: got '3' want '3 2 1 Go!' Copy func Countdown(out io.Writer) { for i := 3; i > 0; i-- { fmt.Fprintln(out, i) } fmt.Fprint(out, "Go!") } Copy const finalWord = "Go!" const countdownStart = 3 func Countdown(out io.Writer) { for i := countdownStart; i > 0; i-- { fmt.Fprintln(out, i) } fmt.Fprint(out, finalWord) } Copy func Countdown(out io.Writer) { for i := countdownStart; i > 0; i-- { time.Sleep(1 * time.Second) fmt.Fprintln(out, i) } time.Sleep(1 * time.Second) fmt.Fprint(out, finalWord) } Copy type Sleeper interface { Sleep() } Copy type SpySleeper struct { Calls int } func (s *SpySleeper) Sleep() { s.Calls++ } Copy func TestCountdown(t *testing.T) { buffer := &bytes.Buffer{} spySleeper := &SpySleeper{} Countdown(buffer, spySleeper) got := buffer.String() want := `3 2 1 Go!` if got != want { t.Errorf("got %q want %q", got, want) } if spySleeper.Calls != 4 { t.Errorf("not enough calls to sleeper, want 4 got %d", spySleeper.Calls) } } Copy too many arguments in call to Countdown have (*bytes.Buffer, *SpySleeper) want (io.Writer) Copy func Countdown(out io.Writer, sleeper Sleeper) { for i := countdownStart; i > 0; i-- { time.Sleep(1 * time.Second) fmt.Fprintln(out, i) } time.Sleep(1 * time.Second) fmt.Fprint(out, finalWord) } Copy ./main.go:26:11: not enough arguments in call to Countdown have (*os.File) want (io.Writer, Sleeper) Copy type DefaultSleeper struct {} func (d *DefaultSleeper) Sleep() { time.Sleep(1 * time.Second) } Copy func main() { sleeper := &DefaultSleeper{} Countdown(os.Stdout, sleeper) } Copy func Countdown(out io.Writer, sleeper Sleeper) { for i := countdownStart; i > 0; i-- { sleeper.Sleep() fmt.Fprintln(out, i) } sleeper.Sleep() fmt.Fprint(out, finalWord) } Copy func Countdown(out io.Writer, sleeper Sleeper) { for i := countdownStart; i > 0; i-- { sleeper.Sleep() } for i := countdownStart; i > 0; i-- { fmt.Fprintln(out, i) } sleeper.Sleep() fmt.Fprint(out, finalWord) } Copy type CountdownOperationsSpy struct { Calls []string } func (s *CountdownOperationsSpy) Sleep() { s.Calls = append(s.Calls, sleep) } func (s *CountdownOperationsSpy) Write(p []byte) (n int, err error) { s.Calls = append(s.Calls, write) return } const write = "write" const sleep = "sleep" Copy t.Run("sleep before every print", func(t *testing.T) { spySleepPrinter := &CountdownOperationsSpy{} Countdown(spySleepPrinter, spySleepPrinter) want := []string{ sleep, write, sleep, write, sleep, write, sleep, write, } if !reflect.DeepEqual(want, spySleepPrinter.Calls) { t.Errorf("wanted calls %v got %v", want, spySleepPrinter.Calls) } }) Copy func TestCountdown(t *testing.T) { t.Run("prints 3 to Go!", func(t *testing.T) { buffer := &bytes.Buffer{} Countdown(buffer, &CountdownOperationsSpy{}) got := buffer.String() want := `3 2 1 Go!` if got != want { t.Errorf("got %q want %q", got, want) } }) t.Run("sleep before every print", func(t *testing.T) { spySleepPrinter := &CountdownOperationsSpy{} Countdown(spySleepPrinter, spySleepPrinter) want := []string{ sleep, write, sleep, write, sleep, write, sleep, write, } if !reflect.DeepEqual(want, spySleepPrinter.Calls) { t.Errorf("wanted calls %v got %v", want, spySleepPrinter.Calls) } }) } Copy type ConfigurableSleeper struct { duration time.Duration sleep func(time.Duration) } Copy type SpyTime struct { durationSlept time.Duration } func (s *SpyTime) Sleep(duration time.Duration) { s.durationSlept = duration } Copy func TestConfigurableSleeper(t *testing.T) { sleepTime := 5 * time.Second spyTime := &SpyTime{} sleeper := ConfigurableSleeper{sleepTime, spyTime.Sleep} sleeper.Sleep() if spyTime.durationSlept != sleepTime { t.Errorf("should have slept for %v but slept for %v", sleepTime, spyTime.durationSlept) } } Copy sleeper.Sleep undefined (type ConfigurableSleeper has no field or method Sleep, but does have sleep) Copy func (c *ConfigurableSleeper) Sleep() { } Copy countdown_test.go:56: should have slept for 5s but slept for 0s Copy func (c *ConfigurableSleeper) Sleep() { c.sleep(c.duration) } Copy func main() { sleeper := &ConfigurableSleeper{1 * time.Second, time.Sleep} Countdown(os.Stdout, sleeper) } --- # Why unit tests and how to make them work for you | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/meta/why.md) . [Here's a link to a video of me chatting about this topic](https://www.youtube.com/watch?v=Kwtit8ZEK7U) If you're not into videos, here's wordy version of it. [](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#software) Software -------------------------------------------------------------------------------------- The promise of software is that it can change. This is why it is called _soft_ ware, it is malleable compared to hardware. A great engineering team should be an amazing asset to a company, writing systems that can evolve with a business to keep delivering value. So why are we so bad at it? How many projects do you hear about that outright fail? Or become "legacy" and have to be entirely re-written (and the re-writes often fail too!) How does a software system "fail" anyway? Can't it just be changed until it's correct? That's what we're promised! A lot of people are choosing Go to build systems because it has made a number of choices which one hopes will make it more legacy-proof. * Compared to my previous life of Scala where [I described how it has enough rope to hang yourself](http://www.quii.dev/Scala_-_Just_enough_rope_to_hang_yourself) , Go has only 25 keywords and _a lot_ of systems can be built from the standard library and a few other small libraries. The hope is that with Go you can write code and come back to it in 6 months time and it'll still make sense. * The tooling in respect to testing, benchmarking, linting & shipping is first class compared to most alternatives. * The standard library is brilliant. * Very fast compilation speed for tight feedback loops * The Go backward compatibility promise. It looks like Go will get generics and other features in the future but the designers have promised that even Go code you wrote 5 years ago will still build. I literally spent weeks upgrading a project from Scala 2.8 to 2.10. Even with all these great properties we can still make terrible systems, so we should look to the past and understand lessons in software engineering that apply no matter how shiny (or not) your language is. In 1974 a clever software engineer called [Manny Lehman](https://en.wikipedia.org/wiki/Manny_Lehman_%28computer_scientist%29) wrote [Lehman's laws of software evolution](https://en.wikipedia.org/wiki/Lehman%27s_laws_of_software_evolution) . > The laws describe a balance between forces driving new developments on one hand, and forces that slow down progress on the other hand. These forces seem like important things to understand if we have any hope of not being in an endless cycle of shipping systems that turn into legacy and then get re-written over and over again. [](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#the-law-of-continuous-change) The Law of Continuous Change ------------------------------------------------------------------------------------------------------------------------------ > Any software system used in the real-world must change or become less and less useful in the environment It feels obvious that a system _has_ to change or it becomes less useful but how often is this ignored? Many teams are incentivised to deliver a project on a particular date and then moved on to the next project. If the software is "lucky" there is at least some kind of hand-off to another set of individuals to maintain it, but they didn't write it of course. People often concern themselves with trying to pick a framework which will help them "deliver quickly" but not focusing on the longevity of the system in terms of how it needs to evolve. Even if you're an incredible software engineer, you will still fall victim to not knowing the future needs of your system. As the business changes some of the brilliant code you wrote is now no longer relevant. Lehman was on a roll in the 70s because he gave us another law to chew on. [](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#the-law-of-increasing-complexity) The Law of Increasing Complexity -------------------------------------------------------------------------------------------------------------------------------------- > As a system evolves, its complexity increases unless work is done to reduce it What he's saying here is we can't have software teams as blind feature factories, piling more and more features on to software in the hope it will survive in the long run. We **have** to keep managing the complexity of the system as the knowledge of our domain changes. [](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#refactoring) Refactoring -------------------------------------------------------------------------------------------- There are _many_ facets of software engineering that keeps software malleable, such as: * Developer empowerment * Generally "good" code. Sensible separation of concerns, etc etc * Communication skills * Architecture * Observability * Deployability * Automated tests * Feedback loops I am going to focus on refactoring. It's a phrase that gets thrown around a lot "we need to refactor this" - said to a developer on their first day of programming without a second thought. Where does the phrase come from? How is refactoring just different from writing code? I know that I and many others have _thought_ we were doing refactoring but we were mistaken [Martin Fowler describes how people are getting it wrong](https://martinfowler.com/bliki/RefactoringMalapropism.html) > However the term "refactoring" is often used when it's not appropriate. If somebody talks about a system being broken for a couple of days while they are refactoring, you can be pretty sure they are not refactoring. So what is it? ### [](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#factorisation) Factorisation When learning maths at school you probably learned about factorisation. Here's a very simple example Calculate `1/2 + 1/4` To do this you _factorise_ the denominators, turning the expression into `2/4 + 1/4` which you can then turn into `3/4`. We can take some important lessons from this. When we _factorise the expression_ we have **not changed the meaning of the expression**. Both of them equal `3/4` but we have made it easier for us to work with; by changing `1/2` to `2/4` it fits into our "domain" easier. When you refactor your code, you are trying to find ways of making your code easier to understand and "fit" into your current understanding of what the system needs to do. Crucially **you should not be changing behaviour**. #### [](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#an-example-in-go) An example in Go Here is a function which greets `name` in a particular `language` Having dozens of `if` statements doesn't feel good and we have a duplication of concatenating a language specific greeting with `,` and the `name.` So I'll refactor the code. The nature of this refactor isn't actually important, what's important is I haven't changed behaviour. When refactoring you can do whatever you like, add interfaces, new types, functions, methods etc. The only rule is you don't change behaviour ### [](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#when-refactoring-code-you-must-not-be-changing-behaviour) When refactoring code you must not be changing behaviour This is very important. If you are changing behaviour at the same time you are doing _two_ things at once. As software engineers we learn to break systems up into different files/packages/functions/etc because we know trying to understand a big blob of stuff is hard. We don't want to have to be thinking about lots of things at once because that's when we make mistakes. I've witnessed so many refactoring endeavours fail because the developers are biting off more than they can chew. When I was doing factorisations in maths classes with pen and paper I would have to manually check that I hadn't changed the meaning of the expressions in my head. How do we know we aren't changing behaviour when refactoring when working with code, especially on a system that is non-trivial? Those who choose not to write tests will typically be reliant on manual testing. For anything other than a small project this will be a tremendous time-sink and does not scale in the long run. **In order to safely refactor you need unit tests** because they provide * Confidence you can reshape code without worrying about changing behaviour * Documentation for humans as to how the system should behave * Much faster and more reliable feedback than manual testing #### [](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#an-example-in-go-1) An example in Go A unit test for our `Hello` function could look like this At the command line I can run `go test` and get immediate feedback as to whether my refactoring efforts have altered behaviour. In practice it's best to learn the magic button to run your tests within your editor/IDE. You want to get in to a state where you are doing * Small refactor * Run tests * Repeat All within a very tight feedback loop so you don't go down rabbit holes and make mistakes. Having a project where all your key behaviours are unit tested and give you feedback well under a second is a very empowering safety net to do bold refactoring when you need to. This helps us manage the incoming force of complexity that Lehman describes. [](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#if-unit-tests-are-so-great-why-is-there-sometimes-resistance-to-writing-them) If unit tests are so great, why is there sometimes resistance to writing them? -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- On the one hand you have people (like me) saying that unit tests are important for the long term health of your system because they ensure you can keep refactoring with confidence. On the other you have people describing experiences of unit tests actually _hindering_ refactoring. Ask yourself, how often do you have to change your tests when refactoring? Over the years I have been on many projects with very good test coverage and yet the engineers are reluctant to refactor because of the perceived effort of changing tests. This is the opposite of what we are promised! ### [](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#why-is-this-happening) Why is this happening? Imagine you were asked to develop a square and we thought the best way to accomplish that would be stick two triangles together. ![](https://quii.gitbook.io/learn-go-with-tests/~gitbook/image?url=https%3A%2F%2Fi.imgur.com%2Fela7SVf.jpg&width=768&dpr=3&quality=100&sign=7bd20485&sv=2) Two right-angled triangles to form a square We write our unit tests around our square to make sure the sides are equal and then we write some tests around our triangles. We want to make sure our triangles render correctly so we assert that the angles sum up to 180 degrees, perhaps check we make 2 of them, etc etc. Test coverage is really important and writing these tests is pretty easy so why not? A few weeks later The Law of Continuous Change strikes our system and a new developer makes some changes. She now believes it would be better if squares were formed with 2 rectangles instead of 2 triangles. ![](https://quii.gitbook.io/learn-go-with-tests/~gitbook/image?url=https%3A%2F%2Fi.imgur.com%2F1G6rYqD.jpg&width=768&dpr=3&quality=100&sign=dcd0fc7d&sv=2) Two rectangles to form a square She tries to do this refactor and gets mixed signals from a number of failing tests. Has she actually broken important behaviours here? She now has to dig through these triangle tests and try and understand what's going on. _It's not actually important that the square was formed out of triangles_ but **our tests have falsely elevated the importance of our implementation details**. [](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#favour-testing-behaviour-rather-than-implementation-detail) Favour testing behaviour rather than implementation detail ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ When I hear people complaining about unit tests it is often because the tests are at the wrong abstraction level. They're testing implementation details, overly spying on collaborators and mocking too much. I believe it stems from a misunderstanding of what unit tests are and chasing vanity metrics (test coverage). If I am saying just test behaviour, should we not just only write system/black-box tests? These kind of tests do have lots of value in terms of verifying key user journeys but they are typically expensive to write and slow to run. For that reason they're not too helpful for _refactoring_ because the feedback loop is slow. In addition black box tests don't tend to help you very much with root causes compared to unit tests. So what _is_ the right abstraction level? [](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#writing-effective-unit-tests-is-a-design-problem) Writing effective unit tests is a design problem ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- Forgetting about tests for a moment, it is desirable to have within your system self-contained, decoupled "units" centered around key concepts in your domain. I like to imagine these units as simple Lego bricks which have coherent APIs that I can combine with other bricks to make bigger systems. Underneath these APIs there could be dozens of things (types, functions et al) collaborating to make them work how they need to. For instance if you were writing a bank in Go, you might have an "account" package. It will present an API that does not leak implementation detail and is easy to integrate with. If you have these units that follow these properties you can write unit tests against their public APIs. _By definition_ these tests can only be testing useful behaviour. Underneath these units I am free to refactor the implementation as much as I need to and the tests for the most part should not get in the way. ### [](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#are-these-unit-tests) Are these unit tests? **YES**. Unit tests are against "units" like I described. They were _never_ about only being against a single class/function/whatever. [](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#bringing-these-concepts-together) Bringing these concepts together -------------------------------------------------------------------------------------------------------------------------------------- We've covered * Refactoring * Unit tests * Unit design What we can start to see is that these facets of software design reinforce each other. ### [](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#refactoring-1) Refactoring * Gives us signals about our unit tests. If we have to do manual checks, we need more tests. If tests are wrongly failing then our tests are at the wrong abstraction level (or have no value and should be deleted). * Helps us handle the complexities within and between our units. ### [](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#unit-tests) Unit tests * Give a safety net to refactor. * Verify and document the behaviour of our units. ### [](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#well-designed-units) (Well designed) units * Easy to write _meaningful_ unit tests. * Easy to refactor. Is there a process to help us arrive at a point where we can constantly refactor our code to manage complexity and keep our systems malleable? [](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#why-test-driven-development-tdd) Why Test Driven Development (TDD) -------------------------------------------------------------------------------------------------------------------------------------- Some people might take Lehman's quotes about how software has to change and overthink elaborate designs, wasting lots of time upfront trying to create the "perfect" extensible system and end up getting it wrong and going nowhere. This is the bad old days of software where an analyst team would spend 6 months writing a requirements document and an architect team would spend another 6 months coming up with a design and a few years later the whole project fails. I say bad old days but this still happens! Agile teaches us that we need to work iteratively, starting small and evolving the software so that we get fast feedback on the design of our software and how it works with real users; TDD enforces this approach. TDD addresses the laws that Lehman talks about and other lessons hard learned through history by encouraging a methodology of constantly refactoring and delivering iteratively. ### [](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#small-steps) Small steps * Write a small test for a small amount of desired behaviour * Check the test fails with a clear error (red) * Write the minimal amount of code to make the test pass (green) * Refactor * Repeat As you become proficient, this way of working will become natural and fast. You'll come to expect this feedback loop to not take very long and feel uneasy if you're in a state where the system isn't "green" because it indicates you may be down a rabbit hole. You'll always be driving small & useful functionality comfortably backed by the feedback from your tests. [](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#wrapping-up) Wrapping up -------------------------------------------------------------------------------------------- * The strength of software is that we can change it. _Most_ software will require change over time in unpredictable ways; but don't try and over-engineer because it's too hard to predict the future. * Instead we need to make it so we can keep our software malleable. In order to change software we have to refactor it as it evolves or it will turn into a mess * A good test suite can help you refactor quicker and in a less stressful manner * Writing good unit tests is a design problem so think about structuring your code so you have meaningful units that you can integrate together like Lego bricks. * TDD can help and force you to design well factored software iteratively, backed by tests to help future work as it arrives. [PreviousRevisiting HTTP Handlers](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/http-handlers-revisited) [NextContributing](https://quii.gitbook.io/learn-go-with-tests/master/meta/contributing) Last updated 6 years ago * [Software](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#software) * [The Law of Continuous Change](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#the-law-of-continuous-change) * [The Law of Increasing Complexity](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#the-law-of-increasing-complexity) * [Refactoring](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#refactoring) * [Factorisation](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#factorisation) * [When refactoring code you must not be changing behaviour](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#when-refactoring-code-you-must-not-be-changing-behaviour) * [If unit tests are so great, why is there sometimes resistance to writing them?](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#if-unit-tests-are-so-great-why-is-there-sometimes-resistance-to-writing-them) * [Why is this happening?](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#why-is-this-happening) * [Favour testing behaviour rather than implementation detail](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#favour-testing-behaviour-rather-than-implementation-detail) * [Writing effective unit tests is a design problem](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#writing-effective-unit-tests-is-a-design-problem) * [Are these unit tests?](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#are-these-unit-tests) * [Bringing these concepts together](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#bringing-these-concepts-together) * [Refactoring](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#refactoring-1) * [Unit tests](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#unit-tests) * [(Well designed) units](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#well-designed-units) * [Why Test Driven Development (TDD)](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#why-test-driven-development-tdd) * [Small steps](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#small-steps) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/meta/why#wrapping-up) Copy func Hello(name, language string) string { if language == "es" { return "Hola, " + name } if language == "fr" { return "Bonjour, " + name } // imagine dozens more languages return "Hello, " + name } Copy func Hello(name, language string) string { return fmt.Sprintf( "%s, %s", greeting(language), name, ) } var greetings = map[string]string { es: "Hola", fr: "Bonjour", //etc.. } func greeting(language string) string { greeting, exists := greetings[language] if exists { return greeting } return "Hello" } Copy func TestHello(t *testing.T) { got := Hello(“Chris”, es) want := "Hola, Chris" if got != want { t.Errorf("got %q want %q", got, want) } } --- # Context | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context.md) . [**You can find all the code for this chapter here**](https://github.com/quii/learn-go-with-tests/tree/master/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. 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 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. Copy func Server(store Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, store.Fetch()) } } The function `Server` takes a `Store` and returns us a `http.HandlerFunc`. Store is defined as: Copy type Store interface { Fetch() string } The returned function calls the `store`'s `Fetch` method to get the data and writes it to the response. We have a corresponding stub for `Store` which we use in a test. Copy type StubStore struct { response string } func (s *StubStore) Fetch() string { return s.response } func TestHandler(t *testing.T) { data := "hello, world" svr := Server(&StubStore{data}) request := httptest.NewRequest(http.MethodGet, "/", nil) response := httptest.NewRecorder() svr.ServeHTTP(response, request) if response.Body.String() != data { t.Errorf(`got "%s", want "%s"`, response.Body.String(), data) } } Now that we have a happy path, we want to make a more realistic scenario where the `Store` can't finish a`Fetch` before the user cancels the request. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#write-the-test-first) Write the test first ----------------------------------------------------------------------------------------------------------------------------- Our handler will need a way of telling the `Store` to cancel the work so update the interface. We will need to adjust our spy so it takes some time to return `data` and a way of knowing it has been told to cancel. We'll also rename it to `SpyStore` as we are now observing the way it is called. It'll have to add `Cancel` as a method to implement the `Store` interface. Let's add a new test where we cancel the request before 100 milliseconds and check the store to see if it gets cancelled. 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. 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`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#try-to-run-the-test) Try to run the test --------------------------------------------------------------------------------------------------------------------------- The test fails as we'd expect. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#write-enough-code-to-make-it-pass) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------- Remember to be disciplined with TDD. Write the _minimal_ amount of code to make our test pass. 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! We'll need to update our happy path test to assert that it does not get cancelled. Run both tests and the happy path test should now be failing and now we're forced to do a more sensible implementation. What have we done here? `context` has a method `Done()` which returns a channel which gets sent a signal when the context is "done" or "cancelled". We want to listen to that signal and call `store.Cancel` if we get it but we want to ignore it if our `Store` manages to `Fetch` before it. To manage this we run `Fetch` in a goroutine and it will write the result into a new channel `data`. We then use `select` to effectively race to the two asynchronous processes and then we either write a response or `Cancel`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#refactor) Refactor ----------------------------------------------------------------------------------------------------- We can refactor our test code a bit by making assertion methods on our spy Remember to pass in the `*testing.T` when creating the spy. 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. 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/) > Incoming requests to a server should create a Context, and outgoing calls to servers should accept a Context. The chain of function calls between them must propagate the Context, optionally replacing it with a derived Context created using WithCancel, WithDeadline, WithTimeout, or WithValue. When a Context is canceled, all Contexts derived from it are also canceled. From the [Go Blog: Context](https://blog.golang.org/context) again: > At Google, we require that Go programmers pass a Context parameter as the first argument to every function on the call path between incoming and outgoing requests. This allows Go code developed by many different teams to interoperate well. It provides simple control over timeouts and cancelation and ensures that critical values like security credentials transit Go programs properly. (Pause for a moment and think of the ramifications of every function having to send in a context, and the ergonomics of that.) Feeling a bit uneasy? Good. Let's try and follow that approach though and instead pass through the `context` to our `Store` and let it be responsible. That way it can also pass the `context` through to it's dependants and they too can be responsible for stopping themselves. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#write-the-test-first-1) Write the test first ------------------------------------------------------------------------------------------------------------------------------- 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. Delete the code inside our handler for now Update our `SpyStore` 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. 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. 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. Finally we can update our tests. Comment out our cancellation test so we can fix the happy path test first. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#try-to-run-the-test-1) Try to run the test ----------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#write-enough-code-to-make-it-pass-1) Write enough code to make it pass --------------------------------------------------------------------------------------------------------------------------------------------------------- Our happy path should be... happy. Now we can fix the other test. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#write-the-test-first-2) 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. Our `SpyResponseWriter` implements `http.ResponseWriter` so we can use it in the test. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#try-to-run-the-test-2) Try to run the test ----------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#write-enough-code-to-make-it-pass-2) Write enough code to make it pass --------------------------------------------------------------------------------------------------------------------------------------------------------- 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. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#wrapping-up) Wrapping up ----------------------------------------------------------------------------------------------------------- ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#what-weve-covered) What we've covered * How to test a HTTP handler that has had the request cancelled by the client. * How to use context to manage cancellation. * How to write a function that accepts `context` and uses it to cancel itself by using goroutines, `select` and channels. * Follow Google's guidelines as to how to manage cancellation by propagating request scoped context through your call-stack. * How to roll your own spy for `http.ResponseWriter` if you need it. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#what-about-context.value) What about context.Value ? [Michal Štrba](https://faiface.github.io/post/context-should-go-away-go2/) and I have a similar opinion. > 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_. 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. 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. #### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#but) But... On other hand, it can be helpful to include information that is orthogonal to a request in a context, such as a trace id. Potentially this information would not be needed by every function in your call-stack and would make your functional signatures very messy. [Jack Lindamood says **Context.Value should inform, not control**](https://medium.com/@cep21/how-to-correctly-use-context-context-in-go-1-7-8f2c0fafdf39) > The content of context.Value is for maintainers not users. It should never be required input for documented or expected results. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#additional-material) Additional material * I really enjoyed reading [Context should go away for Go 2 by Michal Štrba](https://faiface.github.io/post/context-should-go-away-go2/) . His argument is that having to pass `context` everywhere is a smell, that it's pointing to a deficiency in the language in respect to cancellation. He says it would better if this was somehow solved at the language level, rather than at a library level. Until that happens, you will need `context` if you want to manage long running processes. * The [Go blog further describes the motivation for working with `context` and has some examples](https://blog.golang.org/context) [PreviousSync](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync) [NextIntro to property based tests](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals) Last updated 6 years ago * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#write-the-test-first) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#try-to-run-the-test) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#write-enough-code-to-make-it-pass) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#refactor) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#write-the-test-first-1) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#try-to-run-the-test-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#write-enough-code-to-make-it-pass-1) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#write-the-test-first-2) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#try-to-run-the-test-2) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#write-enough-code-to-make-it-pass-2) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#wrapping-up) * [What we've covered](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#what-weve-covered) * [What about context.Value ?](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#what-about-context.value) * [Additional material](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context#additional-material) Copy type Store interface { Fetch() string Cancel() } Copy type SpyStore struct { response string cancelled bool } func (s *SpyStore) Fetch() string { time.Sleep(100 * time.Millisecond) return s.response } func (s *SpyStore) Cancel() { s.cancelled = true } Copy 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") } }) Copy --- FAIL: TestServer (0.00s) --- FAIL: TestServer/tells_store_to_cancel_work_if_request_is_cancelled (0.00s) context_test.go:62: store was not told to cancel Copy func Server(store Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { store.Cancel() fmt.Fprint(w, store.Fetch()) } } Copy t.Run("returns data from store", func(t *testing.T) { store := &SpyStore{response: data} svr := Server(store) request := httptest.NewRequest(http.MethodGet, "/", nil) response := httptest.NewRecorder() svr.ServeHTTP(response, request) 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") } }) Copy func Server(store Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() data := make(chan string, 1) go func() { data <- store.Fetch() }() select { case d := <-data: fmt.Fprint(w, d) case <-ctx.Done(): store.Cancel() } } } Copy func (s *SpyStore) assertWasCancelled() { s.t.Helper() if !s.cancelled { s.t.Errorf("store was not told to cancel") } } func (s *SpyStore) assertWasNotCancelled() { s.t.Helper() if s.cancelled { s.t.Errorf("store was told to cancel") } } Copy func TestServer(t *testing.T) { data := "hello, world" t.Run("returns data from store", func(t *testing.T) { store := &SpyStore{response: data, t: t} svr := Server(store) request := httptest.NewRequest(http.MethodGet, "/", nil) response := httptest.NewRecorder() svr.ServeHTTP(response, request) if response.Body.String() != data { t.Errorf(`got "%s", want "%s"`, response.Body.String(), data) } store.assertWasNotCancelled() }) t.Run("tells store to cancel work if request is cancelled", func(t *testing.T) { store := &SpyStore{response: data, t: t} 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) store.assertWasCancelled() }) } Copy type Store interface { Fetch(ctx context.Context) (string, error) } Copy func Server(store Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { } } Copy type SpyStore struct { response string t *testing.T } func (s *SpyStore) Fetch(ctx context.Context) (string, error) { data := make(chan string, 1) go func() { var result string for _, c := range s.response { select { case <-ctx.Done(): s.t.Log("spy store got cancelled") return default: time.Sleep(10 * time.Millisecond) result += string(c) } } data <- result }() select { case <-ctx.Done(): return "", ctx.Err() case res := <-data: return res, nil } } Copy t.Run("returns data from store", func(t *testing.T) { store := &SpyStore{response: data, t: t} svr := Server(store) request := httptest.NewRequest(http.MethodGet, "/", nil) response := httptest.NewRecorder() svr.ServeHTTP(response, request) if response.Body.String() != data { t.Errorf(`got "%s", want "%s"`, response.Body.String(), data) } }) Copy === RUN TestServer/returns_data_from_store --- FAIL: TestServer (0.00s) --- FAIL: TestServer/returns_data_from_store (0.00s) context_test.go:22: got "", want "hello, world" Copy func Server(store Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { data, _ := store.Fetch(r.Context()) fmt.Fprint(w, data) } } Copy type SpyResponseWriter struct { written bool } func (s *SpyResponseWriter) Header() http.Header { s.written = true return nil } func (s *SpyResponseWriter) Write([]byte) (int, error) { s.written = true return 0, errors.New("not implemented") } func (s *SpyResponseWriter) WriteHeader(statusCode int) { s.written = true } Copy t.Run("tells store to cancel work if request is cancelled", func(t *testing.T) { store := &SpyStore{response: data, t: t} 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 := &SpyResponseWriter{} svr.ServeHTTP(response, request) if response.written { t.Error("a response should not have been written") } }) Copy === RUN TestServer === RUN TestServer/tells_store_to_cancel_work_if_request_is_cancelled --- FAIL: TestServer (0.01s) --- FAIL: TestServer/tells_store_to_cancel_work_if_request_is_cancelled (0.01s) context_test.go:47: a response should not have been written Copy func Server(store Store) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { data, err := store.Fetch(r.Context()) if err != nil { return // todo: log error however you like } fmt.Fprint(w, data) } } --- # Error types | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/error-types.md) . [**You can find all the code here**](https://github.com/quii/learn-go-with-tests/tree/master/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.** Pedro on the Gopher Slack asks > If I’m creating an error like `fmt.Errorf("%s must be foo, got %s", bar, baz)`, is there a way to test equality without comparing the string value? Let's make up a function to help explore this idea. Copy // DumbGetter will get the string body of url if it gets a 200 func DumbGetter(url string) (string, error) { res, err := http.Get(url) if err != nil { return "", fmt.Errorf("problem fetching from %s, %v", url, err) } if res.StatusCode != http.StatusOK { return "", fmt.Errorf("did not get 200 from %s, got %d", url, res.StatusCode) } defer res.Body.Close() body, _ := ioutil.ReadAll(res.Body) // ignoring err for brevity return string(body), nil } It's not uncommon to write a function that might fail for different reasons and we want to make sure we handle each scenario correctly. As Pedro says, we _could_ write a test for the status error like so. This test creates a server which always returns `StatusTeapot` and then we use its URL as the argument to `DumbGetter` so we can see it handles non `200` responses correctly. [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/error-types#problems-with-this-way-of-testing) Problems with this way of testing ----------------------------------------------------------------------------------------------------------------------------------------------------------------- This book tries to emphasise _listen to your tests_ and this test doesn't _feel_ good: * We're constructing the same string as production code does to test it * It's annoying to read and write * Is the exact error message string what we're _actually concerned with_ ? What does this tell us? The ergonomics of our test would be reflected on another bit of code trying to use our code. How does a user of our code react to the specific kind of errors we return? The best they can do is look at the error string which is extremely error prone and horrible to write. [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/error-types#what-we-should-do) What we should do --------------------------------------------------------------------------------------------------------------------------------- With TDD we have the benefit of getting into the mindset of: > How would _I_ want to use this code? What we could do for `DumbGetter` is provide a way for users to use the type system to understand what kind of error has happened. What if `DumbGetter` could return us something like Rather than a magical string, we have actual _data_ to work with. Let's change our existing test to reflect this need We'll have to make `BadStatusError` implement the error interface. ### [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/error-types#what-does-the-test-do) What does the test do? Instead of checking the exact string of the error, we are doing a [type assertion](https://tour.golang.org/methods/15) on the error to see if it is a `BadStatusError`. This reflects our desire for the _kind_ of error clearer. Assuming the assertion passes we can then check the properties of the error are correct. When we run the test, it tells us we didn't return the right kind of error Let's fix `DumbGetter` by updating our error handling code to use our type This change has had some _real positive effects_ * Our `DumbGetter` function has become simpler, it's no longer concerned with the intricacies of an error string, it just creates a `BadStatusError`. * Our tests now reflect (and document) what a user of our code _could_ do if they decided they wanted to do some more sophisticated error handling than just logging. Just do a type assertion and then you get easy access to the properties of the error. * It is still "just" an `error`, so if they choose to they can pass it up the call stack or log it like any other `error`. [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/error-types#wrapping-up) Wrapping up --------------------------------------------------------------------------------------------------------------------- If you find yourself testing for multiple error conditions don't fall in to the trap of comparing the error messages. This leads to flaky and difficult to read/write tests and it reflects the difficulties the users of your code will have if they also need to start doing things differently depending on the kind of errors that have occurred. Always make sure your tests reflect how _you'd_ like to use your code, so in this respect consider creating error types to encapsulate your kinds of errors. This makes handling different kinds of errors easier for users of your code and also makes writing your error handling code simpler and easier to read. [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/error-types#addendum) Addendum --------------------------------------------------------------------------------------------------------------- As of Go 1.13 there are new ways to work with errors in the standard library which is covered in the [Go Blog](https://blog.golang.org/go1.13-errors) In this case we are using [`errors.As`](https://golang.org/pkg/errors/#example_As) to try and extract our error into our custom type. It returns a `bool` to denote success and extracts it into `got` for us. [PreviousOS Exec](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/os-exec) [NextContext-aware Reader](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader) Last updated 6 years ago * [Problems with this way of testing](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/error-types#problems-with-this-way-of-testing) * [What we should do](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/error-types#what-we-should-do) * [What does the test do?](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/error-types#what-does-the-test-do) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/error-types#wrapping-up) * [Addendum](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/error-types#addendum) Copy t.Run("when you don't get a 200 you get a status error", func(t *testing.T) { svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { res.WriteHeader(http.StatusTeapot) })) defer svr.Close() _, err := DumbGetter(svr.URL) if err == nil { t.Fatal("expected an error") } want := fmt.Sprintf("did not get 200 from %s, got %d", svr.URL, http.StatusTeapot) got := err.Error() if got != want { t.Errorf(`got "%v", want "%v"`, got, want) } }) Copy type BadStatusError struct { URL string Status int } Copy t.Run("when you don't get a 200 you get a status error", func(t *testing.T) { svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { res.WriteHeader(http.StatusTeapot) })) defer svr.Close() _, err := DumbGetter(svr.URL) if err == nil { t.Fatal("expected an error") } got, isStatusErr := err.(BadStatusError) if !isStatusErr { t.Fatalf("was not a BadStatusError, got %T", err) } want := BadStatusError{URL: svr.URL, Status: http.StatusTeapot} if got != want { t.Errorf("got %v, want %v", got, want) } }) Copy func (b BadStatusError) Error() string { return fmt.Sprintf("did not get 200 from %s, got %d", b.URL, b.Status) } Copy --- FAIL: TestDumbGetter (0.00s) --- FAIL: TestDumbGetter/when_you_dont_get_a_200_you_get_a_status_error (0.00s) error-types_test.go:56: was not a BadStatusError, got *errors.errorString Copy if res.StatusCode != http.StatusOK { return "", BadStatusError{URL: url, Status: res.StatusCode} } Copy t.Run("when you don't get a 200 you get a status error", func(t *testing.T) { svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { res.WriteHeader(http.StatusTeapot) })) defer svr.Close() _, err := DumbGetter(svr.URL) if err == nil { t.Fatal("expected an error") } var got BadStatusError isBadStatusError := errors.As(err, &got) want := BadStatusError{URL: svr.URL, Status: http.StatusTeapot} if !isBadStatusError { t.Fatalf("was not a BadStatusError, got %T", err) } if got != want { t.Errorf("got %v, want %v", got, want) } }) --- # Revisiting HTTP Handlers | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/http-handlers-revisited.md) . [**You can find all the code here**](https://github.com/quii/learn-go-with-tests/tree/master/q-and-a/http-handlers-revisited) This book already has a chapter on [testing a HTTP handler](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server) but this will feature a broader discussion on designing them, so they are simple to test. We'll take a look at a real example and how we can improve how it's designed by applying principles such as single responsibility principle and separation of concerns. These principles can be realised by using [interfaces](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/structs-methods-and-interfaces) and [dependency injection](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection) . By doing this we'll show how testing handlers is actually quite trivial. ![](https://quii.gitbook.io/learn-go-with-tests/~gitbook/image?url=https%3A%2F%2F4221162866-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-legacy-files%2Fo%2Fassets%252F-L9Tqx5WSaiE4u24Pk05%252Fsync%252F5ef857a36750ad6bd4f866770ccc2252fd173c66.png%3Fgeneration%3D1588855072077281%26alt%3Dmedia&width=768&dpr=3&quality=100&sign=3bb3de6f&sv=2) Common question in Go community illustrated Testing HTTP handlers seems to be a recurring question in the Go community, and I think it points to a wider problem of people misunderstanding how to design them. So often people's difficulties with testing stems from the design of their code rather than the actual writing of tests. As I stress so often in this book: > If your tests are causing you pain, listen to that signal and think about the design of your code. [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/http-handlers-revisited#an-example) An example ------------------------------------------------------------------------------------------------------------------------------- [Santosh Kumar tweeted me](https://twitter.com/sntshk/status/1255559003339284481) > How do I test a http handler which has mongodb dependency? Here is the code Let's just list all the things this one function has to do: 1. Write HTTP responses, send headers, status codes, etc. 2. Decode the request's body into a `User` 3. Connect to a database (and all the details around that) 4. Query the database and applying some business logic depending on the result 5. Generate a password 6. Insert a record This is too much. [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/http-handlers-revisited#what-is-a-http-handler-and-what-should-it-do) What is a HTTP Handler and what should it do ? ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Forgetting specific Go details for a moment, no matter what language I've worked in what has always served me well is thinking about the [separation of concerns](https://en.wikipedia.org/wiki/Separation_of_concerns) and the [single responsibility principle](https://en.wikipedia.org/wiki/Single-responsibility_principle) . This can be quite tricky to apply depending on the problem you're solving. What exactly _is_ a responsibility? The lines can blur depending on how abstractly you're thinking and sometimes your first guess might not be right. Thankfully with HTTP handlers I feel like I have a pretty good idea what they should do, no matter what project I've worked on: 1. Accept a HTTP request, parse and validate it. 2. Call some `ServiceThing` to do `ImportantBusinessLogic` with the data I got from step 1. 3. Send an appropriate `HTTP` response depending on what `ServiceThing` returns. I'm not saying every HTTP handler _ever_ should have roughly this shape, but 99 times out of 100 that seems to be the case for me. When you separate these concerns: * Testing handlers becomes a breeze and is focused a small number of concerns. * Importantly testing `ImportantBusinessLogic` no longer has to concern itself with `HTTP`, you can test the business logic cleanly. * You can use `ImportantBusinessLogic` in other contexts without having to modify it. * If `ImportantBusinessLogic` changes what it does, so long as the interface remains the same you don't have to change your handlers. [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/http-handlers-revisited#gos-handlers) Go's Handlers ------------------------------------------------------------------------------------------------------------------------------------ [`http.HandlerFunc`](https://golang.org/pkg/net/http/#HandlerFunc) > The HandlerFunc type is an adapter to allow the use of ordinary functions as HTTP handlers. `type HandlerFunc func(ResponseWriter, *Request)` Reader, take a breath and look at the code above. What do you notice? **It is a function that takes some arguments** There's no framework magic, no annotations, no magic beans, nothing. It's just a function, _and we know how to test functions_. It fits in nicely with the commentary above: * It takes a [`http.Request`](https://golang.org/pkg/net/http/#Request) which is just a bundle of data for us to inspect, parse and validate. * > [A `http.ResponseWriter` interface is used by an HTTP handler to construct an HTTP response.](https://golang.org/pkg/net/http/#ResponseWriter) ### [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/http-handlers-revisited#super-basic-example-test) Super basic example test To test our function, we _call_ it. For our test we pass a `httptest.ResponseRecorder` as our `http.ResponseWriter` argument, and our function will use it to write the `HTTP` response. The recorder will record (or _spy_ on) what was sent, and then we can make our assertions. [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/http-handlers-revisited#calling-a-servicething-in-our-handler) Calling a `ServiceThing` in our handler --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- A common complaint about TDD tutorials is that they're always "too simple" and not "real world enough". My answer to that is: > Wouldn't it be nice if all your code was simple to read and test like the examples you mention? This is one of the biggest challenges we face but need to keep striving for. It _is possible_ (although not necessarily easy) to design code, so it can be simple to read and test if we practice and apply good software engineering principles. Recapping what the handler from earlier does: 1. Write HTTP responses, send headers, status codes, etc. 2. Decode the request's body into a `User` 3. Connect to a database (and all the details around that) 4. Query the database and applying some business logic depending on the result 5. Generate a password 6. Insert a record Taking the idea of a more ideal separation of concerns I'd want it to be more like: 1. Decode the request's body into a `User` 2. Call a `UserService.Register(user)` (this is our `ServiceThing`) 3. If there's an error act on it (the example always sends a `400 BadRequest` which I don't think is right, I'll just have a catch-all handler of a `500 Internal Server Error` _for now_. I must stress that returning `500` for all errors makes for a terrible API! Later on we can make the error handling more sophisticated, perhaps with [error types](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/error-types) . 4. If there's no error, `201 Created` with the ID as the response body (again for terseness/laziness) For the sake of brevity I won't go over the usual TDD process, check all the other chapters for examples. ### [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/http-handlers-revisited#new-design) New design Our `RegisterUser` method matches the shape of `http.HandlerFunc` so we're good to go. We've attached it as a method on a new type `UserServer` which contains a dependency on a `UserService` which is captured as an interface. Interfaces are a fantastic way to ensure our `HTTP` concerns are decoupled from any specific implementation; we can just call the method on the dependency, and we don't have to care _how_ a user gets registered. If you wish to explore this approach in more detail following TDD read the [Dependency Injection](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/dependency-injection) chapter and the [HTTP Server chapter of the "Build an application" section](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server) . Now that we've decoupled ourselves from any specific implementation detail around registration writing the code for our handler is straightforward and follows the responsibilities described earlier. ### [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/http-handlers-revisited#the-tests) The tests! This simplicity is reflected in our tests. Now our handler isn't coupled to a specific implementation of storage it is trivial for us to write a `MockUserService` to help us write simple, fast unit tests to exercise the specific responsibilities it has. ### [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/http-handlers-revisited#what-about-the-database-code-youre-cheating) What about the database code? You're cheating! This is all very deliberate. We don't want HTTP handlers concerned with our business logic, databases, connections, etc. By doing this we have liberated the handler from messy details, we've _also_ made it easier to test our persistence layer and business logic as it is also no longer coupled to irrelevant HTTP details. All we need to do is now implement our `UserService` using whatever database we want to use We can test this separately and once we're happy in `main` we can snap these two units together for our working application. ### [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/http-handlers-revisited#a-more-robust-and-extensible-design-with-little-effort) A more robust and extensible design with little effort These principles not only make our lives easier in the short-term they make the system easier to extend in the future. It wouldn't be surprising that further iterations of this system we'd want to email the user a confirmation of registration. With the old design we'd have to change the handler _and_ the surrounding tests. This is often how parts of code become unmaintainable, more and more functionality creeps in because it's already _designed_ that way; for the "HTTP handler" to handle... everything! By separating concerns using an interface we don't have to edit the handler _at all_ because it's not concerned with the business logic around registration. [](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/http-handlers-revisited#wrapping-up) Wrapping up --------------------------------------------------------------------------------------------------------------------------------- Testing Go's HTTP handlers is not challenging, but designing good software can be! People make the mistake of thinking HTTP handlers are special and throw out good software engineering practices when writing them which then makes testing them challenging. Reiterating again; **Go's http handlers are just functions**. If you write them like you would other functions, with clear responsibilities, and a good separation of concerns you will have no trouble testing them, and your codebase will be healthier for it. [PreviousContext-aware Reader](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/context-aware-reader) [NextWhy unit tests and how to make them work for you](https://quii.gitbook.io/learn-go-with-tests/master/meta/why) Last updated 6 years ago * [An example](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/http-handlers-revisited#an-example) * [What is a HTTP Handler and what should it do ?](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/http-handlers-revisited#what-is-a-http-handler-and-what-should-it-do) * [Go's Handlers](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/http-handlers-revisited#gos-handlers) * [Super basic example test](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/http-handlers-revisited#super-basic-example-test) * [Calling a ServiceThing in our handler](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/http-handlers-revisited#calling-a-servicething-in-our-handler) * [New design](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/http-handlers-revisited#new-design) * [The tests!](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/http-handlers-revisited#the-tests) * [What about the database code? You're cheating!](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/http-handlers-revisited#what-about-the-database-code-youre-cheating) * [A more robust and extensible design with little effort](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/http-handlers-revisited#a-more-robust-and-extensible-design-with-little-effort) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/http-handlers-revisited#wrapping-up) Copy func Registration(w http.ResponseWriter, r *http.Request) { var res model.ResponseResult var user model.User w.Header().Set("Content-Type", "application/json") jsonDecoder := json.NewDecoder(r.Body) jsonDecoder.DisallowUnknownFields() defer r.Body.Close() // check if there is proper json body or error if err := jsonDecoder.Decode(&user); err != nil { res.Error = err.Error() // return 400 status codes w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(res) return } // Connect to mongodb client, _ := mongo.NewClient(options.Client().ApplyURI("mongodb://127.0.0.1:27017")) ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) err := client.Connect(ctx) if err != nil { panic(err) } defer client.Disconnect(ctx) // Check if username already exists in users datastore, if so, 400 // else insert user right away collection := client.Database("test").Collection("users") filter := bson.D{{"username", user.Username}} var foundUser model.User err = collection.FindOne(context.TODO(), filter).Decode(&foundUser) if foundUser.Username == user.Username { res.Error = UserExists // return 400 status codes w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(res) return } pass, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) if err != nil { res.Error = err.Error() // return 400 status codes w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(res) return } user.Password = string(pass) insertResult, err := collection.InsertOne(context.TODO(), user) if err != nil { res.Error = err.Error() // return 400 status codes w.WriteHeader(http.StatusBadRequest) json.NewEncoder(w).Encode(res) return } // return 200 w.WriteHeader(http.StatusOK) res.Result = fmt.Sprintf("%s: %s", UserCreated, insertResult.InsertedID) json.NewEncoder(w).Encode(res) return } Copy func Teapot(res http.ResponseWriter, req *http.Request) { res.WriteHeader(http.StatusTeapot) } func TestTeapotHandler(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/", nil) res := httptest.NewRecorder() Teapot(res, req) if res.Code != http.StatusTeapot { t.Errorf("got status %d but wanted %d", res.Code, http.StatusTeapot) } } Copy type UserService interface { Register(user User) (insertedID string, err error) } type UserServer struct { service UserService } func NewUserServer(service UserService) *UserServer { return &UserServer{service: service} } func (u *UserServer) RegisterUser(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() // request parsing and validation var newUser User err := json.NewDecoder(r.Body).Decode(&newUser) if err != nil { http.Error(w, fmt.Sprintf("could not decode user payload: %v", err), http.StatusBadRequest) return } // call a service thing to take care of the hard work insertedID, err := u.service.Register(newUser) // depending on what we get back, respond accordingly if err != nil { //todo: handle different kinds of errors differently http.Error(w, fmt.Sprintf("problem registering new user: %v", err), http.StatusInternalServerError) return } w.WriteHeader(http.StatusCreated) fmt.Fprint(w, insertedID) } Copy type MockUserService struct { RegisterFunc func(user User) (string, error) UsersRegistered []User } func (m *MockUserService) Register(user User) (insertedID string, err error) { m.UsersRegistered = append(m.UsersRegistered, user) return m.RegisterFunc(user) } func TestRegisterUser(t *testing.T) { t.Run("can register valid users", func(t *testing.T) { user := User{Name: "CJ"} expectedInsertedID := "whatever" service := &MockUserService{ RegisterFunc: func(user User) (string, error) { return expectedInsertedID, nil }, } server := NewUserServer(service) req := httptest.NewRequest(http.MethodGet, "/", userToJSON(user)) res := httptest.NewRecorder() server.RegisterUser(res, req) assertStatus(t, res.Code, http.StatusCreated) if res.Body.String() != expectedInsertedID { t.Errorf("expected body of %q but got %q", res.Body.String(), expectedInsertedID) } if len(service.UsersRegistered) != 1 { t.Fatalf("expected 1 user added but got %d", len(service.UsersRegistered)) } if !reflect.DeepEqual(service.UsersRegistered[0], user) { t.Errorf("the user registered %+v was not what was expected %+v", service.UsersRegistered[0], user) } }) t.Run("returns 400 bad request if body is not valid user JSON", func(t *testing.T) { server := NewUserServer(nil) req := httptest.NewRequest(http.MethodGet, "/", strings.NewReader("trouble will find me")) res := httptest.NewRecorder() server.RegisterUser(res, req) assertStatus(t, res.Code, http.StatusBadRequest) }) t.Run("returns a 500 internal server error if the service fails", func(t *testing.T) { user := User{Name: "CJ"} service := &MockUserService{ RegisterFunc: func(user User) (string, error) { return "", errors.New("couldn't add new user") }, } server := NewUserServer(service) req := httptest.NewRequest(http.MethodGet, "/", userToJSON(user)) res := httptest.NewRecorder() server.RegisterUser(res, req) assertStatus(t, res.Code, http.StatusInternalServerError) }) } Copy type MongoUserService struct { } func NewMongoUserService() *MongoUserService { //todo: pass in DB URL as argument to this function //todo: connect to db, create a connection pool return &MongoUserService{} } func (m MongoUserService) Register(user User) (insertedID string, err error) { // use m.mongoConnection to perform queries panic("implement me") } Copy func main() { mongoService := NewMongoUserService() server := NewUserServer(mongoService) http.ListenAndServe(":8000", http.HandlerFunc(server.RegisterUser)) } --- # Reflection | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection.md) . [**You can find all the code for this chapter here**](https://github.com/quii/learn-go-with-tests/tree/master/reflection) [From Twitter](https://twitter.com/peterbourgon/status/1011403901419937792?s=09) > golang challenge: write a function `walk(x interface{}, fn func(string))` which takes a struct `x` and calls `fn` for all strings fields found inside. difficulty level: recursively. To do this we will need to use _reflection_. > Reflection in computing is the ability of a program to examine its own structure, particularly through types; it's a form of metaprogramming. It's also a great source of confusion. From [The Go Blog: Reflection](https://blog.golang.org/laws-of-reflection) [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#what-is-interface) What is `interface`? ----------------------------------------------------------------------------------------------------------------------------- We have enjoyed the type-safety that Go has offered us in terms of functions that work with known types, such as `string`, `int` and our own types like `BankAccount`. This means that we get some documentation for free and the compiler will complain if you try and pass the wrong type to a function. You may come across scenarios though where you want to write a function where you don't know the type at compile time. Go lets us get around this with the type `interface{}` which you can think of as just _any_ type. So `walk(x interface{}, fn func(string))` will accept any value for `x`. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#so-why-not-use-interface-for-everything-and-have-really-flexible-functions) So why not use `interface` for everything and have really flexible functions? * As a user of a function that takes `interface` you lose type safety. What if you meant to pass `Foo.bar` of type `string` into a function but instead did `Foo.baz` which is an `int`? The compiler won't be able to inform you of your mistake. You also have no idea _what_ you're allowed to pass to a function. Knowing that a function takes a `UserService` for instance is very useful. * As a writer of such a function, you have to be able to inspect _anything_ that has been passed to you and try and figure out what the type is and what you can do with it. This is done using _reflection_. This can be quite clumsy and difficult to read and is generally less performant (as you have to do checks at runtime). In short only use reflection if you really need to. If you want polymorphic functions, consider if you could design it around an interface (not `interface`, confusingly) so that users can use your function with multiple types if they implement whatever methods you need for your function to work. Our function will need to be able to work with lots of different things. As always we'll take an iterative approach, writing tests for each new thing we want to support and refactoring along the way until we're done. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-test-first) Write the test first -------------------------------------------------------------------------------------------------------------------------------- We'll want to call our function with a struct that has a string field in it (`x`). Then we can spy on the function (`fn`) passed in to see if it is called. * We want to store a slice of strings (`got`) which stores which strings were passed into `fn` by `walk`. Often in previous chapters, we have made dedicated types for this to spy on function/method invocations but in this case, we can just pass in an anonymous function for `fn` that closes over `got`. * We use an anonymous `struct` with a `Name` field of type string to go for the simplest "happy" path. * Finally, call `walk` with `x` and the spy and for now just check the length of `got`, we'll be more specific with our assertions once we've got something very basic working. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#try-to-run-the-test) Try to run the test ------------------------------------------------------------------------------------------------------------------------------ [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- We need to define `walk` Try and run the test again [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-enough-code-to-make-it-pass) Write enough code to make it pass ---------------------------------------------------------------------------------------------------------------------------------------------------------- We can call the spy with any string to make this pass. The test should now be passing. The next thing we'll need to do is make a more specific assertion on what our `fn` is being called with. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-test-first-1) Write the test first ---------------------------------------------------------------------------------------------------------------------------------- Add the following to the existing test to check the string passed to `fn` is correct [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#try-to-run-the-test-1) Try to run the test -------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-enough-code-to-make-it-pass-1) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------ This code is _very unsafe and very naive_, but remember: our goal when we are in "red" (the tests failing) is to write the smallest amount of code possible. We then write more tests to address our concerns. We need to use reflection to have a look at `x` and try and look at its properties. The [reflect package](https://godoc.org/reflect) has a function `ValueOf` which returns us a `Value` of a given variable. This has ways for us to inspect a value, including its fields which we use on the next line. We then make some very optimistic assumptions about the value passed in * We look at the first and only field, there may be no fields at all which would cause a panic * We then call `String()` which returns the underlying value as a string but we know it would be wrong if the field was something other than a string. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#refactor) Refactor -------------------------------------------------------------------------------------------------------- Our code is passing for the simple case but we know our code has a lot of shortcomings. We're going to be writing a number of tests where we pass in different values and checking the array of strings that `fn` was called with. We should refactor our test into a table based test to make this easier to continue testing new scenarios. Now we can easily add a scenario to see what happens if we have more than one string field. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-test-first-2) Write the test first ---------------------------------------------------------------------------------------------------------------------------------- Add the following scenario to the `cases`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#try-to-run-the-test-2) Try to run the test -------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-enough-code-to-make-it-pass-2) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------ `val` has a method `NumField` which returns the number of fields in the value. This lets us iterate over the fields and call `fn` which passes our test. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#refactor-1) Refactor ---------------------------------------------------------------------------------------------------------- It doesn't look like there's any obvious refactors here that would improve the code so let's press on. The next shortcoming in `walk` is that it assumes every field is a `string`. Let's write a test for this scenario. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-test-first-3) Write the test first ---------------------------------------------------------------------------------------------------------------------------------- Add the following case [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#try-to-run-the-test-3) Try to run the test -------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-enough-code-to-make-it-pass-3) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------ We need to check that the type of the field is a `string`. We can do that by checking its [`Kind`](https://godoc.org/reflect#Kind) . [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#refactor-2) Refactor ---------------------------------------------------------------------------------------------------------- Again it looks like the code is reasonable enough for now. The next scenario is what if it isn't a "flat" `struct`? In other words, what happens if we have a `struct` with some nested fields? [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-test-first-4) Write the test first ---------------------------------------------------------------------------------------------------------------------------------- We have been using the anonymous struct syntax to declare types ad-hocly for our tests so we could continue to do that like so But we can see that when you get inner anonymous structs the syntax gets a little messy. [There is a proposal to make it so the syntax would be nicer](https://github.com/golang/go/issues/12854) . Let's just refactor this by making a known type for this scenario and reference it in the test. There is a little indirection in that some of the code for our test is outside the test but readers should be able to infer the structure of the `struct` by looking at the initialisation. Add the following type declarations somewhere in your test file Now we can add this to our cases which reads a lot clearer than before [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#try-to-run-the-test-4) Try to run the test -------------------------------------------------------------------------------------------------------------------------------- The problem is we're only iterating on the fields on the first level of the type's hierarchy. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-enough-code-to-make-it-pass-4) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------ The solution is quite simple, we again inspect its `Kind` and if it happens to be a `struct` we just call `walk` again on that inner `struct`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#refactor-3) Refactor ---------------------------------------------------------------------------------------------------------- When you're doing a comparison on the same value more than once _generally_ refactoring into a `switch` will improve readability and make your code easier to extend. What if the value of the struct passed in is a pointer? [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-test-first-5) Write the test first ---------------------------------------------------------------------------------------------------------------------------------- Add this case [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#try-to-run-the-test-5) Try to run the test -------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-enough-code-to-make-it-pass-5) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------ You can't use `NumField` on a pointer `Value`, we need to extract the underlying value before we can do that by using `Elem()`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#refactor-4) Refactor ---------------------------------------------------------------------------------------------------------- Let's encapsulate the responsibility of extracting the `reflect.Value` from a given `interface{}` into a function. This actually adds _more_ code but I feel the abstraction level is right. * Get the `reflect.Value` of `x` so I can inspect it, I don't care how. * Iterate over the fields, doing whatever needs to be done depending on its type. Next, we need to cover slices. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-test-first-6) Write the test first ---------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#try-to-run-the-test-6) Try to run the test -------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) Write the minimal amount of code for the test to run and check the failing test output ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- This is similar to the pointer scenario before, we are trying to call `NumField` on our `reflect.Value` but it doesn't have one as it's not a struct. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-enough-code-to-make-it-pass-6) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------ [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#refactor-5) Refactor ---------------------------------------------------------------------------------------------------------- This works but it's yucky. No worries, we have working code backed by tests so we are free to tinker all we like. If you think a little abstractly, we want to call `walk` on either * Each field in a struct * Each _thing_ in a slice Our code at the moment does this but doesn't reflect it very well. We just have a check at the start to see if it's a slice (with a `return` to stop the rest of the code executing) and if it's not we just assume it's a struct. Let's rework the code so instead we check the type _first_ and then do our work. Looking much better! If it's a struct or a slice we iterate over its values calling `walk` on each one. Otherwise, if it's a `reflect.String` we can call `fn`. Still, to me it feels like it could be better. There's repetition of the operation of iterating over fields/values and then calling `walk` but conceptually they're the same. If the `value` is a `reflect.String` then we just call `fn` like normal. Otherwise, our `switch` will extract out two things depending on the type * How many fields there are * How to extract the `Value` (`Field` or `Index`) Once we've determined those things we can iterate through `numberOfValues` calling `walk` with the result of the `getField` function. Now we've done this, handling arrays should be trivial. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-test-first-7) Write the test first ---------------------------------------------------------------------------------------------------------------------------------- Add to the cases [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#try-to-run-the-test-7) Try to run the test -------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-enough-code-to-make-it-pass-7) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------ Arrays can be handled the same way as slices, so just add it to the case with a comma The next type we want to handle is `map`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-test-first-8) Write the test first ---------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#try-to-run-the-test-8) Try to run the test -------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-enough-code-to-make-it-pass-8) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------ Again if you think a little abstractly you can see that `map` is very similar to `struct`, it's just the keys are unknown at compile time. However, by design you cannot get values out of a map by index. It's only done by _key_, so that breaks our abstraction, darn. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#refactor-6) Refactor ---------------------------------------------------------------------------------------------------------- How do you feel right now? It felt like maybe a nice abstraction at the time but now the code feels a little wonky. _This is OK!_ Refactoring is a journey and sometimes we will make mistakes. A major point of TDD is it gives us the freedom to try these things out. By taking small steps backed by tests this is in no way an irreversible situation. Let's just put it back to how it was before the refactor. We've introduced `walkValue` which DRYs up the calls to `walk` inside our `switch` so that they only have to extract out the `reflect.Value`s from `val`. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#one-final-problem) One final problem Remember that maps in Go do not guarantee order. So your tests will sometimes fail because we assert that the calls to `fn` are done in a particular order. To fix this, we'll need to move our assertion with the maps to a new test where we do not care about the order. Here is how `assertContains` is defined The next type we want to handle is `chan`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-test-first-9) Write the test first ---------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#try-to-run-the-test-9) Try to run the test -------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-enough-code-to-make-it-pass-9) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------ We can iterate through all values sent through channel until it was closed with Recv() The next type we want to handle is `func`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-test-first-10) Write the test first ----------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#try-to-run-the-test-10) Try to run the test --------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-enough-code-to-make-it-pass-10) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------- Non zero-argument functions do not seem to make a lot of sense in this scenario. But we should allow for arbitrary return values. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#wrapping-up) Wrapping up -------------------------------------------------------------------------------------------------------------- * Introduced some of the concepts from the `reflect` package. * Used recursion to traverse arbitrary data structures. * Did an in retrospect bad refactor but didn't get too upset about it. By working iteratively with tests it's not such a big deal. * This only covered a small aspect of reflection. [The Go blog has an excellent post covering more details](https://blog.golang.org/laws-of-reflection) . * Now that you know about reflection, do your best to avoid using it. [PreviousSelect](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/select) [NextSync](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/sync) Last updated 5 years ago * [What is interface?](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#what-is-interface) * [So why not use interface for everything and have really flexible functions?](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#so-why-not-use-interface-for-everything-and-have-really-flexible-functions) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-test-first) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#try-to-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-enough-code-to-make-it-pass) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-test-first-1) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#try-to-run-the-test-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-enough-code-to-make-it-pass-1) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#refactor) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-test-first-2) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#try-to-run-the-test-2) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-enough-code-to-make-it-pass-2) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#refactor-1) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-test-first-3) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#try-to-run-the-test-3) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-enough-code-to-make-it-pass-3) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#refactor-2) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-test-first-4) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#try-to-run-the-test-4) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-enough-code-to-make-it-pass-4) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#refactor-3) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-test-first-5) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#try-to-run-the-test-5) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-enough-code-to-make-it-pass-5) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#refactor-4) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-test-first-6) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#try-to-run-the-test-6) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-enough-code-to-make-it-pass-6) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#refactor-5) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-test-first-7) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#try-to-run-the-test-7) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-enough-code-to-make-it-pass-7) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-test-first-8) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#try-to-run-the-test-8) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-enough-code-to-make-it-pass-8) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#refactor-6) * [One final problem](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#one-final-problem) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-test-first-9) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#try-to-run-the-test-9) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-enough-code-to-make-it-pass-9) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-the-test-first-10) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#try-to-run-the-test-10) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#write-enough-code-to-make-it-pass-10) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/reflection#wrapping-up) Copy func TestWalk(t *testing.T) { expected := "Chris" var got []string x := struct { Name string }{expected} walk(x, func(input string) { got = append(got, input) }) if len(got) != 1 { t.Errorf("wrong number of function calls, got %d want %d", len(got), 1) } } Copy ./reflection_test.go:21:2: undefined: walk Copy func walk(x interface{}, fn func(input string)) { } Copy === RUN TestWalk --- FAIL: TestWalk (0.00s) reflection_test.go:19: wrong number of function calls, got 0 want 1 FAIL Copy func walk(x interface{}, fn func(input string)) { fn("I still can't believe South Korea beat Germany 2-0 to put them last in their group") } Copy if got[0] != expected { t.Errorf("got %q, want %q", got[0], expected) } Copy === RUN TestWalk --- FAIL: TestWalk (0.00s) reflection_test.go:23: got 'I still can't believe South Korea beat Germany 2-0 to put them last in their group', want 'Chris' FAIL Copy func walk(x interface{}, fn func(input string)) { val := reflect.ValueOf(x) field := val.Field(0) fn(field.String()) } Copy func TestWalk(t *testing.T) { cases := []struct{ Name string Input interface{} ExpectedCalls []string } { { "Struct with one string field", struct { Name string }{ "Chris"}, []string{"Chris"}, }, } for _, test := range cases { t.Run(test.Name, func(t *testing.T) { var got []string walk(test.Input, func(input string) { got = append(got, input) }) if !reflect.DeepEqual(got, test.ExpectedCalls) { t.Errorf("got %v, want %v", got, test.ExpectedCalls) } }) } } Copy { "Struct with two string fields", struct { Name string City string }{"Chris", "London"}, []string{"Chris", "London"}, } Copy === RUN TestWalk/Struct_with_two_string_fields --- FAIL: TestWalk/Struct_with_two_string_fields (0.00s) reflection_test.go:40: got [Chris], want [Chris London] Copy func walk(x interface{}, fn func(input string)) { val := reflect.ValueOf(x) for i:=0; i], want [Chris] Copy func walk(x interface{}, fn func(input string)) { val := reflect.ValueOf(x) for i := 0; i < val.NumField(); i++ { field := val.Field(i) if field.Kind() == reflect.String { fn(field.String()) } } } Copy { "Nested fields", struct { Name string Profile struct { Age int City string } }{"Chris", struct { Age int City string }{33, "London"}}, []string{"Chris", "London"}, }, Copy type Person struct { Name string Profile Profile } type Profile struct { Age int City string } Copy { "Nested fields", Person{ "Chris", Profile{33, "London"}, }, []string{"Chris", "London"}, }, Copy === RUN TestWalk/Nested_fields --- FAIL: TestWalk/Nested_fields (0.00s) reflection_test.go:54: got [Chris], want [Chris London] Copy func walk(x interface{}, fn func(input string)) { val := reflect.ValueOf(x) for i := 0; i < val.NumField(); i++ { field := val.Field(i) if field.Kind() == reflect.String { fn(field.String()) } if field.Kind() == reflect.Struct { walk(field.Interface(), fn) } } } Copy func walk(x interface{}, fn func(input string)) { val := reflect.ValueOf(x) for i := 0; i < val.NumField(); i++ { field := val.Field(i) switch field.Kind() { case reflect.String: fn(field.String()) case reflect.Struct: walk(field.Interface(), fn) } } } Copy { "Pointers to things", &Person{ "Chris", Profile{33, "London"}, }, []string{"Chris", "London"}, }, Copy === RUN TestWalk/Pointers_to_things panic: reflect: call of reflect.Value.NumField on ptr Value [recovered] panic: reflect: call of reflect.Value.NumField on ptr Value Copy func walk(x interface{}, fn func(input string)) { val := reflect.ValueOf(x) if val.Kind() == reflect.Ptr { val = val.Elem() } for i := 0; i < val.NumField(); i++ { field := val.Field(i) switch field.Kind() { case reflect.String: fn(field.String()) case reflect.Struct: walk(field.Interface(), fn) } } } Copy func walk(x interface{}, fn func(input string)) { val := getValue(x) for i := 0; i < val.NumField(); i++ { field := val.Field(i) switch field.Kind() { case reflect.String: fn(field.String()) case reflect.Struct: walk(field.Interface(), fn) } } } func getValue(x interface{}) reflect.Value { val := reflect.ValueOf(x) if val.Kind() == reflect.Ptr { val = val.Elem() } return val } Copy { "Slices", []Profile { {33, "London"}, {34, "Reykjavík"}, }, []string{"London", "Reykjavík"}, }, Copy === RUN TestWalk/Slices panic: reflect: call of reflect.Value.NumField on slice Value [recovered] panic: reflect: call of reflect.Value.NumField on slice Value Copy func walk(x interface{}, fn func(input string)) { val := getValue(x) if val.Kind() == reflect.Slice { for i:=0; i< val.Len(); i++ { walk(val.Index(i).Interface(), fn) } return } for i := 0; i < val.NumField(); i++ { field := val.Field(i) switch field.Kind() { case reflect.String: fn(field.String()) case reflect.Struct: walk(field.Interface(), fn) } } } Copy func walk(x interface{}, fn func(input string)) { val := getValue(x) switch val.Kind() { case reflect.Struct: for i:=0; i Package bufio implements buffered I/O. It wraps an io.Reader or io.Writer object, creating another object (Reader or Writer) that also implements the interface but provides buffering and some help for textual I/O. Update the code to the following The tests will now pass. * `Scanner.Scan()` will read up to a newline. * We then use `Scanner.Text()` to return the `string` the scanner read to. Now that we have some passing tests, we should wire this up into `main`. Remember we should always strive to have fully-integrated working software as quickly as we can. In `main.go` add the following and run it. (you may have to adjust the path of the second dependency to match what's on your computer) You should get an error What's happening here is because we are trying to assign to the fields `playerStore` and `in` in `CLI`. These are unexported (private) fields. We _could_ do this in our test code because our test is in the same package as `CLI` (`poker`). But our `main` is in package `main` so it does not have access. This highlights the importance of _integrating your work_. We rightfully made the dependencies of our `CLI` private (because we don't want them exposed to users of `CLI`s) but haven't made a way for users to construct it. Is there a way to have caught this problem earlier? ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#package-mypackage_test) `package mypackage_test` In all other examples so far, when we make a test file we declare it as being in the same package that we are testing. This is fine and it means on the odd occasion where we want to test something internal to the package we have access to the unexported types. But given we have advocated for _not_ testing internal things _generally_, can Go help enforce that? What if we could test our code where we only have access to the exported types (like our `main` does)? When you're writing a project with multiple packages I would strongly recommend that your test package name has `_test` at the end. When you do this you will only be able to have access to the public types in your package. This would help with this specific case but also helps enforce the discipline of only testing public APIs. If you still wish to test internals you can make a separate test with the package you want to test. An adage with TDD is that if you cannot test your code then it is probably hard for users of your code to integrate with it. Using `package foo_test` will help with this by forcing you to test your code as if you are importing it like users of your package will. Before fixing `main` let's change the package of our test inside `CLI_test.go` to `poker_test`. If you have a well-configured IDE you will suddenly see a lot of red! If you run the compiler you'll get the following errors We have now stumbled into more questions on package design. In order to test our software we made unexported stubs and helper functions which are no longer available for us to use in our `CLI_test` because the helpers are defined in the `_test.go` files in the `poker` package. #### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#do-we-want-to-have-our-stubs-and-helpers-public) Do we want to have our stubs and helpers 'public'? This is a subjective discussion. One could argue that you do not want to pollute your package's API with code to facilitate tests. In the presentation ["Advanced Testing with Go"](https://speakerdeck.com/mitchellh/advanced-testing-with-go?slide=53) by Mitchell Hashimoto, it is described how at HashiCorp they advocate doing this so that users of the package can write tests without having to re-invent the wheel writing stubs. In our case, this would mean anyone using our `poker` package won't have to create their own stub `PlayerStore` if they wish to work with our code. Anecdotally I have used this technique in other shared packages and it has proved extremely useful in terms of users saving time when integrating with our packages. So let's create a file called `testing.go` and add our stub and our helpers. You'll need to make the helpers public (remember exporting is done with a capital letter at the start) if you want them to be exposed to importers of our package. In our `CLI` test you'll need to call the code as if you were using it within a different package. You'll now see we have the same problems as we had in `main` The easiest way to get around this is to make a constructor as we have for other types. We'll also change `CLI` so it stores a `bufio.Scanner` instead of the reader as it's now automatically wrapped at construction time. By doing this, we can then simplify and refactor our reading code Change the test to use the constructor instead and we should be back to the tests passing. Finally, we can go back to our new `main.go` and use the constructor we just made Try and run it, type "Bob wins". ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#refactor-1) Refactor We have some repetition in our respective applications where we are opening a file and creating a `FileSystemStore` from its contents. This feels like a slight weakness in our package's design so we should make a function in it to encapsulate opening a file from a path and returning you the `PlayerStore`. Now refactor both of our applications to use this function to create the store. #### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#cli-application-code) CLI application code #### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#web-server-application-code) Web server application code Notice the symmetry: despite being different user interfaces the setup is almost identical. This feels like good validation of our design so far. And notice also that `FileSystemPlayerStoreFromFile` returns a closing function, so we can close the underlying file once we are done using the Store. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#wrapping-up) Wrapping up --------------------------------------------------------------------------------------------------------------------- ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#package-structure) Package structure This chapter meant we wanted to create two applications, re-using the domain code we've written so far. In order to do this, we needed to update our package structure so that we had separate folders for our respective `main`s. By doing this we ran into integration problems due to unexported values so this further demonstrates the value of working in small "slices" and integrating often. We learned how `mypackage_test` helps us create a testing environment which is the same experience for other packages integrating with your code, to help you catch integration problems and see how easy (or not!) your code is to work with. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#reading-user-input) Reading user input We saw how reading from `os.Stdin` is very easy for us to work with as it implements `io.Reader`. We used `bufio.Scanner` to easily read line by line user input. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#simple-abstractions-leads-to-simpler-code-re-use) Simple abstractions leads to simpler code re-use It was almost no effort to integrate `PlayerStore` into our new application (once we had made the package adjustments) and subsequently testing was very easy too because we decided to expose our stub version too. [PreviousIO and sorting](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io) [NextTime](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time) Last updated 6 years ago * [A reminder of the code](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#a-reminder-of-the-code) * [Some project refactoring first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#some-project-refactoring-first) * [Final checks](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#final-checks) * [Walking skeleton](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#walking-skeleton) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#write-the-test-first) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#try-to-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#write-enough-code-to-make-it-pass) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#write-the-test-first-1) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#try-to-run-the-test-1) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#write-enough-code-to-make-it-pass-1) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#refactor) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#write-the-test-first-2) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#try-to-run-the-test-2) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#write-enough-code-to-make-it-pass-2) * [package mypackage\_test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#package-mypackage_test) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#refactor-1) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#wrapping-up) * [Package structure](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#package-structure) * [Reading user input](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#reading-user-input) * [Simple abstractions leads to simpler code re-use](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line#simple-abstractions-leads-to-simpler-code-re-use) Copy . ├── FileSystemStore.go ├── FileSystemStore_test.go ├── cmd │ └── webserver │ └── main.go ├── league.go ├── server.go ├── server_integration_test.go ├── server_test.go ├── tape.go └── tape_test.go Copy package main import ( "github.com/quii/learn-go-with-tests/command-line/v1" "log" "net/http" "os" ) const dbFileName = "game.db.json" func main() { db, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666) if err != nil { log.Fatalf("problem opening %s %v", dbFileName, err) } store, err := poker.NewFileSystemPlayerStore(db) if err != nil { log.Fatalf("problem creating file system player store, %v ", err) } server := poker.NewPlayerServer(store) if err := http.ListenAndServe(":5000", server); err != nil { log.Fatalf("could not listen on port 5000 %v", err) } } Copy package main import "fmt" func main() { fmt.Println("Let's play poker") } Copy func TestCLI(t *testing.T) { playerStore := &StubPlayerStore{} cli := &CLI{playerStore} cli.PlayPoker() if len(playerStore.winCalls) != 1 { t.Fatal("expected a win call but didn't get any") } } Copy # github.com/quii/learn-go-with-tests/command-line/v2 ./cli_test.go:25:10: undefined: CLI Copy type CLI struct { playerStore PlayerStore } func (cli *CLI) PlayPoker() {} Copy --- FAIL: TestCLI (0.00s) cli_test.go:30: expected a win call but didn't get any FAIL Copy func (cli *CLI) PlayPoker() { cli.playerStore.RecordWin("Cleo") } Copy func TestCLI(t *testing.T) { in := strings.NewReader("Chris wins\n") playerStore := &StubPlayerStore{} cli := &CLI{playerStore, in} cli.PlayPoker() if len(playerStore.winCalls) < 1 { t.Fatal("expected a win call but didn't get any") } got := playerStore.winCalls[0] want := "Chris" if got != want { t.Errorf("didn't record correct winner, got %q, want %q", got, want) } } Copy type CLI struct { playerStore PlayerStore in io.Reader } Copy --- FAIL: TestCLI (0.00s) CLI_test.go:23: didn't record the correct winner, got 'Cleo', want 'Chris' FAIL Copy func (cli *CLI) PlayPoker() { cli.playerStore.RecordWin("Chris") } Copy func assertPlayerWin(t *testing.T, store *StubPlayerStore, winner string) { t.Helper() if len(store.winCalls) != 1 { t.Fatalf("got %d calls to RecordWin want %d", len(store.winCalls), 1) } if store.winCalls[0] != winner { t.Errorf("did not store correct winner got %q want %q", store.winCalls[0], winner) } } Copy func TestCLI(t *testing.T) { in := strings.NewReader("Chris wins\n") playerStore := &StubPlayerStore{} cli := &CLI{playerStore, in} cli.PlayPoker() assertPlayerWin(t, playerStore, "Chris") } Copy func TestCLI(t *testing.T) { t.Run("record chris win from user input", func(t *testing.T) { in := strings.NewReader("Chris wins\n") playerStore := &StubPlayerStore{} cli := &CLI{playerStore, in} cli.PlayPoker() assertPlayerWin(t, playerStore, "Chris") }) t.Run("record cleo win from user input", func(t *testing.T) { in := strings.NewReader("Cleo wins\n") playerStore := &StubPlayerStore{} cli := &CLI{playerStore, in} cli.PlayPoker() assertPlayerWin(t, playerStore, "Cleo") }) } Copy === RUN TestCLI --- FAIL: TestCLI (0.00s) === RUN TestCLI/record_chris_win_from_user_input --- PASS: TestCLI/record_chris_win_from_user_input (0.00s) === RUN TestCLI/record_cleo_win_from_user_input --- FAIL: TestCLI/record_cleo_win_from_user_input (0.00s) CLI_test.go:27: did not store correct winner got 'Chris' want 'Cleo' FAIL Copy type CLI struct { playerStore PlayerStore in io.Reader } func (cli *CLI) PlayPoker() { reader := bufio.NewScanner(cli.in) reader.Scan() cli.playerStore.RecordWin(extractWinner(reader.Text())) } func extractWinner(userInput string) string { return strings.Replace(userInput, " wins", "", 1) } Copy package main import ( "fmt" "github.com/quii/learn-go-with-tests/command-line/v3" "log" "os" ) const dbFileName = "game.db.json" func main() { fmt.Println("Let's play poker") fmt.Println("Type {Name} wins to record a win") db, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666) if err != nil { log.Fatalf("problem opening %s %v", dbFileName, err) } store, err := poker.NewFileSystemPlayerStore(db) if err != nil { log.Fatalf("problem creating file system player store, %v ", err) } game := poker.CLI{store, os.Stdin} game.PlayPoker() } Copy command-line/v3/cmd/cli/main.go:32:25: implicit assignment of unexported field 'playerStore' in poker.CLI literal command-line/v3/cmd/cli/main.go:32:34: implicit assignment of unexported field 'in' in poker.CLI literal Copy ./CLI_test.go:12:19: undefined: StubPlayerStore ./CLI_test.go:17:3: undefined: assertPlayerWin ./CLI_test.go:22:19: undefined: StubPlayerStore ./CLI_test.go:27:3: undefined: assertPlayerWin Copy package poker import "testing" type StubPlayerStore struct { scores map[string]int winCalls []string league []Player } func (s *StubPlayerStore) GetPlayerScore(name string) int { score := s.scores[name] return score } func (s *StubPlayerStore) RecordWin(name string) { s.winCalls = append(s.winCalls, name) } func (s *StubPlayerStore) GetLeague() League { return s.league } func AssertPlayerWin(t *testing.T, store *StubPlayerStore, winner string) { t.Helper() if len(store.winCalls) != 1 { t.Fatalf("got %d calls to RecordWin want %d", len(store.winCalls), 1) } if store.winCalls[0] != winner { t.Errorf("did not store correct winner got %q want %q", store.winCalls[0], winner) } } // todo for you - the rest of the helpers Copy func TestCLI(t *testing.T) { t.Run("record chris win from user input", func(t *testing.T) { in := strings.NewReader("Chris wins\n") playerStore := &poker.StubPlayerStore{} cli := &poker.CLI{playerStore, in} cli.PlayPoker() poker.AssertPlayerWin(t, playerStore, "Chris") }) t.Run("record cleo win from user input", func(t *testing.T) { in := strings.NewReader("Cleo wins\n") playerStore := &poker.StubPlayerStore{} cli := &poker.CLI{playerStore, in} cli.PlayPoker() poker.AssertPlayerWin(t, playerStore, "Cleo") }) } Copy ./CLI_test.go:15:26: implicit assignment of unexported field 'playerStore' in poker.CLI literal ./CLI_test.go:15:39: implicit assignment of unexported field 'in' in poker.CLI literal ./CLI_test.go:25:26: implicit assignment of unexported field 'playerStore' in poker.CLI literal ./CLI_test.go:25:39: implicit assignment of unexported field 'in' in poker.CLI literal Copy type CLI struct { playerStore PlayerStore in *bufio.Scanner } func NewCLI(store PlayerStore, in io.Reader) *CLI { return &CLI{ playerStore: store, in: bufio.NewScanner(in), } } Copy func (cli *CLI) PlayPoker() { userInput := cli.readLine() cli.playerStore.RecordWin(extractWinner(userInput)) } func extractWinner(userInput string) string { return strings.Replace(userInput, " wins", "", 1) } func (cli *CLI) readLine() string { cli.in.Scan() return cli.in.Text() } Copy game := poker.NewCLI(store, os.Stdin) Copy func FileSystemPlayerStoreFromFile(path string) (*FileSystemPlayerStore, func(), error) { db, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0666) if err != nil { return nil, nil, fmt.Errorf("problem opening %s %v", path, err) } closeFunc := func() { db.Close() } store, err := NewFileSystemPlayerStore(db) if err != nil { return nil, nil, fmt.Errorf("problem creating file system player store, %v ", err) } return store, closeFunc, nil } Copy package main import ( "fmt" "github.com/quii/learn-go-with-tests/command-line/v3" "log" "os" ) const dbFileName = "game.db.json" func main() { store, close, err := poker.FileSystemPlayerStoreFromFile(dbFileName) if err != nil { log.Fatal(err) } defer close() fmt.Println("Let's play poker") fmt.Println("Type {Name} wins to record a win") poker.NewCLI(store, os.Stdin).PlayPoker() } Copy package main import ( "github.com/quii/learn-go-with-tests/command-line/v3" "log" "net/http" ) const dbFileName = "game.db.json" func main() { store, close, err := poker.FileSystemPlayerStoreFromFile(dbFileName) if err != nil { log.Fatal(err) } defer close() server := poker.NewPlayerServer(store) if err := http.ListenAndServe(":5000", server); err != nil { log.Fatalf("could not listen on port 5000 %v", err) } } --- # JSON, routing and embedding | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json.md) . [**You can find all the code for this chapter here**](https://github.com/quii/learn-go-with-tests/tree/master/json) [In the previous chapter](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server) we created a web server to store how many games players have won. Our product owner has a new requirement; to have a new endpoint called `/league` which returns a list of all players stored. She would like this to be returned as JSON. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#here-is-the-code-we-have-so-far) Here is the code we have so far ----------------------------------------------------------------------------------------------------------------------------------------------------- Copy // server.go package main import ( "fmt" "net/http" ) type PlayerStore interface { GetPlayerScore(name string) int RecordWin(name string) } type PlayerServer struct { store PlayerStore } func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { player := r.URL.Path[len("/players/"):] switch r.Method { case http.MethodPost: p.processWin(w, player) case http.MethodGet: p.showScore(w, player) } } func (p *PlayerServer) showScore(w http.ResponseWriter, player string) { score := p.store.GetPlayerScore(player) if score == 0 { w.WriteHeader(http.StatusNotFound) } fmt.Fprint(w, score) } func (p *PlayerServer) processWin(w http.ResponseWriter, player string) { p.store.RecordWin(player) w.WriteHeader(http.StatusAccepted) } You can find the corresponding tests in the link at the top of the chapter. We'll start by making the league table endpoint. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#write-the-test-first) Write the test first ------------------------------------------------------------------------------------------------------------------------------- We'll extend the existing suite as we have some useful test functions and a fake `PlayerStore` to use. Before worrying about actual scores and JSON we will try and keep the changes small with the plan to iterate toward our goal. The simplest start is to check we can hit `/league` and get an `OK` back. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#try-to-run-the-test) Try to run the test ----------------------------------------------------------------------------------------------------------------------------- Your `PlayerServer` should be panicking like this. Go to the line of code in the stack trace which is pointing to `server.go`. In the previous chapter, we mentioned this was a fairly naive way of doing our routing. What is happening is it's trying to split the string of the path starting at an index beyond `/league` so it is `slice bounds out of range`. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#write-enough-code-to-make-it-pass) Write enough code to make it pass --------------------------------------------------------------------------------------------------------------------------------------------------------- Go has a built-in routing mechanism called [`ServeMux`](https://golang.org/pkg/net/http/#ServeMux) (request multiplexer) which lets you attach `http.Handler`s to particular request paths. Let's commit some sins and get the tests passing in the quickest way we can, knowing we can refactor it with safety once we know the tests are passing. * When the request starts we create a router and then we tell it for `x` path use `y` handler. * So for our new endpoint, we use `http.HandlerFunc` and an _anonymous function_ to `w.WriteHeader(http.StatusOK)` when `/league` is requested to make our new test pass. * For the `/players/` route we just cut and paste our code into another `http.HandlerFunc`. * Finally, we handle the request that came in by calling our new router's `ServeHTTP` (notice how `ServeMux` is _also_ an `http.Handler`?) The tests should now pass. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#refactor) Refactor ------------------------------------------------------------------------------------------------------- `ServeHTTP` is looking quite big, we can separate things out a bit by refactoring our handlers into separate methods. It's quite odd (and inefficient) to be setting up a router as a request comes in and then calling it. What we ideally want to do is have some kind of `NewPlayerServer` function which will take our dependencies and do the one-time setup of creating the router. Each request can then just use that one instance of the router. * `PlayerServer` now needs to store a router. * We have moved the routing creation out of `ServeHTTP` and into our `NewPlayerServer` so this only has to be done once, not per request. * You will need to update all the test and production code where we used to do `PlayerServer{&store}` with `NewPlayerServer(&store)`. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#one-final-refactor) One final refactor Try changing the code to the following. Finally make sure you **delete** `func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request)` as it is no longer needed! [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#embedding) Embedding --------------------------------------------------------------------------------------------------------- We changed the second property of `PlayerServer`, removing the named property `router http.ServeMux` and replaced it with `http.Handler`; this is called _embedding_. > Go does not provide the typical, type-driven notion of subclassing, but it does have the ability to “borrow” pieces of an implementation by embedding types within a struct or interface. [Effective Go - Embedding](https://golang.org/doc/effective_go.html#embedding) What this means is that our `PlayerServer` now has all the methods that `http.Handler` has, which is just `ServeHTTP`. To "fill in" the `http.Handler` we assign it to the `router` we create in `NewPlayerServer`. We can do this because `http.ServeMux` has the method `ServeHTTP`. This lets us remove our own `ServeHTTP` method, as we are already exposing one via the embedded type. Embedding is a very interesting language feature. You can use it with interfaces to compose new interfaces. And you can use it with concrete types too, not just interfaces. As you'd expect if you embed a concrete type you'll have access to all its public methods and fields. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#any-downsides) Any downsides? You must be careful with embedding types because you will expose all public methods and fields of the type you embed. In our case, it is ok because we embedded just the _interface_ that we wanted to expose (`http.Handler`). If we had been lazy and embedded `http.ServeMux` instead (the concrete type) it would still work _but_ users of `PlayerServer` would be able to add new routes to our server because `Handle(path, handler)` would be public. **When embedding types, really think about what impact that has on your public API.** It is a _very_ common mistake to misuse embedding and end up polluting your APIs and exposing the internals of your type. Now we've restructured our application we can easily add new routes and have the start of the `/league` endpoint. We now need to make it return some useful information. We should return some JSON that looks something like this. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#write-the-test-first-1) Write the test first --------------------------------------------------------------------------------------------------------------------------------- We'll start by trying to parse the response into something meaningful. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#why-not-test-the-json-string) Why not test the JSON string? You could argue a simpler initial step would be just to assert that the response body has a particular JSON string. In my experience tests that assert against JSON strings have the following problems. * _Brittleness_. If you change the data-model your tests will fail. * _Hard to debug_. It can be tricky to understand what the actual problem is when comparing two JSON strings. * _Poor intention_. Whilst the output should be JSON, what's really important is exactly what the data is, rather than how it's encoded. * _Re-testing the standard library_. There is no need to test how the standard library outputs JSON, it is already tested. Don't test other people's code. Instead, we should look to parse the JSON into data structures that are relevant for us to test with. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#data-modelling) Data modelling Given the JSON data model, it looks like we need an array of `Player` with some fields so we have created a new type to capture this. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#json-decoding) JSON decoding To parse JSON into our data model we create a `Decoder` from `encoding/json` package and then call its `Decode` method. To create a `Decoder` it needs an `io.Reader` to read from which in our case is our response spy's `Body`. `Decode` takes the address of the thing we are trying to decode into which is why we declare an empty slice of `Player` the line before. Parsing JSON can fail so `Decode` can return an `error`. There's no point continuing the test if that fails so we check for the error and stop the test with `t.Fatalf` if it happens. Notice that we print the response body along with the error as it's important for someone running the test to see what string cannot be parsed. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#try-to-run-the-test-1) Try to run the test ------------------------------------------------------------------------------------------------------------------------------- Our endpoint currently does not return a body so it cannot be parsed into JSON. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#write-enough-code-to-make-it-pass-1) Write enough code to make it pass ----------------------------------------------------------------------------------------------------------------------------------------------------------- The test now passes. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#encoding-and-decoding) Encoding and Decoding Notice the lovely symmetry in the standard library. * To create an `Encoder` you need an `io.Writer` which is what `http.ResponseWriter` implements. * To create a `Decoder` you need an `io.Reader` which the `Body` field of our response spy implements. Throughout this book, we have used `io.Writer` and this is another demonstration of its prevalence in the standard library and how a lot of libraries easily work with it. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#refactor-1) Refactor --------------------------------------------------------------------------------------------------------- It would be nice to introduce a separation of concern between our handler and getting the `leagueTable` as we know we're going to not hard-code that very soon. Next, we'll want to extend our test so that we can control exactly what data we want back. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#write-the-test-first-2) Write the test first --------------------------------------------------------------------------------------------------------------------------------- We can update the test to assert that the league table contains some players that we will stub in our store. Update `StubPlayerStore` to let it store a league, which is just a slice of `Player`. We'll store our expected data in there. Next, update our current test by putting some players in the league property of our stub and assert they get returned from our server. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#try-to-run-the-test-2) Try to run the test ------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- You'll need to update the other tests as we have a new field in `StubPlayerStore`; set it to nil for the other tests. Try running the tests again and you should get [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#write-enough-code-to-make-it-pass-2) Write enough code to make it pass ----------------------------------------------------------------------------------------------------------------------------------------------------------- We know the data is in our `StubPlayerStore` and we've abstracted that away into an interface `PlayerStore`. We need to update this so anyone passing us in a `PlayerStore` can provide us with the data for leagues. Now we can update our handler code to call that rather than returning a hard-coded list. Delete our method `getLeagueTable()` and then update `leagueHandler` to call `GetLeague()`. Try and run the tests. The compiler is complaining because `InMemoryPlayerStore` and `StubPlayerStore` do not have the new method we added to our interface. For `StubPlayerStore` it's pretty easy, just return the `league` field we added earlier. Here's a reminder of how `InMemoryStore` is implemented. Whilst it would be pretty straightforward to implement `GetLeague` "properly" by iterating over the map remember we are just trying to _write the minimal amount of code to make the tests pass_. So let's just get the compiler happy for now and live with the uncomfortable feeling of an incomplete implementation in our `InMemoryStore`. What this is really telling us is that _later_ we're going to want to test this but let's park that for now. Try and run the tests, the compiler should pass and the tests should be passing! [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#refactor-2) Refactor --------------------------------------------------------------------------------------------------------- The test code does not convey out intent very well and has a lot of boilerplate we can refactor away. Here are the new helpers One final thing we need to do for our server to work is make sure we return a `content-type` header in the response so machines can recognise we are returning `JSON`. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#write-the-test-first-3) Write the test first --------------------------------------------------------------------------------------------------------------------------------- Add this assertion to the existing test [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#try-to-run-the-test-3) Try to run the test ------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#write-enough-code-to-make-it-pass-3) Write enough code to make it pass ----------------------------------------------------------------------------------------------------------------------------------------------------------- Update `leagueHandler` The test should pass. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#refactor-3) Refactor --------------------------------------------------------------------------------------------------------- Add a helper for `assertContentType`. Use it in the test. Now that we have sorted out `PlayerServer` for now we can turn our attention to `InMemoryPlayerStore` because right now if we tried to demo this to the product owner `/league` will not work. The quickest way for us to get some confidence is to add to our integration test, we can hit the new endpoint and check we get back the correct response from `/league`. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#write-the-test-first-4) Write the test first --------------------------------------------------------------------------------------------------------------------------------- We can use `t.Run` to break up this test a bit and we can reuse the helpers from our server tests - again showing the importance of refactoring tests. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#try-to-run-the-test-4) Try to run the test ------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#write-enough-code-to-make-it-pass-4) Write enough code to make it pass ----------------------------------------------------------------------------------------------------------------------------------------------------------- `InMemoryPlayerStore` is returning `nil` when you call `GetLeague()` so we'll need to fix that. All we need to do is iterate over the map and convert each key/value to a `Player`. The test should now pass. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#wrapping-up) Wrapping up ------------------------------------------------------------------------------------------------------------- We've continued to safely iterate on our program using TDD, making it support new endpoints in a maintainable way with a router and it can now return JSON for our consumers. In the next chapter, we will cover persisting the data and sorting our league. What we've covered: * **Routing**. The standard library offers you an easy to use type to do routing. It fully embraces the `http.Handler` interface in that you assign routes to `Handler`s and the router itself is also a `Handler`. It does not have some features you might expect though such as path variables (e.g `/users/{id}`). You can easily parse this information yourself but you might want to consider looking at other routing libraries if it becomes a burden. Most of the popular ones stick to the standard library's philosophy of also implementing `http.Handler`. * **Type embedding**. We touched a little on this technique but you can [learn more about it from Effective Go](https://golang.org/doc/effective_go.html#embedding) . If there is one thing you should take away from this is that it can be extremely useful but _always thinking about your public API, only expose what's appropriate_. * **JSON deserializing and serializing**. The standard library makes it very trivial to serialise and deserialise your data. It is also open to configuration and you can customise how these data transformations work if necessary. [PreviousHTTP server](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server) [NextIO and sorting](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io) Last updated 6 years ago * [Here is the code we have so far](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#here-is-the-code-we-have-so-far) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#write-the-test-first) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#try-to-run-the-test) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#write-enough-code-to-make-it-pass) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#refactor) * [One final refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#one-final-refactor) * [Embedding](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#embedding) * [Any downsides?](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#any-downsides) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#write-the-test-first-1) * [Why not test the JSON string?](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#why-not-test-the-json-string) * [Data modelling](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#data-modelling) * [JSON decoding](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#json-decoding) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#try-to-run-the-test-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#write-enough-code-to-make-it-pass-1) * [Encoding and Decoding](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#encoding-and-decoding) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#refactor-1) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#write-the-test-first-2) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#try-to-run-the-test-2) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#write-enough-code-to-make-it-pass-2) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#refactor-2) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#write-the-test-first-3) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#try-to-run-the-test-3) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#write-enough-code-to-make-it-pass-3) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#refactor-3) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#write-the-test-first-4) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#try-to-run-the-test-4) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#write-enough-code-to-make-it-pass-4) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json#wrapping-up) Copy // InMemoryPlayerStore.go package main func NewInMemoryPlayerStore() *InMemoryPlayerStore { return &InMemoryPlayerStore{map[string]int{}} } type InMemoryPlayerStore struct { store map[string]int } func (i *InMemoryPlayerStore) RecordWin(name string) { i.store[name]++ } func (i *InMemoryPlayerStore) GetPlayerScore(name string) int { return i.store[name] } Copy // main.go package main import ( "log" "net/http" ) func main() { server := &PlayerServer{NewInMemoryPlayerStore()} if err := http.ListenAndServe(":5000", server); err != nil { log.Fatalf("could not listen on port 5000 %v", err) } } Copy func TestLeague(t *testing.T) { store := StubPlayerStore{} server := &PlayerServer{&store} t.Run("it returns 200 on /league", func(t *testing.T) { request, _ := http.NewRequest(http.MethodGet, "/league", nil) response := httptest.NewRecorder() server.ServeHTTP(response, request) assertStatus(t, response.Code, http.StatusOK) }) } Copy === RUN TestLeague/it_returns_200_on_/league panic: runtime error: slice bounds out of range [recovered] panic: runtime error: slice bounds out of range goroutine 6 [running]: testing.tRunner.func1(0xc42010c3c0) /usr/local/Cellar/go/1.10/libexec/src/testing/testing.go:742 +0x29d panic(0x1274d60, 0x1438240) /usr/local/Cellar/go/1.10/libexec/src/runtime/panic.go:505 +0x229 github.com/quii/learn-go-with-tests/json-and-io/v2.(*PlayerServer).ServeHTTP(0xc420048d30, 0x12fc1c0, 0xc420010940, 0xc420116000) /Users/quii/go/src/github.com/quii/learn-go-with-tests/json-and-io/v2/server.go:20 +0xec Copy player := r.URL.Path[len("/players/"):] Copy func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { router := http.NewServeMux() router.Handle("/league", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) router.Handle("/players/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { player := r.URL.Path[len("/players/"):] switch r.Method { case http.MethodPost: p.processWin(w, player) case http.MethodGet: p.showScore(w, player) } })) router.ServeHTTP(w, r) } Copy func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { router := http.NewServeMux() router.Handle("/league", http.HandlerFunc(p.leagueHandler)) router.Handle("/players/", http.HandlerFunc(p.playersHandler)) router.ServeHTTP(w, r) } func (p *PlayerServer) leagueHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } func (p *PlayerServer) playersHandler(w http.ResponseWriter, r *http.Request) { player := r.URL.Path[len("/players/"):] switch r.Method { case http.MethodPost: p.processWin(w, player) case http.MethodGet: p.showScore(w, player) } } Copy type PlayerServer struct { store PlayerStore router *http.ServeMux } func NewPlayerServer(store PlayerStore) *PlayerServer { p := &PlayerServer{ store, http.NewServeMux(), } p.router.Handle("/league", http.HandlerFunc(p.leagueHandler)) p.router.Handle("/players/", http.HandlerFunc(p.playersHandler)) return p } func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { p.router.ServeHTTP(w, r) } Copy type PlayerServer struct { store PlayerStore http.Handler } func NewPlayerServer(store PlayerStore) *PlayerServer { p := new(PlayerServer) p.store = store router := http.NewServeMux() router.Handle("/league", http.HandlerFunc(p.leagueHandler)) router.Handle("/players/", http.HandlerFunc(p.playersHandler)) p.Handler = router return p } Copy type Animal interface { Eater Sleeper } Copy [\ {\ "Name":"Bill",\ "Wins":10\ },\ {\ "Name":"Alice",\ "Wins":15\ }\ ] Copy func TestLeague(t *testing.T) { store := StubPlayerStore{} server := NewPlayerServer(&store) t.Run("it returns 200 on /league", func(t *testing.T) { request, _ := http.NewRequest(http.MethodGet, "/league", nil) response := httptest.NewRecorder() server.ServeHTTP(response, request) var got []Player err := json.NewDecoder(response.Body).Decode(&got) if err != nil { t.Fatalf("Unable to parse response from server %q into slice of Player, '%v'", response.Body, err) } assertStatus(t, response.Code, http.StatusOK) }) } Copy type Player struct { Name string Wins int } Copy var got []Player err := json.NewDecoder(response.Body).Decode(&got) Copy === RUN TestLeague/it_returns_200_on_/league --- FAIL: TestLeague/it_returns_200_on_/league (0.00s) server_test.go:107: Unable to parse response from server '' into slice of Player, 'unexpected end of JSON input' Copy func (p *PlayerServer) leagueHandler(w http.ResponseWriter, r *http.Request) { leagueTable := []Player{ {"Chris", 20}, } json.NewEncoder(w).Encode(leagueTable) w.WriteHeader(http.StatusOK) } Copy func (p *PlayerServer) leagueHandler(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(p.getLeagueTable()) w.WriteHeader(http.StatusOK) } func (p *PlayerServer) getLeagueTable() []Player { return []Player{ {"Chris", 20}, } } Copy type StubPlayerStore struct { scores map[string]int winCalls []string league []Player } Copy func TestLeague(t *testing.T) { t.Run("it returns the league table as JSON", func(t *testing.T) { wantedLeague := []Player{ {"Cleo", 32}, {"Chris", 20}, {"Tiest", 14}, } store := StubPlayerStore{nil, nil, wantedLeague} server := NewPlayerServer(&store) request, _ := http.NewRequest(http.MethodGet, "/league", nil) response := httptest.NewRecorder() server.ServeHTTP(response, request) var got []Player err := json.NewDecoder(response.Body).Decode(&got) if err != nil { t.Fatalf("Unable to parse response from server %q into slice of Player, '%v'", response.Body, err) } assertStatus(t, response.Code, http.StatusOK) if !reflect.DeepEqual(got, wantedLeague) { t.Errorf("got %v want %v", got, wantedLeague) } }) } Copy ./server_test.go:33:3: too few values in struct initializer ./server_test.go:70:3: too few values in struct initializer Copy === RUN TestLeague/it_returns_the_league_table_as_JSON --- FAIL: TestLeague/it_returns_the_league_table_as_JSON (0.00s) server_test.go:124: got [{Chris 20}] want [{Cleo 32} {Chris 20} {Tiest 14}] Copy type PlayerStore interface { GetPlayerScore(name string) int RecordWin(name string) GetLeague() []Player } Copy func (p *PlayerServer) leagueHandler(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(p.store.GetLeague()) w.WriteHeader(http.StatusOK) } Copy # github.com/quii/learn-go-with-tests/json-and-io/v4 ./main.go:9:50: cannot use NewInMemoryPlayerStore() (type *InMemoryPlayerStore) as type PlayerStore in argument to NewPlayerServer: *InMemoryPlayerStore does not implement PlayerStore (missing GetLeague method) ./server_integration_test.go:11:27: cannot use store (type *InMemoryPlayerStore) as type PlayerStore in argument to NewPlayerServer: *InMemoryPlayerStore does not implement PlayerStore (missing GetLeague method) ./server_test.go:36:28: cannot use &store (type *StubPlayerStore) as type PlayerStore in argument to NewPlayerServer: *StubPlayerStore does not implement PlayerStore (missing GetLeague method) ./server_test.go:74:28: cannot use &store (type *StubPlayerStore) as type PlayerStore in argument to NewPlayerServer: *StubPlayerStore does not implement PlayerStore (missing GetLeague method) ./server_test.go:106:29: cannot use &store (type *StubPlayerStore) as type PlayerStore in argument to NewPlayerServer: *StubPlayerStore does not implement PlayerStore (missing GetLeague method) Copy func (s *StubPlayerStore) GetLeague() []Player { return s.league } Copy type InMemoryPlayerStore struct { store map[string]int } Copy func (i *InMemoryPlayerStore) GetLeague() []Player { return nil } Copy t.Run("it returns the league table as JSON", func(t *testing.T) { wantedLeague := []Player{ {"Cleo", 32}, {"Chris", 20}, {"Tiest", 14}, } store := StubPlayerStore{nil, nil, wantedLeague} server := NewPlayerServer(&store) request := newLeagueRequest() response := httptest.NewRecorder() server.ServeHTTP(response, request) got := getLeagueFromResponse(t, response.Body) assertStatus(t, response.Code, http.StatusOK) assertLeague(t, got, wantedLeague) }) Copy func getLeagueFromResponse(t *testing.T, body io.Reader) (league []Player) { t.Helper() err := json.NewDecoder(body).Decode(&league) if err != nil { t.Fatalf("Unable to parse response from server %q into slice of Player, '%v'", body, err) } return } func assertLeague(t *testing.T, got, want []Player) { t.Helper() if !reflect.DeepEqual(got, want) { t.Errorf("got %v want %v", got, want) } } func newLeagueRequest() *http.Request { req, _ := http.NewRequest(http.MethodGet, "/league", nil) return req } Copy if response.Result().Header.Get("content-type") != "application/json" { t.Errorf("response did not have content-type of application/json, got %v", response.Result().Header) } Copy === RUN TestLeague/it_returns_the_league_table_as_JSON --- FAIL: TestLeague/it_returns_the_league_table_as_JSON (0.00s) server_test.go:124: response did not have content-type of application/json, got map[Content-Type:[text/plain; charset=utf-8]] Copy func (p *PlayerServer) leagueHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("content-type", "application/json") json.NewEncoder(w).Encode(p.store.GetLeague()) } Copy const jsonContentType = "application/json" func assertContentType(t *testing.T, response *httptest.ResponseRecorder, want string) { t.Helper() if response.Result().Header.Get("content-type") != want { t.Errorf("response did not have content-type of %s, got %v", want, response.Result().Header) } } Copy assertContentType(t, response, jsonContentType) Copy func TestRecordingWinsAndRetrievingThem(t *testing.T) { store := NewInMemoryPlayerStore() server := NewPlayerServer(store) player := "Pepper" server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player)) server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player)) server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player)) t.Run("get score", func(t *testing.T) { response := httptest.NewRecorder() server.ServeHTTP(response, newGetScoreRequest(player)) assertStatus(t, response.Code, http.StatusOK) assertResponseBody(t, response.Body.String(), "3") }) t.Run("get league", func(t *testing.T) { response := httptest.NewRecorder() server.ServeHTTP(response, newLeagueRequest()) assertStatus(t, response.Code, http.StatusOK) got := getLeagueFromResponse(t, response.Body) want := []Player{ {"Pepper", 3}, } assertLeague(t, got, want) }) } Copy === RUN TestRecordingWinsAndRetrievingThem/get_league --- FAIL: TestRecordingWinsAndRetrievingThem/get_league (0.00s) server_integration_test.go:35: got [] want [{Pepper 3}] Copy func (i *InMemoryPlayerStore) GetLeague() []Player { var league []Player for name, wins := range i.store { league = append(league, Player{name, wins}) } return league } --- # Integers | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/integers.md) . [**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. **Note:** Go source files can only have one `package` per directory. Make sure that your files are organised into their own packages. [Here is a good explanation on this.](https://dave.cheney.net/2014/12/01/five-suggestions-for-setting-up-a-go-project) Your project directory might look something like this: Copy learnGoWithTests | |-> helloworld | |- hello.go | |- hello_test.go | |-> integers | |- adder_test.go | |- go.mod |- README.md [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/integers#write-the-test-first) Write the test first ----------------------------------------------------------------------------------------------------------------------- Copy package integers import "testing" func TestAdder(t *testing.T) { sum := Add(2, 2) expected := 4 if sum != expected { t.Errorf("expected '%d' but got '%d'", expected, sum) } } You will notice that we're using `%d` as our format strings rather than `%q`. That's because we want it to print an integer rather than a string. Also note that we are no longer using the main package, instead we've defined a package named `integers`, as the name suggests this will group functions for working with integers such as `Add`. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/integers#try-and-run-the-test) Try and run the test ----------------------------------------------------------------------------------------------------------------------- Run the test `go test` Inspect the compilation error `./adder_test.go:6:9: undefined: Add` [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/integers#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Write enough code to satisfy the compiler _and that's all_ - remember we want to check that our tests fail for the correct reason. Remember, when you have more than one argument of the same type (in our case two integers) rather than having `(x int, y int)` you can shorten it to `(x, y int)`. Now run the tests, and we should be happy that the test is correctly reporting what is wrong. `adder_test.go:10: expected '4' but got '0'` If you have noticed we learnt about _named return value_ in the [last](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/hello-world#onelastrefactor) section but aren't using the same here. It should generally be used when the meaning of the result isn't clear from context, in our case it's pretty much clear that `Add` function will add the parameters. You can refer [this](https://go.dev/wiki/CodeReviewComments#named-result-parameters) wiki for more details. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/integers#write-enough-code-to-make-it-pass) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------- In the strictest sense of TDD we should now write the _minimal amount of code to make the test pass_. A pedantic programmer may do this Ah hah! Foiled again, TDD is a sham right? We could write another test, with some different numbers to force that test to fail but that feels like [a game of cat and mouse](https://en.m.wikipedia.org/wiki/Cat_and_mouse) . Once we're more familiar with Go's syntax I will introduce a technique called _"Property Based Testing"_, which would stop annoying developers and help you find bugs. For now, let's fix it properly If you re-run the tests they should pass. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/integers#refactor) Refactor ----------------------------------------------------------------------------------------------- There's not a lot in the _actual_ code we can really improve on here. We explored earlier how by naming the return argument it appears in the documentation but also in most developer's text editors. This is great because it aids the usability of code you are writing. It is preferable that a user can understand the usage of your code by just looking at the type signature and documentation. You can add documentation to functions with comments, and these will appear in Go Doc just like when you look at the standard library's documentation. ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/integers#testable-examples) Testable Examples If you really want to go the extra mile you can make [Testable Examples](https://blog.golang.org/examples) . You will find many examples in the standard library documentation. Often code examples that can be found outside the codebase, such as a readme file, become out of date and incorrect compared to the actual code because they don't get checked. Example functions are compiled whenever tests are executed. Because such examples are validated by the Go compiler, you can be confident your documentation's examples always reflect current code behavior. Example functions begin with `Example` (much like test functions begin with `Test`), and reside in a package's `_test.go` files. Add the following `ExampleAdd` function to the `adder_test.go` file. (If your editor doesn't automatically import packages for you, the compilation step will fail because you will be missing `import "fmt"` in `adder_test.go`. It is strongly recommended you research how to have these kind of errors fixed for you automatically in whatever editor you are using.) Adding this code will cause the example to appear in your documentation, making your code even more accessible. If ever your code changes so that the example is no longer valid, your build will fail. Running the package's test suite, we can see the example `ExampleAdd` function is executed with no further arrangement from us: Notice the special format of the comment, `// Output: 6`. While the example will always be compiled, adding this comment means the example will also be executed. Go ahead and temporarily remove the comment `// Output: 6`, then run `go test`, and you will see `ExampleAdd` is no longer executed. Examples without output comments are useful for demonstrating code that cannot run as unit tests, such as that which accesses the network, while guaranteeing the example at least compiles. To view example documentation, let's take a quick look at `pkgsite`. Before navigating to your project's directory, make sure you have installed `pkgsite` by running the following command: `go install golang.org/x/pkgsite/cmd/pkgsite@latest`, then run `pkgsite -open .`, which should open a web browser for you, pointing to `http://localhost:8080`. Inside here you'll see a list of all of Go's Standard Library packages, plus Third Party packages you have installed, under which you should see your example documentation for `github.com/quii/learn-go-with-tests`. Follow that link, and then look under `Integers`, then under `func Add`, then expand `Example` and you should see the example you added for `sum := Add(1, 5)`. If you publish your code with examples to a public URL, you can share the documentation of your code at [pkg.go.dev](https://pkg.go.dev/) . For example, [here](https://pkg.go.dev/github.com/quii/learn-go-with-tests/integers/v2) is the finalised API for this chapter. This web interface allows you to search for documentation of standard library packages and third-party packages. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/integers#wrapping-up) Wrapping up ----------------------------------------------------------------------------------------------------- What we have covered: * More practice of the TDD workflow * Integers, addition * Writing better documentation so users of our code can understand its usage quickly * Examples of how to use our code, which are checked as part of our tests [PreviousHello, World](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/hello-world) [NextIteration](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/iteration) Last updated 2 months ago * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/integers#write-the-test-first) * [Try and run the test](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/integers#try-and-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/integers#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/integers#write-enough-code-to-make-it-pass) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/integers#refactor) * [Testable Examples](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/integers#testable-examples) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/integers#wrapping-up) Copy package integers func Add(x, y int) int { return 0 } Copy func Add(x, y int) int { return 4 } Copy func Add(x, y int) int { return x + y } Copy // Add takes two integers and returns the sum of them. func Add(x, y int) int { return x + y } Copy func ExampleAdd() { sum := Add(1, 5) fmt.Println(sum) // Output: 6 } Copy $ go test -v === RUN TestAdder --- PASS: TestAdder (0.00s) === RUN ExampleAdd --- PASS: ExampleAdd (0.00s) --- # Install Go | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/install-go.md) . The official installation instructions for Go are available [here](https://golang.org/doc/install) . [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/install-go#go-environment) Go Environment ------------------------------------------------------------------------------------------------------------- ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/install-go#go-modules) Go Modules Go 1.11 introduced [Modules](https://go.dev/wiki/Modules) . This approach is the default build mode since Go 1.16, therefore the use of `GOPATH` is not recommended. Modules aim to solve problems related to dependency management, version selection and reproducible builds; they also enable users to run Go code outside of `GOPATH`. Using Modules is pretty straightforward. Select any directory outside `GOPATH` as the root of your project, and create a new module with the `go mod init` command. A `go.mod` file will be generated, containing the module path, a Go version, and its dependency requirements, which are the other modules needed for a successful build. If no `` is specified, `go mod init` will try to guess the module path from the directory structure. It can also be overridden by supplying an argument. Copy mkdir my-project cd my-project go mod init A `go.mod` file could look like this: Copy module cmd go 1.16 The built-in documentation provides an overview of all available `go mod` commands. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/install-go#go-linting) Go Linting ----------------------------------------------------------------------------------------------------- An improvement over the default linter can be configured using [GolangCI-Lint](https://golangci-lint.run/) . This can be installed as follows: [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/install-go#refactoring-and-your-tooling) Refactoring and your tooling ----------------------------------------------------------------------------------------------------------------------------------------- A big emphasis of this book is the importance of refactoring. Your tools can help you do bigger refactoring with confidence. You should be familiar enough with your editor to perform the following with a simple key combination: * **Extract/Inline variable**. Taking magic values and giving them a name lets you simplify your code quickly. * **Extract method/function**. It is vital to be able to take a section of code and extract functions/methods * **Rename**. You should be able to rename symbols across files confidently. * **go fmt**. Go has an opinioned formatter called `go fmt`. Your editor should run this on every file saved. * **Run tests**. You should be able to do any of the above and then quickly re-run your tests to ensure your refactoring hasn't broken anything. In addition, to help you work with your code, you should be able to: * **View function signature**. You should never be unsure how to call a function in Go. Your IDE should describe a function in terms of its documentation, its parameters and what it returns. * **View function definition**. If it's still unclear what a function does, you should be able to jump to the source code and try and figure it out yourself. * **Find usages of a symbol**. Understanding a function's context can help you make decisions when refactoring. Mastering your tools will help you concentrate on the code and reduce context switching. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/install-go#wrapping-up) Wrapping up ------------------------------------------------------------------------------------------------------- At this point, you should have Go installed, an editor available, and some basic tooling in place. Go has a very large ecosystem of third-party products. We have identified a few useful components here. For a more complete list, see [https://awesome-go.com](https://awesome-go.com/) . [PreviousLearn Go with Tests](https://quii.gitbook.io/learn-go-with-tests) [NextHello, World](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/hello-world) Last updated 2 years ago * [Go Environment](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/install-go#go-environment) * [Go Modules](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/install-go#go-modules) * [Go Linting](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/install-go#go-linting) * [Refactoring and your tooling](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/install-go#refactoring-and-your-tooling) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/install-go#wrapping-up) Copy go help mod go help mod init Copy brew install golangci-lint --- # Iteration | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/iteration.md) . [**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! Let's write a test for a function that repeats a character 5 times. There's nothing new so far, so try and write it yourself for practice. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/iteration#write-the-test-first) Write the test first ------------------------------------------------------------------------------------------------------------------------ Copy package iteration import "testing" func TestRepeat(t *testing.T) { repeated := Repeat("a") expected := "aaaaa" if repeated != expected { t.Errorf("expected %q but got %q", expected, repeated) } } [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/iteration#try-and-run-the-test) Try and run the test ------------------------------------------------------------------------------------------------------------------------ `./repeat_test.go:6:14: undefined: Repeat` [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/iteration#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ _Keep the discipline!_ You don't need to know anything new right now to make the test fail properly. All you need to do right now is enough to make it compile so you can check your test is written well. Isn't it nice to know you already know enough Go to write tests for some basic problems? This means you can now play with the production code as much as you like and know it's behaving as you'd hope. `repeat_test.go:10: expected 'aaaaa' but got ''` [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/iteration#write-enough-code-to-make-it-pass) Write enough code to make it pass -------------------------------------------------------------------------------------------------------------------------------------------------- The `for` syntax is very unremarkable and follows most C-like languages. Unlike other languages like C, Java, or JavaScript there are no parentheses surrounding the three components of the for statement and the braces `{ }` are always required. You might wonder what is happening in the row as we've been using `:=` so far to declare and initializing variables. However, `:=` is simply [short hand for both steps](https://gobyexample.com/variables) . Here we are declaring a `string` variable only. Hence, the explicit version. We can also use `var` to declare functions, as we'll see later on. Run the test and it should pass. Additional variants of the for loop are described [here](https://gobyexample.com/for) . [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/iteration#refactor) Refactor ------------------------------------------------------------------------------------------------ Now it's time to refactor and introduce another construct `+=` assignment operator. `+=` called _"the Add AND assignment operator"_, adds the right operand to the left operand and assigns the result to left operand. It works with other types like integers. ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/iteration#benchmarking) Benchmarking Writing [benchmarks](https://golang.org/pkg/testing/#hdr-Benchmarks) in Go is another first-class feature of the language and it is very similar to writing tests. You'll see the code is very similar to a test. The `testing.B` gives you access to the loop function. `Loop()` returns true as long as the benchmark should continue running. When the benchmark code is executed, it measures how long it takes. After `Loop()` returns false, `b.N` contains the total number of iterations that ran. The number of times the code is run shouldn't matter to you, the framework will determine what is a "good" value for that to let you have some decent results. To run the benchmarks do `go test -bench=.` (or if you're in Windows Powershell `go test -bench="."`) What `136 ns/op` means is our function takes on average 136 nanoseconds to run (on my computer). Which is pretty ok! To test this it ran it 10000000 times. **Note:** By default benchmarks are run sequentially. Only the body of the loop is timed; it automatically excludes setup and cleanup code from benchmark timing. A typical benchmark is structured like: Strings in Go are immutable, meaning every concatenation, such as in our `Repeat` function, involves copying memory to accommodate the new string. This impacts performance, particularly during heavy string concatenation. The standard library provides the `strings.Builder`[stringsBuilder](https://pkg.go.dev/strings#Builder) type which minimizes memory copying. It implements a `WriteString` method which we can use to concatenate strings: **Note**: We have to call the `String` method to retrieve the final result. We can use `BenchmarkRepeat` to confirm that `strings.Builder` significantly improves performance. Run `go test -bench=. -benchmem`: The `-benchmem` flag reports information about memory allocations: * `B/op`: the number of bytes allocated per iteration * `allocs/op`: the number of memory allocations per iteration [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/iteration#practice-exercises) Practice exercises -------------------------------------------------------------------------------------------------------------------- * Change the test so a caller can specify how many times the character is repeated and then fix the code * Write `ExampleRepeat` to document your function * Have a look through the [strings](https://golang.org/pkg/strings) package. Find functions you think could be useful and experiment with them by writing tests like we have here. Investing time learning the standard library will really pay off over time. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/iteration#wrapping-up) Wrapping up ------------------------------------------------------------------------------------------------------ * More TDD practice * Learned `for` * Learned how to write benchmarks [PreviousIntegers](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/integers) [NextArrays and slices](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices) Last updated 10 months ago * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/iteration#write-the-test-first) * [Try and run the test](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/iteration#try-and-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/iteration#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/iteration#write-enough-code-to-make-it-pass) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/iteration#refactor) * [Benchmarking](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/iteration#benchmarking) * [Practice exercises](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/iteration#practice-exercises) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/iteration#wrapping-up) Copy package iteration func Repeat(character string) string { return "" } Copy func Repeat(character string) string { var repeated string for i := 0; i < 5; i++ { repeated = repeated + character } return repeated } Copy var repeated string Copy const repeatCount = 5 func Repeat(character string) string { var repeated string for i := 0; i < repeatCount; i++ { repeated += character } return repeated } Copy func BenchmarkRepeat(b *testing.B) { for b.Loop() { Repeat("a") } } Copy goos: darwin goarch: amd64 pkg: github.com/quii/learn-go-with-tests/for/v4 10000000 136 ns/op PASS Copy func Benchmark(b *testing.B) { //... setup ... for b.Loop() { //... code to measure ... } //... cleanup ... } Copy const repeatCount = 5 func Repeat(character string) string { var repeated strings.Builder for i := 0; i < repeatCount; i++ { repeated.WriteString(character) } return repeated.String() } Copy goos: darwin goarch: amd64 pkg: github.com/quii/learn-go-with-tests/for/v4 10000000 25.70 ns/op 8 B/op 1 allocs/op PASS --- # WebSockets | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets.md) . [**You can find all the code for this chapter here**](https://github.com/quii/learn-go-with-tests/tree/master/websockets) In this chapter we'll learn how to use WebSockets to improve our application. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#project-recap) 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. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#next-steps) Next steps ----------------------------------------------------------------------------------------------------------------- The product owner is thrilled with the command line application but would prefer it if we could bring that functionality to the browser. She imagines a web page with a text box that allows the user to enter the number of players and when they submit the form the page displays the blind value and automatically updates it when appropriate. Like the command line application the user can declare the winner and it'll get saved in the database. On the face of it, it sounds quite simple but as always we must emphasise taking an _iterative_ approach to writing software. 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. > 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. 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. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#what-about-tests-for-the-javascript) What about tests for the JavaScript ? 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. Sorry folks. Lobby O'Reilly to pay me to make a "Learn JavaScript with tests". [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#write-the-test-first) Write the test first ------------------------------------------------------------------------------------------------------------------------------------- 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 The _easiest_ thing we can do for now is check when we `GET /game` that we get a `200`. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#try-to-run-the-test) Try to run the test ----------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#write-enough-code-to-make-it-pass) Write enough code to make it pass --------------------------------------------------------------------------------------------------------------------------------------------------------------- Our server has a router setup so it's relatively easy to fix. To our router add And then write the `game` method [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#refactor) Refactor ------------------------------------------------------------------------------------------------------------- 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. You'll also notice I changed `assertStatus` to accept `response` rather than `response.Code` as I feel it reads better. Now we need to make the endpoint return some HTML, here it is 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. * 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. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#how-do-we-test-we-return-the-correct-markup) How do we test we return the correct markup? There are a few ways. As has been emphasised throughout the book, it is important that the tests you write have sufficient value to justify the cost. 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. 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` Next change the endpoint we just wrote to the following [`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`. 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. 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. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#write-the-test-first-1) Write the test first --------------------------------------------------------------------------------------------------------------------------------------- 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` This will fetch the code for the excellent [Gorilla WebSocket](https://github.com/gorilla/websocket) library. Now we can update our tests for our new requirement. 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. 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`. Finally we assert on the player store to check the winner was recorded. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#try-to-run-the-test-1) Try to run the test ------------------------------------------------------------------------------------------------------------------------------------- We have not changed our server to accept WebSocket connections on `/ws` so we're not shaking hands yet. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#write-enough-code-to-make-it-pass-1) Write enough code to make it pass ----------------------------------------------------------------------------------------------------------------------------------------------------------------- Add another listing to our router Then add our new `webSocket` handler To accept a WebSocket connection we `Upgrade` the request. If you now re-run the test you should move on to the next error. Now that we have a connection opened, we'll want to listen for a message and then record it as the winner. (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. 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. Let's go with that for now but acknowledge that putting in arbitrary sleeps into tests **is very bad practice**. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#refactor-1) 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 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. We can move the `upgrader` to a private value inside our package because we don't need to redeclare it on every WebSocket connection request Our call to `template.ParseFiles("game.html")` will run on every `GET /game` which means we'll go to the file system on every request even though we have no need to re-parse the template. Let's refactor our code so that we parse the template once in `NewPlayerServer` instead. We'll have to make it so this function can now return an error in case we have problems fetching the template from disk or parsing it. Here's the relevant changes to `PlayerServer` 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. Similarly I created another helper `mustDialWS` so that I could hide nasty error noise when creating the WebSocket connection. Finally in our test code we can create a helper to tidy up sending messages Now the tests are passing try running the server and declare some winners in `/game`. You should see them recorded in `/league`. Remember that every time we get a winner we _close the connection_, you will need to refresh the page to open the connection again. We've made a trivial web form that lets users record the winner of a game. Let's iterate on it to make it so the user can start a game by providing a number of players and the server will push messages to the client informing them of what the blind value is as time passes. First of all update `game.html` to update our client side code for the new requirements 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. When the user was prompted in the CLI for number of players it would `Start` the game which would kick off the blind alerts and when the user declared the winner they would `Finish`. This is the same requirements we have now, just a different way of getting the inputs; so we should look to re-use this concept if we can. Our "real" implementation of `Game` is `TexasHoldem` By sending in a `BlindAlerter` `TexasHoldem` can schedule blind alerts to be sent to _wherever_ And as a reminder, here is our implementation of the `BlindAlerter` we use in the CLI. 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. Open BlindAlerter.go and add the parameter `to io.Writer` The idea of a `StdoutAlerter` doesn't fit our new model so just rename it to `Alerter` 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. 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. Let the compiler tell you what you need to fix. The change isn't so bad: * Update `TexasHoldem` so it properly implements `Game` * In `CLI` when we start the game, pass in our `out` property (`cli.game.Start(numberOfPlayers, cli.out)`) * In `TexasHoldem`'s test i use `game.Start(5, ioutil.Discard)` to fix the compilation problem and configure the alert output to be discarded If you've got everything right, everything should be green! Now we can try and use `Game` within `Server`. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#write-the-test-first-2) Write the test first --------------------------------------------------------------------------------------------------------------------------------------- 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. It looks like we should be able to test drive out a similar outcome using `GameSpy` Replace the old websocket test with the following * 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. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#try-to-run-the-test-2) Try to run the test ------------------------------------------------------------------------------------------------------------------------------------- You'll have a number of compilation errors around `mustMakePlayerServer` in other tests. Introduce an unexported variable `dummyGame` and use it through all the tests that aren't compiling The final error is where we are trying to pass in `Game` to `NewPlayerServer` but it doesn't support it yet [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) 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 Finally! [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#write-enough-code-to-make-it-pass-2) 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 already have a method called `game` so rename that to `playGame`) Next lets assign it in our constructor Now we can use our `Game` within `webSocket`. 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. For now start the web server up. You'll need to update the `main.go` to pass a `Game` to the `PlayerServer` 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. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#refactor-2) Refactor --------------------------------------------------------------------------------------------------------------- The way we're using WebSockets is fairly basic and the error handling is fairly naive, so I wanted to encapsulate that in a type just to remove that messiness from the server code. We may wish to revisit it later but for now this'll tidy things up a bit Now the server code is a bit simplified Once we figure out how to not discard the blind messages we're done. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#lets-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. The problematic line of code we have is 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. Give it a go: The compiler complains It seems the obvious thing to do, would be to make it so `playerServerWS` _does_ implement `io.Writer`. To do so we use the underlying `*websocket.Conn` to use `WriteMessage` to send the message down the websocket This seems too easy! Try and run the application and see if it works. Beforehand edit `TexasHoldem` so that the blind increment time is shorter so you can see it in action 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. 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. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#write-the-test-first-3) Write the test first --------------------------------------------------------------------------------------------------------------------------------------- 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. Add `BlindAlert` field. Update `GameSpy` `Start` to send the canned message to `out`. 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 * We've added a `wantedBlindAlert` and configured our `GameSpy` to send it to `out` if `Start` is called. * We hope it gets sent in the websocket connection so we've added a call to `ws.ReadMessage()` to wait for a message to be sent and then check it's the one we expected. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#try-to-run-the-test-3) 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. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) Write the minimal amount of code for the test to run and check the failing test output --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- We should never have tests that hang so let's introduce a way of handling code that we want to timeout. 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. Finally I made a helper function for our assertion just to make things a bit neater Here's how the test reads now Now if you run the test... [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#write-enough-code-to-make-it-pass-3) Write enough code to make it pass ----------------------------------------------------------------------------------------------------------------------------------------------------------------- Finally we can now change our server code so it sends our WebSocket connection to the game when it starts [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#refactor-3) 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. We can refactor our helpers `assertGameStartedWith` and `assertFinishCalledWith` so that they can retry their assertions for a short period before failing. Here's how you can do it for `assertFinishCalledWith` and you can use the same approach for the other helper. Here is how `retryUntil` is defined [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#wrapping-up) 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. 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. We covered a few things in this chapter ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#websockets) 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 ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#handling-code-in-tests-that-can-be-delayed-or-never-finish) 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. * The `time` package has some helpful functions which also send signals via channels about events in time so we can set timeouts [PreviousTime](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time) [NextOS Exec](https://quii.gitbook.io/learn-go-with-tests/master/questions-and-answers/os-exec) Last updated 6 years ago * [Project recap](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#project-recap) * [Next steps](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#next-steps) * [What about tests for the JavaScript ?](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#what-about-tests-for-the-javascript) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#write-the-test-first) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#try-to-run-the-test) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#write-enough-code-to-make-it-pass) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#refactor) * [How do we test we return the correct markup?](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#how-do-we-test-we-return-the-correct-markup) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#write-the-test-first-1) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#try-to-run-the-test-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#write-enough-code-to-make-it-pass-1) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#refactor-1) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#write-the-test-first-2) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#try-to-run-the-test-2) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#write-enough-code-to-make-it-pass-2) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#refactor-2) * [Let's not write a test!](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#lets-not-write-a-test) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#write-the-test-first-3) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#try-to-run-the-test-3) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#write-enough-code-to-make-it-pass-3) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#refactor-3) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#wrapping-up) * [WebSockets](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#websockets) * [Handling code in tests that can be delayed or never finish](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets#handling-code-in-tests-that-can-be-delayed-or-never-finish) Copy type PlayerServer struct { store PlayerStore http.Handler } const jsonContentType = "application/json" func NewPlayerServer(store PlayerStore) *PlayerServer { p := new(PlayerServer) p.store = store router := http.NewServeMux() router.Handle("/league", http.HandlerFunc(p.leagueHandler)) router.Handle("/players/", http.HandlerFunc(p.playersHandler)) p.Handler = router return p } Copy func TestGame(t *testing.T) { t.Run("GET /game returns 200", func(t *testing.T) { server := NewPlayerServer(&StubPlayerStore{}) request, _ := http.NewRequest(http.MethodGet, "/game", nil) response := httptest.NewRecorder() server.ServeHTTP(response, request) assertStatus(t, response.Code, http.StatusOK) }) } Copy --- FAIL: TestGame (0.00s) === RUN TestGame/GET_/game_returns_200 --- FAIL: TestGame/GET_/game_returns_200 (0.00s) server_test.go:109: did not get correct status, got 404, want 200 Copy router.Handle("/game", http.HandlerFunc(p.game)) Copy func (p *PlayerServer) game(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } Copy func TestGame(t *testing.T) { t.Run("GET /game returns 200", func(t *testing.T) { server := NewPlayerServer(&StubPlayerStore{}) request := newGameRequest() response := httptest.NewRecorder() server.ServeHTTP(response, request) assertStatus(t, response, http.StatusOK) }) } Copy Let's play poker
Copy func (p *PlayerServer) game(w http.ResponseWriter, r *http.Request) { tmpl, err := template.ParseFiles("game.html") if err != nil { http.Error(w, fmt.Sprintf("problem loading template %s", err.Error()), http.StatusInternalServerError) return } tmpl.Execute(w, nil) } Copy t.Run("when we get a message over a websocket it is a winner of a game", func(t *testing.T) { store := &StubPlayerStore{} winner := "Ruth" server := httptest.NewServer(NewPlayerServer(store)) defer server.Close() wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/ws" ws, _, err := websocket.DefaultDialer.Dial(wsURL, nil) if err != nil { t.Fatalf("could not open a ws connection on %s %v", wsURL, err) } defer ws.Close() if err := ws.WriteMessage(websocket.TextMessage, []byte(winner)); err != nil { t.Fatalf("could not send message over ws connection %v", err) } AssertPlayerWin(t, store, winner) }) Copy === RUN TestGame/when_we_get_a_message_over_a_websocket_it_is_a_winner_of_a_game --- FAIL: TestGame/when_we_get_a_message_over_a_websocket_it_is_a_winner_of_a_game (0.00s) server_test.go:124: could not open a ws connection on ws://127.0.0.1:55838/ws websocket: bad handshake Copy router.Handle("/ws", http.HandlerFunc(p.webSocket)) Copy func (p *PlayerServer) webSocket(w http.ResponseWriter, r *http.Request) { upgrader := websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, } upgrader.Upgrade(w, r, nil) } Copy === RUN TestGame/when_we_get_a_message_over_a_websocket_it_is_a_winner_of_a_game --- FAIL: TestGame/when_we_get_a_message_over_a_websocket_it_is_a_winner_of_a_game (0.00s) server_test.go:132: got 0 calls to RecordWin want 1 Copy func (p *PlayerServer) webSocket(w http.ResponseWriter, r *http.Request) { upgrader := websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, } conn, _ := upgrader.Upgrade(w, r, nil) _, winnerMsg, _ := conn.ReadMessage() p.store.RecordWin(string(winnerMsg)) } Copy time.Sleep(10 * time.Millisecond) AssertPlayerWin(t, store, winner) Copy var wsUpgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, } func (p *PlayerServer) webSocket(w http.ResponseWriter, r *http.Request) { conn, _ := wsUpgrader.Upgrade(w, r, nil) _, winnerMsg, _ := conn.ReadMessage() p.store.RecordWin(string(winnerMsg)) } Copy type PlayerServer struct { store PlayerStore http.Handler template *template.Template } const htmlTemplatePath = "game.html" func NewPlayerServer(store PlayerStore) (*PlayerServer, error) { p := new(PlayerServer) tmpl, err := template.ParseFiles(htmlTemplatePath) if err != nil { return nil, fmt.Errorf("problem opening %s %v", htmlTemplatePath, err) } p.template = tmpl p.store = store router := http.NewServeMux() router.Handle("/league", http.HandlerFunc(p.leagueHandler)) router.Handle("/players/", http.HandlerFunc(p.playersHandler)) router.Handle("/game", http.HandlerFunc(p.game)) router.Handle("/ws", http.HandlerFunc(p.webSocket)) p.Handler = router return p, nil } func (p *PlayerServer) game(w http.ResponseWriter, r *http.Request) { p.template.Execute(w, nil) } Copy func mustMakePlayerServer(t *testing.T, store PlayerStore) *PlayerServer { server, err := NewPlayerServer(store) if err != nil { t.Fatal("problem creating player server", err) } return server } Copy func mustDialWS(t *testing.T, url string) *websocket.Conn { ws, _, err := websocket.DefaultDialer.Dial(url, nil) if err != nil { t.Fatalf("could not open a ws connection on %s %v", url, err) } return ws } Copy func writeWSMessage(t *testing.T, conn *websocket.Conn, message string) { t.Helper() if err := conn.WriteMessage(websocket.TextMessage, []byte(message)); err != nil { t.Fatalf("could not send message over ws connection %v", err) } } Copy Lets play poker

Another great game of poker everyone!

Go check the league table

Copy type Game interface { Start(numberOfPlayers int) Finish(winner string) } Copy type TexasHoldem struct { alerter BlindAlerter store PlayerStore } Copy type BlindAlerter interface { ScheduleAlertAt(duration time.Duration, amount int) } Copy func StdOutAlerter(duration time.Duration, amount int) { time.AfterFunc(duration, func() { fmt.Fprintf(os.Stdout, "Blind is now %d\n", amount) }) } Copy type BlindAlerter interface { ScheduleAlertAt(duration time.Duration, amount int, to io.Writer) } type BlindAlerterFunc func(duration time.Duration, amount int, to io.Writer) func (a BlindAlerterFunc) ScheduleAlertAt(duration time.Duration, amount int, to io.Writer) { a(duration, amount, to) } Copy func Alerter(duration time.Duration, amount int, to io.Writer) { time.AfterFunc(duration, func() { fmt.Fprintf(to, "Blind is now %d\n", amount) }) } Copy type Game interface { Start(numberOfPlayers int, alertsDestination io.Writer) Finish(winner string) } Copy 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) assertGameStartedWith(t, game, 3) assertFinishCalledWith(t, game, "Chris") }) Copy t.Run("start a game with 3 players and declare Ruth the winner", func(t *testing.T) { game := &poker.GameSpy{} winner := "Ruth" server := httptest.NewServer(mustMakePlayerServer(t, dummyPlayerStore, game)) ws := mustDialWS(t, "ws"+strings.TrimPrefix(server.URL, "http")+"/ws") defer server.Close() defer ws.Close() writeWSMessage(t, ws, "3") writeWSMessage(t, ws, winner) time.Sleep(10 * time.Millisecond) assertGameStartedWith(t, game, 3) assertFinishCalledWith(t, game, winner) }) Copy var ( dummyGame = &GameSpy{} ) Copy ./server_test.go:21:38: too many arguments in call to "github.com/quii/learn-go-with-tests/WebSockets/v2".NewPlayerServer have ("github.com/quii/learn-go-with-tests/WebSockets/v2".PlayerStore, "github.com/quii/learn-go-with-tests/WebSockets/v2".Game) want ("github.com/quii/learn-go-with-tests/WebSockets/v2".PlayerStore) Copy func NewPlayerServer(store PlayerStore, game Game) (*PlayerServer, error) { Copy === RUN TestGame/start_a_game_with_3_players_and_declare_Ruth_the_winner --- FAIL: TestGame (0.01s) --- FAIL: TestGame/start_a_game_with_3_players_and_declare_Ruth_the_winner (0.01s) server_test.go:146: wanted Start called with 3 but got 0 server_test.go:147: expected finish called with 'Ruth' but got '' FAIL Copy type PlayerServer struct { store PlayerStore http.Handler template *template.Template game Game } Copy func NewPlayerServer(store PlayerStore, game Game) (*PlayerServer, error) { p := new(PlayerServer) tmpl, err := template.ParseFiles(htmlTemplatePath) if err != nil { return nil, fmt.Errorf("problem opening %s %v", htmlTemplatePath, err) } p.game = game // etc Copy func (p *PlayerServer) webSocket(w http.ResponseWriter, r *http.Request) { conn, _ := wsUpgrader.Upgrade(w, r, nil) _, numberOfPlayersMsg, _ := conn.ReadMessage() numberOfPlayers, _ := strconv.Atoi(string(numberOfPlayersMsg)) p.game.Start(numberOfPlayers, ioutil.Discard) //todo: Don't discard the blinds messages! _, winner, _ := conn.ReadMessage() p.game.Finish(string(winner)) } Copy func main() { db, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666) if err != nil { log.Fatalf("problem opening %s %v", dbFileName, err) } store, err := poker.NewFileSystemPlayerStore(db) if err != nil { log.Fatalf("problem creating file system player store, %v ", err) } game := poker.NewTexasHoldem(poker.BlindAlerterFunc(poker.Alerter), store) server, err := poker.NewPlayerServer(store, game) if err != nil { log.Fatalf("problem creating player server %v", err) } if err := http.ListenAndServe(":5000", server); err != nil { log.Fatalf("could not listen on port 5000 %v", err) } } Copy type playerServerWS struct { *websocket.Conn } func newPlayerServerWS(w http.ResponseWriter, r *http.Request) *playerServerWS { conn, err := wsUpgrader.Upgrade(w, r, nil) if err != nil { log.Printf("problem upgrading connection to WebSockets %v\n", err) } return &playerServerWS{conn} } func (w *playerServerWS) WaitForMsg() string { _, msg, err := w.ReadMessage() if err != nil { log.Printf("error reading from websocket %v\n", err) } return string(msg) } Copy func (p *PlayerServer) webSocket(w http.ResponseWriter, r *http.Request) { ws := newPlayerServerWS(w, r) numberOfPlayersMsg := ws.WaitForMsg() numberOfPlayers, _ := strconv.Atoi(numberOfPlayersMsg) p.game.Start(numberOfPlayers, ioutil.Discard) //todo: Don't discard the blinds messages! winner := ws.WaitForMsg() p.game.Finish(winner) } Copy p.game.Start(numberOfPlayers, ioutil.Discard) //todo: Don't discard the blinds messages! Copy func (p *PlayerServer) webSocket(w http.ResponseWriter, r *http.Request) { ws := newPlayerServerWS(w, r) numberOfPlayersMsg := ws.WaitForMsg() numberOfPlayers, _ := strconv.Atoi(numberOfPlayersMsg) p.game.Start(numberOfPlayers, ws) //etc... Copy ./server.go:71:14: cannot use ws (type *playerServerWS) as type io.Writer in argument to p.game.Start: *playerServerWS does not implement io.Writer (missing Write method) Copy func (w *playerServerWS) Write(p []byte) (n int, err error) { err = w.WriteMessage(websocket.TextMessage, p) if err != nil { return 0, err } return len(p), nil } Copy blindIncrement := time.Duration(5+numberOfPlayers) * time.Second // (rather than a minute) Copy type GameSpy struct { StartCalled bool StartCalledWith int BlindAlert []byte FinishedCalled bool FinishCalledWith string } Copy func (g *GameSpy) Start(numberOfPlayers int, out io.Writer) { g.StartCalled = true g.StartCalledWith = numberOfPlayers out.Write(g.BlindAlert) } Copy t.Run("start a game with 3 players, send some blind alerts down WS and declare Ruth the winner", func(t *testing.T) { wantedBlindAlert := "Blind is 100" winner := "Ruth" game := &GameSpy{BlindAlert: []byte(wantedBlindAlert)} server := httptest.NewServer(mustMakePlayerServer(t, dummyPlayerStore, game)) ws := mustDialWS(t, "ws"+strings.TrimPrefix(server.URL, "http")+"/ws") defer server.Close() defer ws.Close() writeWSMessage(t, ws, "3") writeWSMessage(t, ws, winner) time.Sleep(10 * time.Millisecond) assertGameStartedWith(t, game, 3) assertFinishCalledWith(t, game, winner) _, gotBlindAlert, _ := ws.ReadMessage() if string(gotBlindAlert) != wantedBlindAlert { t.Errorf("got blind alert %q, want %q", string(gotBlindAlert), wantedBlindAlert) } }) Copy func within(t *testing.T, d time.Duration, assert func()) { t.Helper() done := make(chan struct{}, 1) go func() { assert() done <- struct{}{} }() select { case <-time.After(d): t.Error("timed out") case <-done: } } Copy func assertWebsocketGotMsg(t *testing.T, ws *websocket.Conn, want string) { _, msg, _ := ws.ReadMessage() if string(msg) != want { t.Errorf(`got "%s", want "%s"`, string(msg), want) } } Copy t.Run("start a game with 3 players, send some blind alerts down WS and declare Ruth the winner", func(t *testing.T) { wantedBlindAlert := "Blind is 100" winner := "Ruth" game := &GameSpy{BlindAlert: []byte(wantedBlindAlert)} server := httptest.NewServer(mustMakePlayerServer(t, dummyPlayerStore, game)) ws := mustDialWS(t, "ws"+strings.TrimPrefix(server.URL, "http")+"/ws") defer server.Close() defer ws.Close() writeWSMessage(t, ws, "3") writeWSMessage(t, ws, winner) time.Sleep(tenMS) assertGameStartedWith(t, game, 3) assertFinishCalledWith(t, game, winner) within(t, tenMS, func() { assertWebsocketGotMsg(t, ws, wantedBlindAlert) }) }) Copy === RUN TestGame === RUN TestGame/start_a_game_with_3_players,_send_some_blind_alerts_down_WS_and_declare_Ruth_the_winner --- FAIL: TestGame (0.02s) --- FAIL: TestGame/start_a_game_with_3_players,_send_some_blind_alerts_down_WS_and_declare_Ruth_the_winner (0.02s) server_test.go:143: timed out server_test.go:150: got "", want "Blind is 100" Copy func (p *PlayerServer) webSocket(w http.ResponseWriter, r *http.Request) { ws := newPlayerServerWS(w, r) numberOfPlayersMsg := ws.WaitForMsg() numberOfPlayers, _ := strconv.Atoi(numberOfPlayersMsg) p.game.Start(numberOfPlayers, ws) winner := ws.WaitForMsg() p.game.Finish(winner) } Copy func assertFinishCalledWith(t *testing.T, game *GameSpy, winner string) { t.Helper() passed := retryUntil(500*time.Millisecond, func() bool { return game.FinishCalledWith == winner }) if !passed { t.Errorf("expected finish called with %q but got %q", winner, game.FinishCalledWith) } } Copy func retryUntil(d time.Duration, f func() bool) bool { deadline := time.Now().Add(d) for time.Now().Before(deadline) { if f() { return true } } return false } --- # HTTP server | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server.md) . [**You can find all the code for this chapter here**](https://github.com/quii/learn-go-with-tests/tree/master/http-server) You have been asked to create a web server where users can track how many games players have won. * `GET /players/{name}` should return a number indicating the total number of wins * `POST /players/{name}` should record a win for that name, incrementing for every subsequent `POST` We will follow the TDD approach, getting working software as quickly as we can and then making small iterative improvements until we have the solution. By taking this approach we * Keep the problem space small at any given time * Don't go down rabbit holes * If we ever get stuck/lost, doing a revert wouldn't lose loads of work. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#red-green-refactor) Red, green, refactor ------------------------------------------------------------------------------------------------------------------------------------ Throughout this book, we have emphasised the TDD process of write a test & watch it fail (red), write the _minimal_ amount of code to make it work (green) and then refactor. This discipline of writing the minimal amount of code is important in terms of the safety TDD gives you. You should be striving to get out of "red" as soon as you can. Kent Beck describes it as: > Make the test work quickly, committing whatever sins necessary in process. You can commit these sins because you will refactor afterwards backed by the safety of the tests. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#what-if-you-dont-do-this) What if you don't do this? The more changes you make while in red, the more likely you are to add more problems, not covered by tests. The idea is to be iteratively writing useful code with small steps, driven by tests so that you don't fall into a rabbit hole for hours. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#chicken-and-egg) Chicken and egg How can we incrementally build this? We can't `GET` a player without having stored something and it seems hard to know if `POST` has worked without the `GET` endpoint already existing. This is where _mocking_ shines. * `GET` will need a `PlayerStore` _thing_ to get scores for a player. This should be an interface so when we test we can create a simple stub to test our code without needing to have implemented any actual storage code. * For `POST` we can _spy_ on its calls to `PlayerStore` to make sure it stores players correctly. Our implementation of saving won't be coupled to retrieval. * For having some working software quickly we can make a very simple in-memory implementation and then later we can create an implementation backed by whatever storage mechanism we prefer. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-the-test-first) Write the test first -------------------------------------------------------------------------------------------------------------------------------------- We can write a test and make it pass by returning a hard-coded value to get us started. Kent Beck refers this as "Faking it". Once we have a working test we can then write more tests to help us remove that constant. By doing this very small step, we can make the important start of getting an overall project structure working correctly without having to worry too much about our application logic. To create a web server in Go you will typically call [ListenAndServe](https://golang.org/pkg/net/http/#ListenAndServe) . This will start a web server listening on a port, creating a goroutine for every request and running it against a [`Handler`](https://golang.org/pkg/net/http/#Handler) . A type implements the Handler interface by implementing the `ServeHTTP` method which expects two arguments, the first is where we _write our response_ and the second is the HTTP request that was sent to the server. Let's write a test for a function `PlayerServer` that takes in those two arguments. The request sent in will be to get a player's score, which we expect to be `"20"`. In order to test our server, we will need a `Request` to send in and we'll want to _spy_ on what our handler writes to the `ResponseWriter`. * We use `http.NewRequest` to create a request. The first argument is the request's method and the second is the request's path. The `nil` argument refers to the request's body, which we don't need to set in this case. * `net/http/httptest` has a spy already made for us called `ResponseRecorder` so we can use that. It has many helpful methods to inspect what has been written as a response. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#try-to-run-the-test) Try to run the test ------------------------------------------------------------------------------------------------------------------------------------ `./server_test.go:13:2: undefined: PlayerServer` [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- The compiler is here to help, just listen to it. Define `PlayerServer` Try again Add the arguments to our function The code now compiles and the test fails [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-enough-code-to-make-it-pass) Write enough code to make it pass ---------------------------------------------------------------------------------------------------------------------------------------------------------------- From the DI chapter, we touched on HTTP servers with a `Greet` function. We learned that net/http's `ResponseWriter` also implements io `Writer` so we can use `fmt.Fprint` to send strings as HTTP responses. The test should now pass. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#complete-the-scaffolding) Complete the scaffolding ---------------------------------------------------------------------------------------------------------------------------------------------- We want to wire this up into an application. This is important because * We'll have _actual working software_, we don't want to write tests for the sake of it, it's good to see the code in action. * As we refactor our code, it's likely we will change the structure of the program. We want to make sure this is reflected in our application too as part of the incremental approach. Create a new file for our application and put this code in. So far all of our application code has been in one file, however, this isn't best practice for larger projects where you'll want to separate things into different files. To run this, do `go build` which will take all the `.go` files in the directory and build you a program. You can then execute it with `./myprogram`. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#http.handlerfunc) `http.HandlerFunc` Earlier we explored that the `Handler` interface is what we need to implement in order to make a server. _Typically_ we do that by creating a `struct` and make it implement the interface by implementing its own ServeHTTP method. However the use-case for structs is for holding data but _currently_ we have no state, so it doesn't feel right to be creating one. [HandlerFunc](https://golang.org/pkg/net/http/#HandlerFunc) lets us avoid this. > The HandlerFunc type is an adapter to allow the use of ordinary functions as HTTP handlers. If f is a function with the appropriate signature, HandlerFunc(f) is a Handler that calls f. From the documentation, we see that type `HandlerFunc` has already implemented the `ServeHTTP` method. By type casting our `PlayerServer` function with it, we have now implemented the required `Handler`. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#http.listenandserve-5000) `http.ListenAndServe(":5000"...)` `ListenAndServe` takes a port to listen on a `Handler`. If the port is already being listened to it will return an `error` so we are using an `if` statement to capture that scenario and log the problem to the user. What we're going to do now is write _another_ test to force us into making a positive change to try and move away from the hard-coded value. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-the-test-first-1) Write the test first ---------------------------------------------------------------------------------------------------------------------------------------- We'll add another subtest to our suite which tries to get the score of a different player, which will break our hard-coded approach. You may have been thinking > Surely we need some kind of concept of storage to control which player gets what score. It's weird that the values seem so arbitrary in our tests. Remember we are just trying to take as small as steps as reasonably possible, so we're just trying to break the constant for now. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#try-to-run-the-test-1) Try to run the test -------------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-enough-code-to-make-it-pass-1) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------------ This test has forced us to actually look at the request's URL and make a decision. So whilst in our heads, we may have been worrying about player stores and interfaces the next logical step actually seems to be about _routing_. If we had started with the store code the amount of changes we'd have to do would be very large compared to this. **This is a smaller step towards our final goal and was driven by tests**. We're resisting the temptation to use any routing libraries right now, just the smallest step to get our test passing. `r.URL.Path` returns the path of the request which we can then use [`strings.TrimPrefix`](https://golang.org/pkg/strings/#TrimPrefix) to trim away `/players/` to get the requested player. It's not very robust but will do the trick for now. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#refactor) Refactor -------------------------------------------------------------------------------------------------------------- We can simplify the `PlayerServer` by separating out the score retrieval into a function And we can DRY up some of the code in the tests by making some helpers However, we still shouldn't be happy. It doesn't feel right that our server knows the scores. Our refactoring has made it pretty clear what to do. We moved the score calculation out of the main body of our handler into a function `GetPlayerScore`. This feels like the right place to separate the concerns using interfaces. Let's move our function we re-factored to be an interface instead For our `PlayerServer` to be able to use a `PlayerStore`, it will need a reference to one. Now feels like the right time to change our architecture so that our `PlayerServer` is now a `struct`. Finally, we will now implement the `Handler` interface by adding a method to our new struct and putting in our existing handler code. The only other change is we now call our `store.GetPlayerScore` to get the score, rather than the local function we defined (which we can now delete). Here is the full code listing of our server ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#fix-the-issues) Fix the issues This was quite a few changes and we know our tests and application will no longer compile, but just relax and let the compiler work through it. `./main.go:9:58: type PlayerServer is not an expression` We need to change our tests to instead create a new instance of our `PlayerServer` and then call its method `ServeHTTP`. Notice we're still not worrying about making stores _just yet_, we just want the compiler passing as soon as we can. You should be in the habit of prioritising having code that compiles and then code that passes the tests. By adding more functionality (like stub stores) whilst the code isn't compiling, we are opening ourselves up to potentially _more_ compilation problems. Now `main.go` won't compile for the same reason. Finally, everything is compiling but the tests are failing This is because we have not passed in a `PlayerStore` in our tests. We'll need to make a stub one up. A `map` is a quick and easy way of making a stub key/value store for our tests. Now let's create one of these stores for our tests and send it into our `PlayerServer`. Our tests now pass and are looking better. The _intent_ behind our code is clearer now due to the introduction of the store. We're telling the reader that because we have _this data in a_ `_PlayerStore_` that when you use it with a `PlayerServer` you should get the following responses. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#run-the-application) Run the application Now our tests are passing the last thing we need to do to complete this refactor is to check if our application is working. The program should start up but you'll get a horrible response if you try and hit the server at `http://localhost:5000/players/Pepper`. The reason for this is that we have not passed in a `PlayerStore`. We'll need to make an implementation of one, but that's difficult right now as we're not storing any meaningful data so it'll have to be hard-coded for the time being. If you run `go build` again and hit the same URL you should get `"123"`. Not great, but until we store data that's the best we can do. We have a few options as to what to do next * Handle the scenario where the player doesn't exist * Handle the `POST /players/{name}` scenario * It didn't feel great that our main application was starting up but not actually working. We had to manually test to see the problem. Whilst the `POST` scenario gets us closer to the "happy path", I feel it'll be easier to tackle the missing player scenario first as we're in that context already. We'll get to the rest later. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-the-test-first-2) Write the test first ---------------------------------------------------------------------------------------------------------------------------------------- Add a missing player scenario to our existing suite [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#try-to-run-the-test-2) Try to run the test -------------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-enough-code-to-make-it-pass-2) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------------ Sometimes I heavily roll my eyes when TDD advocates say "make sure you just write the minimal amount of code to make it pass" as it can feel very pedantic. But this scenario illustrates the example well. I have done the bare minimum (knowing it is not correct), which is write a `StatusNotFound` on **all responses** but all our tests are passing! **By doing the bare minimum to make the tests pass it can highlight gaps in your tests**. In our case, we are not asserting that we should be getting a `StatusOK` when players _do_ exist in the store. Update the other two tests to assert on the status and fix the code. Here are the new tests We're checking the status in all our tests now so I made a helper `assertStatus` to facilitate that. Now our first two tests fail because of the 404 instead of 200, so we can fix `PlayerServer` to only return not found if the score is 0. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#storing-scores) Storing scores Now that we can retrieve scores from a store it now makes sense to be able to store new scores. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-the-test-first-3) Write the test first ---------------------------------------------------------------------------------------------------------------------------------------- For a start let's just check we get the correct status code if we hit the particular route with POST. This lets us drive out the functionality of accepting a different kind of request and handling it differently to `GET /players/{name}`. Once this works we can then start asserting on our handler's interaction with the store. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#try-to-run-the-test-3) Try to run the test -------------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-enough-code-to-make-it-pass-3) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------------ Remember we are deliberately committing sins, so an `if` statement based on the request's method will do the trick. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#refactor-1) Refactor ---------------------------------------------------------------------------------------------------------------- The handler is looking a bit muddled now. Let's break the code up to make it easier to follow and isolate the different functionality into new functions. This makes the routing aspect of `ServeHTTP` a bit clearer and means our next iterations on storing can just be inside `processWin`. Next, we want to check that when we do our `POST /players/{name}` that our `PlayerStore` is told to record the win. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-the-test-first-4) Write the test first ---------------------------------------------------------------------------------------------------------------------------------------- We can accomplish this by extending our `StubPlayerStore` with a new `RecordWin` method and then spy on its invocations. Now extend our test to check the number of invocations for a start [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#try-to-run-the-test-4) Try to run the test -------------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) Write the minimal amount of code for the test to run and check the failing test output ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- We need to update our code where we create a `StubPlayerStore` as we've added a new field [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-enough-code-to-make-it-pass-4) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------------ As we're only asserting the number of calls rather than the specific values it makes our initial iteration a little smaller. We need to update `PlayerServer`'s idea of what a `PlayerStore` is by changing the interface if we're going to be able to call `RecordWin`. By doing this `main` no longer compiles The compiler tells us what's wrong. Let's update `InMemoryPlayerStore` to have that method. Try and run the tests and we should be back to compiling code - but the test is still failing. Now that `PlayerStore` has `RecordWin` we can call it within our `PlayerServer` Run the tests and it should be passing! Obviously `"Bob"` isn't exactly what we want to send to `RecordWin`, so let's further refine the test. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-the-test-first-5) Write the test first ---------------------------------------------------------------------------------------------------------------------------------------- Now that we know there is one element in our `winCalls` slice we can safely reference the first one and check it is equal to `player`. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#try-to-run-the-test-5) Try to run the test -------------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-enough-code-to-make-it-pass-5) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------------ We changed `processWin` to take `http.Request` so we can look at the URL to extract the player's name. Once we have that we can call our `store` with the correct value to make the test pass. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#refactor-2) Refactor ---------------------------------------------------------------------------------------------------------------- We can DRY up this code a bit as we're extracting the player name the same way in two places Even though our tests are passing we don't really have working software. If you try and run `main` and use the software as intended it doesn't work because we haven't got round to implementing `PlayerStore` correctly. This is fine though; by focusing on our handler we have identified the interface that we need, rather than trying to design it up-front. We _could_ start writing some tests around our `InMemoryPlayerStore` but it's only here temporarily until we implement a more robust way of persisting player scores (i.e. a database). What we'll do for now is write an _integration test_ between our `PlayerServer` and `InMemoryPlayerStore` to finish off the functionality. This will let us get to our goal of being confident our application is working, without having to directly test `InMemoryPlayerStore`. Not only that, but when we get around to implementing `PlayerStore` with a database, we can test that implementation with the same integration test. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#integration-tests) Integration tests Integration tests can be useful for testing that larger areas of your system work but you must bear in mind: * They are harder to write * When they fail, it can be difficult to know why (usually it's a bug within a component of the integration test) and so can be harder to fix * They are sometimes slower to run (as they often are used with "real" components, like a database) For that reason, it is recommended that you research _The Test Pyramid_. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-the-test-first-6) Write the test first ---------------------------------------------------------------------------------------------------------------------------------------- In the interest of brevity, I am going to show you the final refactored integration test. * We are creating our two components we are trying to integrate with: `InMemoryPlayerStore` and `PlayerServer`. * We then fire off 3 requests to record 3 wins for `player`. We're not too concerned about the status codes in this test as it's not relevant to whether they are integrating well. * The next response we do care about (so we store a variable `response`) because we are going to try and get the `player`'s score. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#try-to-run-the-test-6) Try to run the test -------------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-enough-code-to-make-it-pass-6) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------------ I am going to take some liberties here and write more code than you may be comfortable with without writing a test. _This is allowed_! We still have a test checking things should be working correctly but it is not around the specific unit we're working with (`InMemoryPlayerStore`). If I were to get stuck in this scenario, I would revert my changes back to the failing test and then write more specific unit tests around `InMemoryPlayerStore` to help me drive out a solution. * We need to store the data so I've added a `map[string]int` to the `InMemoryPlayerStore` struct * For convenience I've made `NewInMemoryPlayerStore` to initialise the store, and updated the integration test to use it (`store := NewInMemoryPlayerStore()`) * The rest of the code is just wrapping around the `map` The integration test passes, now we just need to change `main` to use `NewInMemoryPlayerStore()` Build it, run it and then use `curl` to test it out. * Run this a few times, change the player names if you like `curl -X POST http://localhost:5000/players/Pepper` * Check scores with `curl http://localhost:5000/players/Pepper` Great! You've made a REST-ish service. To take this forward you'd want to pick a data store to persist the scores longer than the length of time the program runs. * Pick a store (Bolt? Mongo? Postgres? File system?) * Make `PostgresPlayerStore` implement `PlayerStore` * TDD the functionality so you're sure it works * Plug it into the integration test, check it's still ok * Finally plug it into `main` [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#refactor-3) Refactor ---------------------------------------------------------------------------------------------------------------- We are almost there! Lets take some effort to prevent concurrency errors like these By adding mutexes, we enforce concurrency safety especially for the counter in our `RecordWin` function. Read more about mutexes in the sync chapter. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#wrapping-up) Wrapping up -------------------------------------------------------------------------------------------------------------------- ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#http.handler) `http.Handler` * Implement this interface to create web servers * Use `http.HandlerFunc` to turn ordinary functions into `http.Handler`s * Use `httptest.NewRecorder` to pass in as a `ResponseWriter` to let you spy on the responses your handler sends * Use `http.NewRequest` to construct the requests you expect to come in to your system ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#interfaces-mocking-and-di) Interfaces, Mocking and DI * Lets you iteratively build the system up in smaller chunks * Allows you to develop a handler that needs a storage without needing actual storage * TDD to drive out the interfaces you need ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#commit-sins-then-refactor-and-then-commit-to-source-control) Commit sins, then refactor (and then commit to source control) * You need to treat having failing compilation or failing tests as a red situation that you need to get out of as soon as you can. * Write just the necessary code to get there. _Then_ refactor and make the code nice. * By trying to do too many changes whilst the code isn't compiling or the tests are failing puts you at risk of compounding the problems. * Sticking to this approach forces you to write small tests, which means small changes, which helps keep working on complex systems manageable. [PreviousIntro](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/app-intro) [NextJSON, routing and embedding](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json) Last updated 6 years ago * [Red, green, refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#red-green-refactor) * [What if you don't do this?](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#what-if-you-dont-do-this) * [Chicken and egg](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#chicken-and-egg) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-the-test-first) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#try-to-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-enough-code-to-make-it-pass) * [Complete the scaffolding](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#complete-the-scaffolding) * [http.HandlerFunc](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#http.handlerfunc) * [http.ListenAndServe(":5000"...)](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#http.listenandserve-5000) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-the-test-first-1) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#try-to-run-the-test-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-enough-code-to-make-it-pass-1) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#refactor) * [Fix the issues](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#fix-the-issues) * [Run the application](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#run-the-application) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-the-test-first-2) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#try-to-run-the-test-2) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-enough-code-to-make-it-pass-2) * [Storing scores](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#storing-scores) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-the-test-first-3) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#try-to-run-the-test-3) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-enough-code-to-make-it-pass-3) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#refactor-1) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-the-test-first-4) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#try-to-run-the-test-4) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-enough-code-to-make-it-pass-4) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-the-test-first-5) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#try-to-run-the-test-5) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-enough-code-to-make-it-pass-5) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#refactor-2) * [Integration tests](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#integration-tests) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-the-test-first-6) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#try-to-run-the-test-6) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#write-enough-code-to-make-it-pass-6) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#refactor-3) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#wrapping-up) * [http.Handler](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#http.handler) * [Interfaces, Mocking and DI](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#interfaces-mocking-and-di) * [Commit sins, then refactor (and then commit to source control)](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/http-server#commit-sins-then-refactor-and-then-commit-to-source-control) Copy func ListenAndServe(addr string, handler Handler) error Copy type Handler interface { ServeHTTP(ResponseWriter, *Request) } Copy func TestGETPlayers(t *testing.T) { t.Run("returns Pepper's score", func(t *testing.T) { request, _ := http.NewRequest(http.MethodGet, "/players/Pepper", nil) response := httptest.NewRecorder() PlayerServer(response, request) got := response.Body.String() want := "20" if got != want { t.Errorf("got %q, want %q", got, want) } }) } Copy func PlayerServer() {} Copy ./server_test.go:13:14: too many arguments in call to PlayerServer have (*httptest.ResponseRecorder, *http.Request) want () Copy import "net/http" func PlayerServer(w http.ResponseWriter, r *http.Request) { } Copy === RUN TestGETPlayers/returns_Pepper's_score --- FAIL: TestGETPlayers/returns_Pepper's_score (0.00s) server_test.go:20: got '', want '20' Copy func PlayerServer(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "20") } Copy package main import ( "log" "net/http" ) func main() { handler := http.HandlerFunc(PlayerServer) if err := http.ListenAndServe(":5000", handler); err != nil { log.Fatalf("could not listen on port 5000 %v", err) } } Copy type HandlerFunc func(ResponseWriter, *Request) Copy t.Run("returns Floyd's score", func(t *testing.T) { request, _ := http.NewRequest(http.MethodGet, "/players/Floyd", nil) response := httptest.NewRecorder() PlayerServer(response, request) got := response.Body.String() want := "10" if got != want { t.Errorf("got %q, want %q", got, want) } }) Copy === RUN TestGETPlayers/returns_Pepper's_score --- PASS: TestGETPlayers/returns_Pepper's_score (0.00s) === RUN TestGETPlayers/returns_Floyd's_score --- FAIL: TestGETPlayers/returns_Floyd's_score (0.00s) server_test.go:34: got '20', want '10' Copy func PlayerServer(w http.ResponseWriter, r *http.Request) { player := strings.TrimPrefix(r.URL.Path, "/players/") if player == "Pepper" { fmt.Fprint(w, "20") return } if player == "Floyd" { fmt.Fprint(w, "10") return } } Copy func PlayerServer(w http.ResponseWriter, r *http.Request) { player := strings.TrimPrefix(r.URL.Path, "/players/") fmt.Fprint(w, GetPlayerScore(player)) } func GetPlayerScore(name string) string { if name == "Pepper" { return "20" } if name == "Floyd" { return "10" } return "" } Copy func TestGETPlayers(t *testing.T) { t.Run("returns Pepper's score", func(t *testing.T) { request := newGetScoreRequest("Pepper") response := httptest.NewRecorder() PlayerServer(response, request) assertResponseBody(t, response.Body.String(), "20") }) t.Run("returns Floyd's score", func(t *testing.T) { request := newGetScoreRequest("Floyd") response := httptest.NewRecorder() PlayerServer(response, request) assertResponseBody(t, response.Body.String(), "10") }) } func newGetScoreRequest(name string) *http.Request { req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("/players/%s", name), nil) return req } func assertResponseBody(t *testing.T, got, want string) { t.Helper() if got != want { t.Errorf("response body is wrong, got %q want %q", got, want) } } Copy type PlayerStore interface { GetPlayerScore(name string) int } Copy type PlayerServer struct { store PlayerStore } Copy func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { player := strings.TrimPrefix(r.URL.Path, "/players/") fmt.Fprint(w, p.store.GetPlayerScore(player)) } Copy type PlayerStore interface { GetPlayerScore(name string) int } type PlayerServer struct { store PlayerStore } func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { player := strings.TrimPrefix(r.URL.Path, "/players/") fmt.Fprint(w, p.store.GetPlayerScore(player)) } Copy func TestGETPlayers(t *testing.T) { server := &PlayerServer{} t.Run("returns Pepper's score", func(t *testing.T) { request := newGetScoreRequest("Pepper") response := httptest.NewRecorder() server.ServeHTTP(response, request) assertResponseBody(t, response.Body.String(), "20") }) t.Run("returns Floyd's score", func(t *testing.T) { request := newGetScoreRequest("Floyd") response := httptest.NewRecorder() server.ServeHTTP(response, request) assertResponseBody(t, response.Body.String(), "10") }) } Copy func main() { server := &PlayerServer{} if err := http.ListenAndServe(":5000", server); err != nil { log.Fatalf("could not listen on port 5000 %v", err) } } Copy === RUN TestGETPlayers/returns_the_Pepper's_score panic: runtime error: invalid memory address or nil pointer dereference [recovered] panic: runtime error: invalid memory address or nil pointer dereference Copy type StubPlayerStore struct { scores map[string]int } func (s *StubPlayerStore) GetPlayerScore(name string) int { score := s.scores[name] return score } Copy func TestGETPlayers(t *testing.T) { store := StubPlayerStore{ map[string]int{ "Pepper": 20, "Floyd": 10, }, } server := &PlayerServer{&store} t.Run("returns Pepper's score", func(t *testing.T) { request := newGetScoreRequest("Pepper") response := httptest.NewRecorder() server.ServeHTTP(response, request) assertResponseBody(t, response.Body.String(), "20") }) t.Run("returns Floyd's score", func(t *testing.T) { request := newGetScoreRequest("Floyd") response := httptest.NewRecorder() server.ServeHTTP(response, request) assertResponseBody(t, response.Body.String(), "10") }) } Copy type InMemoryPlayerStore struct{} func (i *InMemoryPlayerStore) GetPlayerScore(name string) int { return 123 } func main() { server := &PlayerServer{&InMemoryPlayerStore{}} if err := http.ListenAndServe(":5000", server); err != nil { log.Fatalf("could not listen on port 5000 %v", err) } } Copy t.Run("returns 404 on missing players", func(t *testing.T) { request := newGetScoreRequest("Apollo") response := httptest.NewRecorder() server.ServeHTTP(response, request) got := response.Code want := http.StatusNotFound if got != want { t.Errorf("got status %d want %d", got, want) } }) Copy === RUN TestGETPlayers/returns_404_on_missing_players --- FAIL: TestGETPlayers/returns_404_on_missing_players (0.00s) server_test.go:56: got status 200 want 404 Copy func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { player := strings.TrimPrefix(r.URL.Path, "/players/") w.WriteHeader(http.StatusNotFound) fmt.Fprint(w, p.store.GetPlayerScore(player)) } Copy func TestGETPlayers(t *testing.T) { store := StubPlayerStore{ map[string]int{ "Pepper": 20, "Floyd": 10, }, } server := &PlayerServer{&store} t.Run("returns Pepper's score", func(t *testing.T) { request := newGetScoreRequest("Pepper") response := httptest.NewRecorder() server.ServeHTTP(response, request) assertStatus(t, response.Code, http.StatusOK) assertResponseBody(t, response.Body.String(), "20") }) t.Run("returns Floyd's score", func(t *testing.T) { request := newGetScoreRequest("Floyd") response := httptest.NewRecorder() server.ServeHTTP(response, request) assertStatus(t, response.Code, http.StatusOK) assertResponseBody(t, response.Body.String(), "10") }) t.Run("returns 404 on missing players", func(t *testing.T) { request := newGetScoreRequest("Apollo") response := httptest.NewRecorder() server.ServeHTTP(response, request) assertStatus(t, response.Code, http.StatusNotFound) }) } func assertStatus(t *testing.T, got, want int) { t.Helper() if got != want { t.Errorf("did not get correct status, got %d, want %d", got, want) } } func newGetScoreRequest(name string) *http.Request { req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("/players/%s", name), nil) return req } func assertResponseBody(t *testing.T, got, want string) { t.Helper() if got != want { t.Errorf("response body is wrong, got %q want %q", got, want) } } Copy func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { player := strings.TrimPrefix(r.URL.Path, "/players/") score := p.store.GetPlayerScore(player) if score == 0 { w.WriteHeader(http.StatusNotFound) } fmt.Fprint(w, score) } Copy func TestStoreWins(t *testing.T) { store := StubPlayerStore{ map[string]int{}, } server := &PlayerServer{&store} t.Run("it returns accepted on POST", func(t *testing.T) { request, _ := http.NewRequest(http.MethodPost, "/players/Pepper", nil) response := httptest.NewRecorder() server.ServeHTTP(response, request) assertStatus(t, response.Code, http.StatusAccepted) }) } Copy === RUN TestStoreWins/it_returns_accepted_on_POST --- FAIL: TestStoreWins/it_returns_accepted_on_POST (0.00s) server_test.go:70: did not get correct status, got 404, want 202 Copy func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost { w.WriteHeader(http.StatusAccepted) return } player := strings.TrimPrefix(r.URL.Path, "/players/") score := p.store.GetPlayerScore(player) if score == 0 { w.WriteHeader(http.StatusNotFound) } fmt.Fprint(w, score) } Copy func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPost: p.processWin(w) case http.MethodGet: p.showScore(w, r) } } func (p *PlayerServer) showScore(w http.ResponseWriter, r *http.Request) { player := strings.TrimPrefix(r.URL.Path, "/players/") score := p.store.GetPlayerScore(player) if score == 0 { w.WriteHeader(http.StatusNotFound) } fmt.Fprint(w, score) } func (p *PlayerServer) processWin(w http.ResponseWriter) { w.WriteHeader(http.StatusAccepted) } Copy type StubPlayerStore struct { scores map[string]int winCalls []string } func (s *StubPlayerStore) GetPlayerScore(name string) int { score := s.scores[name] return score } func (s *StubPlayerStore) RecordWin(name string) { s.winCalls = append(s.winCalls, name) } Copy func TestStoreWins(t *testing.T) { store := StubPlayerStore{ map[string]int{}, } server := &PlayerServer{&store} t.Run("it records wins when POST", func(t *testing.T) { request := newPostWinRequest("Pepper") response := httptest.NewRecorder() server.ServeHTTP(response, request) assertStatus(t, response.Code, http.StatusAccepted) if len(store.winCalls) != 1 { t.Errorf("got %d calls to RecordWin want %d", len(store.winCalls), 1) } }) } func newPostWinRequest(name string) *http.Request { req, _ := http.NewRequest(http.MethodPost, fmt.Sprintf("/players/%s", name), nil) return req } Copy ./server_test.go:26:20: too few values in struct initializer ./server_test.go:65:20: too few values in struct initializer Copy store := StubPlayerStore{ map[string]int{}, nil, } Copy --- FAIL: TestStoreWins (0.00s) --- FAIL: TestStoreWins/it_records_wins_when_POST (0.00s) server_test.go:80: got 0 calls to RecordWin want 1 Copy type PlayerStore interface { GetPlayerScore(name string) int RecordWin(name string) } Copy ./main.go:17:46: cannot use InMemoryPlayerStore literal (type *InMemoryPlayerStore) as type PlayerStore in field value: *InMemoryPlayerStore does not implement PlayerStore (missing RecordWin method) Copy type InMemoryPlayerStore struct{} func (i *InMemoryPlayerStore) RecordWin(name string) {} Copy func (p *PlayerServer) processWin(w http.ResponseWriter) { p.store.RecordWin("Bob") w.WriteHeader(http.StatusAccepted) } Copy t.Run("it records wins on POST", func(t *testing.T) { player := "Pepper" request := newPostWinRequest(player) response := httptest.NewRecorder() server.ServeHTTP(response, request) assertStatus(t, response.Code, http.StatusAccepted) if len(store.winCalls) != 1 { t.Fatalf("got %d calls to RecordWin want %d", len(store.winCalls), 1) } if store.winCalls[0] != player { t.Errorf("did not store correct winner got %q want %q", store.winCalls[0], player) } }) Copy === RUN TestStoreWins/it_records_wins_on_POST --- FAIL: TestStoreWins/it_records_wins_on_POST (0.00s) server_test.go:86: did not store correct winner got 'Bob' want 'Pepper' Copy func (p *PlayerServer) processWin(w http.ResponseWriter, r *http.Request) { player := strings.TrimPrefix(r.URL.Path, "/players/") p.store.RecordWin(player) w.WriteHeader(http.StatusAccepted) } Copy func (p *PlayerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { player := strings.TrimPrefix(r.URL.Path, "/players/") switch r.Method { case http.MethodPost: p.processWin(w, player) case http.MethodGet: p.showScore(w, player) } } func (p *PlayerServer) showScore(w http.ResponseWriter, player string) { score := p.store.GetPlayerScore(player) if score == 0 { w.WriteHeader(http.StatusNotFound) } fmt.Fprint(w, score) } func (p *PlayerServer) processWin(w http.ResponseWriter, player string) { p.store.RecordWin(player) w.WriteHeader(http.StatusAccepted) } Copy func TestRecordingWinsAndRetrievingThem(t *testing.T) { store := InMemoryPlayerStore{} server := PlayerServer{&store} player := "Pepper" server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player)) server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player)) server.ServeHTTP(httptest.NewRecorder(), newPostWinRequest(player)) response := httptest.NewRecorder() server.ServeHTTP(response, newGetScoreRequest(player)) assertStatus(t, response.Code, http.StatusOK) assertResponseBody(t, response.Body.String(), "3") } Copy --- FAIL: TestRecordingWinsAndRetrievingThem (0.00s) server_integration_test.go:24: response body is wrong, got '123' want '3' Copy func NewInMemoryPlayerStore() *InMemoryPlayerStore { return &InMemoryPlayerStore{map[string]int{}} } type InMemoryPlayerStore struct { store map[string]int } func (i *InMemoryPlayerStore) RecordWin(name string) { i.store[name]++ } func (i *InMemoryPlayerStore) GetPlayerScore(name string) int { return i.store[name] } Copy package main import ( "log" "net/http" ) func main() { server := &PlayerServer{NewInMemoryPlayerStore()} if err := http.ListenAndServe(":5000", server); err != nil { log.Fatalf("could not listen on port 5000 %v", err) } } Copy fatal error: concurrent map read and map write --- # Intro to property based tests | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals.md) . [**You can find all the code for this chapter here**](https://github.com/quii/learn-go-with-tests/tree/master/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. We are going to write a function which converts an [Arabic number](https://en.wikipedia.org/wiki/Arabic_numerals) (numbers 0 to 9) to a Roman Numeral. If you haven't heard of [Roman Numerals](https://en.wikipedia.org/wiki/Roman_numerals) they are how the Romans wrote down numbers. You build them by sticking symbols together and those symbols represent numbers So `I` is "one". `III` is three. Seems easy but there's a few interesting rules. `V` means five, but `IV` is 4 (not `IIII`). `MCMLXXXIV` is 1984. That looks complicated and it's hard to imagine how we can write code to figure this out right from the start. As this book stresses, a key skill for software developers is to try and identify "thin vertical slices" of _useful_ functionality and then **iterating**. The TDD workflow helps facilitate iterative development. So rather than 1984, let's start with 1. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-the-test-first) Write the test first ------------------------------------------------------------------------------------------------------------------------------------ Copy func TestRomanNumerals(t *testing.T) { got := ConvertToRoman(1) want := "I" if got != want { t.Errorf("got %q, want %q", got, want) } } If you've got this far in the book this is hopefully feeling very boring and routine to you. That's a good thing. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#try-to-run-the-test) Try to run the test ---------------------------------------------------------------------------------------------------------------------------------- `./numeral_test.go:6:9: undefined: ConvertToRoman` Let the compiler guide the way [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Create our function but don't make the test pass yet, always make sure the tests fails how you expect It should run now [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-enough-code-to-make-it-pass) Write enough code to make it pass -------------------------------------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#refactor) Refactor ------------------------------------------------------------------------------------------------------------ Not much to refactor yet. _I know_ it feels weird just to hard-code the result but with TDD we want to stay out of "red" for as long as possible. It may _feel_ like we haven't accomplished much but we've defined our API and got a test capturing one of our rules; even if the "real" code is pretty dumb. Now use that uneasy feeling to write a new test to force us to write slightly less dumb code. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-the-test-first-1) Write the test first -------------------------------------------------------------------------------------------------------------------------------------- We can use subtests to nicely group our tests [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#try-to-run-the-test-1) Try to run the test ------------------------------------------------------------------------------------------------------------------------------------ Not much surprise there [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-enough-code-to-make-it-pass-1) Write enough code to make it pass ---------------------------------------------------------------------------------------------------------------------------------------------------------------- Yup, it still feels like we're not actually tackling the problem. So we need to write more tests to drive us forward. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#refactor-1) Refactor -------------------------------------------------------------------------------------------------------------- We have some repetition in our tests. When you're testing something which feels like it's a matter of "given input X, we expect Y" you should probably use table based tests. We can now easily add more cases without having to write any more test boilerplate. Let's push on and go for 3 [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-the-test-first-2) Write the test first -------------------------------------------------------------------------------------------------------------------------------------- Add the following to our cases [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#try-to-run-the-test-2) Try to run the test ------------------------------------------------------------------------------------------------------------------------------------ [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-enough-code-to-make-it-pass-2) Write enough code to make it pass ---------------------------------------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#refactor-2) Refactor -------------------------------------------------------------------------------------------------------------- OK so I'm starting to not enjoy these if statements and if you look at the code hard enough you can see that we're building a string of `I` based on the size of `arabic`. We "know" that for more complicated numbers we will be doing some kind of arithmetic and string concatenation. Let's try a refactor with these thoughts in mind, it _might not_ be suitable for the end solution but that's OK. We can always throw our code away and start afresh with the tests we have to guide us. You may not have used [`strings.Builder`](https://golang.org/pkg/strings/#Builder) before > A Builder is used to efficiently build a string using Write methods. It minimizes memory copying. Normally I wouldn't bother with such optimisations until I have an actual performance problem but the amount of code is not much larger than a "manual" appending on a string so we may as well use the faster approach. The code looks better to me and describes the domain _as we know it right now_. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#the-romans-were-into-dry-too) The Romans were into DRY too... Things start getting more complicated now. The Romans in their wisdom thought repeating characters would become hard to read and count. So a rule with Roman Numerals is you can't have the same character repeated more than 3 times in a row. Instead you take the next highest symbol and then "subtract" by putting a symbol to the left of it. Not all symbols can be used as subtractors; only I (1), X (10) and C (100). For example `5` in Roman Numerals is `V`. To create 4 you do not do `IIII`, instead you do `IV`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-the-test-first-3) Write the test first -------------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#try-to-run-the-test-3) Try to run the test ------------------------------------------------------------------------------------------------------------------------------------ [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-enough-code-to-make-it-pass-3) Write enough code to make it pass ---------------------------------------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#refactor-3) Refactor -------------------------------------------------------------------------------------------------------------- I don't "like" that we have broken our string building pattern and I want to carry on with it. In order for 4 to "fit" with my current thinking I now count down from the Arabic number, adding symbols to our string as we progress. Not sure if this will work in the long run but let's see! Let's make 5 work [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-the-test-first-4) Write the test first -------------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#try-to-run-the-test-4) Try to run the test ------------------------------------------------------------------------------------------------------------------------------------ [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-enough-code-to-make-it-pass-4) Write enough code to make it pass ---------------------------------------------------------------------------------------------------------------------------------------------------------------- Just copy the approach we did for 4 [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#refactor-4) Refactor -------------------------------------------------------------------------------------------------------------- Repetition in loops like this are usually a sign of an abstraction waiting to be called out. Short-circuiting loops can be an effective tool for readability but it could also be telling you something else. We are looping over our Arabic number and if we hit certain symbols we are calling `break` but what we are _really_ doing is subtracting over `i` in a ham-fisted manner. * Given the signals I'm reading from our code, driven from our tests of some very basic scenarios I can see that to build a Roman Numeral I need to subtract from `arabic` as I apply symbols * The `for` loop no longer relies on an `i` and instead we will keep building our string until we have subtracted enough symbols away from `arabic`. I'm pretty sure this approach will be valid for 6 (VI), 7 (VII) and 8 (VIII) too. Nonetheless add the cases in to our test suite and check (I won't include the code for brevity, check the github for samples if you're unsure). 9 follows the same rule as 4 in that we should subtract `I` from the representation of the following number. 10 is represented in Roman Numerals with `X`; so therefore 9 should be `IX`. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-the-test-first-5) Write the test first -------------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#try-to-run-the-test-5) Try to run the test ------------------------------------------------------------------------------------------------------------------------------------ [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-enough-code-to-make-it-pass-5) Write enough code to make it pass ---------------------------------------------------------------------------------------------------------------------------------------------------------------- We should be able to adopt the same approach as before [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#refactor-5) Refactor -------------------------------------------------------------------------------------------------------------- It _feels_ like the code is still telling us there's a refactor somewhere but it's not totally obvious to me, so let's keep going. I'll skip the code for this too, but add to your test cases a test for `10` which should be `X` and make it pass before reading on. Here are a few tests I added as I'm confident up to 39 our code should work If you've ever done OO programming, you'll know that you should view `switch` statements with a bit of suspicion. Usually you are capturing a concept or data inside some imperative code when in fact it could be captured in a class structure instead. Go isn't strictly OO but that doesn't mean we ignore the lessons OO offers entirely (as much as some would like to tell you). Our switch statement is describing some truths about Roman Numerals along with behaviour. We can refactor this by decoupling the data from the behaviour. This feels much better. We've declared some rules around the numerals as data rather than hidden in an algorithm and we can see how we just work through the Arabic number, trying to add symbols to our result if they fit. Does this abstraction work for bigger numbers? Extend the test suite so it works for the Roman number for 50 which is `L`. Here are some test cases, try and make them pass. Need help? You can see what symbols to add in [this gist](https://gist.github.com/pamelafox/6c7b948213ba55332d86efd0f0b037de) . [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#and-the-rest) And the rest! --------------------------------------------------------------------------------------------------------------------- Here are the remaining symbols Arabic Roman 100 C 500 D 1000 M Take the same approach for the remaining symbols, it should just be a matter of adding data to both the tests and our array of symbols. Does your code work for `1984`: `MCMLXXXIV` ? Here is my final test suite * I removed `description` as I felt the _data_ described enough of the information. * I added a few other edge cases I found just to give me a little more confidence. With table based tests this is very cheap to do. I didn't change the algorithm, all I had to do was update the `allRomanNumerals` array. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#parsing-roman-numerals) Parsing Roman Numerals ---------------------------------------------------------------------------------------------------------------------------------------- We're not done yet. Next we're going to write a function that converts _from_ a Roman Numeral to an `int` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-the-test-first-6) Write the test first -------------------------------------------------------------------------------------------------------------------------------------- We can re-use our test cases here with a little refactoring Move the `cases` variable outside of the test as a package variable in a `var` block. Notice I am using the slice functionality to just run one of the tests for now (`cases[:1]`) as trying to make all of those tests pass all at once is too big a leap [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#try-to-run-the-test-6) Try to run the test ------------------------------------------------------------------------------------------------------------------------------------ [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) Write the minimal amount of code for the test to run and check the failing test output -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Add our new function definition The test should now run and fail [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-enough-code-to-make-it-pass-6) Write enough code to make it pass ---------------------------------------------------------------------------------------------------------------------------------------------------------------- You know what to do Next, change the slice index in our test to move to the next test case (e.g. `cases[:2]`). Make it pass yourself with the dumbest code you can think of, continue writing dumb code (best book ever right?) for the third case too. Here's my dumb code. Through the dumbness of _real code that works_ we can start to see a pattern like before. We need to iterate through the input and build _something_, in this case a total. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-the-test-first-7) Write the test first -------------------------------------------------------------------------------------------------------------------------------------- Next we move to `cases[:4]` (`IV`) which now fails because it gets 2 back as that's the length of the string. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-enough-code-to-make-it-pass-7) Write enough code to make it pass ---------------------------------------------------------------------------------------------------------------------------------------------------------------- This is horrible but it does work. It's so bad I felt the need to add comments. * I wanted to be able to look up an integer value for a given roman numeral so I made a type from our array of `RomanNumeral`s and then added a method to it, `ValueOf` * Next in our loop we need to look ahead _if_ the string is big enough _and the current symbol is a valid subtractor_. At the moment it's just `I` (1) but can also be `X` (10) or `C` (100). * If it satisfies both of these conditions we need to lookup the value and add it to the total _if_ it is one of the special subtractors, otherwise ignore it * Then we need to further increment `i` so we don't count this symbol twice [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#refactor-6) Refactor -------------------------------------------------------------------------------------------------------------- I'm not entirely convinced this will be the long-term approach and there's potentially some interesting refactors we could do, but I'll resist that in case our approach is totally wrong. I'd rather make a few more tests pass first and see. For the meantime I made the first `if` statement slightly less horrible. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-the-test-first-8) Write the test first -------------------------------------------------------------------------------------------------------------------------------------- Let's move on to `cases[:5]` [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-enough-code-to-make-it-pass-8) Write enough code to make it pass ---------------------------------------------------------------------------------------------------------------------------------------------------------------- Apart from when it is subtractive our code assumes that every character is a `I` which is why the value is 1. We should be able to re-use our `ValueOf` method to fix this. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#refactor-7) Refactor -------------------------------------------------------------------------------------------------------------- When you index strings in Go, you get a `byte`. This is why when we build up the string again we have to do stuff like `string([]byte{symbol})`. It's repeated a couple of times, let's just move that functionality so that `ValueOf` takes some bytes instead. Then we can just pass in the bytes as is, to our function It's still pretty nasty, but it's getting there. If you start moving our `cases[:xx]` number through you'll see that quite a few are passing now. Remove the slice operator entirely and see which ones fail, here's some examples from my suite I think all we're missing is an update to `couldBeSubtractive` so that it accounts for the other kinds of subtractive symbols Try again, they still fail. However we left a comment earlier... We should never be just incrementing `total` as that implies every symbol is a `I`. Replace it with: And all the tests pass! Now that we have fully working software we can indulge ourselves in some refactoring, with confidence. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#refactor-8) Refactor -------------------------------------------------------------------------------------------------------------- Here is all the code I finished up with. I had a few failed attempts but as I keep emphasising, that's fine and the tests help me play around with the code freely. My main problem with the previous code is similar to our refactor from earlier. We had too many concerns coupled together. We wrote an algorithm which was trying to extract Roman Numerals from a string _and_ then find their values. So I created a new type `windowedRoman` which took care of extracting the numerals, offering a `Symbols` method to retrieve them as a slice. This meant our `ConvertToArabic` function could simply iterate over the symbols and total them. I broke the code down a bit by extracting some functions, especially around the wonky if statement to figure out if the symbol we are currently dealing with is a two character subtractive symbol. There's probably a more elegant way but I'm not going to sweat it. The code is there and it works and it is tested. If I (or anyone else) finds a better way they can safely change it - the hard work is done. [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#an-intro-to-property-based-tests) An intro to property based tests ------------------------------------------------------------------------------------------------------------------------------------------------------------ There have been a few rules in the domain of Roman Numerals that we have worked with in this chapter * Can't have more than 3 consecutive symbols * Only I (1), X (10) and C (100) can be "subtractors" * Taking the result of `ConvertToRoman(N)` and passing it to `ConvertToArabic` should return us `N` The tests we have written so far can be described as "example" based tests where we provide the tooling some examples around our code to verify. What if we could take these rules that we know about our domain and somehow exercise them against our code? Property based tests help you do this by throwing random data at your code and verifying the rules you describe always hold true. A lot of people think property based tests are mainly about random data but they would be mistaken. The real challenge about property based tests is having a _good_ understanding of your domain so you can write these properties. Enough words, let's see some code ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#rationale-of-property) Rationale of property Our first test will check that if we transform a number into Roman, when we use our other function to convert it back to a number that we get what we originally had. * Given random number (e.g `4`). * Call `ConvertToRoman` with random number (should return `IV` if `4`). * Take the result of above and pass it to `ConvertToArabic`. * The above should give us our original input (`4`). This feels like a good test to build us confidence because it should break if there's a bug in either. The only way it could pass is if they have the same kind of bug; which isn't impossible but feels unlikely. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#technical-explanation) Technical explanation We're using the [testing/quick](https://golang.org/pkg/testing/quick/) package from the standard library Reading from the bottom, we provide `quick.Check` a function that it will run against a number of random inputs, if the function returns `false` it will be seen as failing the check. Our `assertion` function above takes a random number and runs our functions to test the property. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#run-our-test) Run our test Try running it; your computer may hang for a while, so kill it when you're bored :) What's going on? Try adding the following to the assertion code. You should see something like this: Just running this very simple property has exposed a flaw in our implementation. We used `int` as our input but: * You can't do negative numbers with Roman Numerals * Given our rule of a max of 3 consecutive symbols we can't represent a value greater than 3999 ([well, kinda](https://www.quora.com/Which-is-the-maximum-number-in-Roman-numerals) ) and `int` has a much higher maximum value than 3999. This is great! We've been forced to think more deeply about our domain which is a real strength of property based tests. Clearly `int` is not a great type. What if we tried something a little more appropriate? ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#uint16) [`uint16`](https://golang.org/pkg/builtin/#uint16) Go has types for _unsigned integers_, which means they cannot be negative; so that rules out one class of bug in our code immediately. By adding 16, it means it is a 16 bit integer which can store a max of `65535`, which is still too big but gets us closer to what we need. Try updating the code to use `uint16` rather than `int`. I updated `assertion` in the test to give a bit more visibility. If you run the test they now actually run and you can see what is being tested. You can run multiple times to see our code stands up well to the various values! This gives me a lot of confidence that our code is working how we want. The default number of runs `quick.Check` performs is 100 but you can change that with a config. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#further-work) Further work * Can you write property tests that check the other properties we described? * Can you think of a way of making it so it's impossible for someone to call our code with a number greater than 3999? * You could return an error * Or create a new type that cannot represent > 3999 * What do you think is best? [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#wrapping-up) Wrapping up ------------------------------------------------------------------------------------------------------------------ ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#more-tdd-practice-with-iterative-development) More TDD practice with iterative development Did the thought of writing code that converts 1984 into MCMLXXXIV feel intimidating to you at first? It did to me and I've been writing software for quite a long time. The trick, as always, is to **get started with something simple** and take **small steps**. At no point in this process did we make any large leaps, do any huge refactorings, or get in a mess. I can hear someone cynically saying "this is just a kata". I can't argue with that, but I still take this same approach for every project I work on. I never ship a big distributed system in my first step, I find the simplest thing the team could ship (usually a "Hello world" website) and then iterate on small bits of functionality in manageable chunks, just like how we did here. The skill is knowing _how_ to split work up, and that comes with practice and with some lovely TDD to help you on your way. ### [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#property-based-tests) Property based tests * Built into the standard library * If you can think of ways to describe your domain rules in code, they are an excellent tool for giving you more confidence * Force you to think about your domain deeply * Potentially a nice complement to your test suite [](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#postscript) Postscript ---------------------------------------------------------------------------------------------------------------- This book is reliant on valuable feedback from the community. [Dave](http://github.com/gypsydave5) is an enormous help in practically every chapter. But he had a real rant about my use of 'Arabic numerals' in this chapter so, in the interests of full disclosure, here's what he said. > Just going to write up why a value of type `int` isn't really an 'arabic numeral'. This might be me being way too precise so I'll completely understand if you tell me to f off. > > A _digit_ is a character used in the representation of numbers - from the Latin for 'finger', as we usually have ten of them. In the Arabic (also called Hindu-Arabic) number system there are ten of them. These Arabic digits are: > > A _numeral_ is the representation of a number using a collection of digits. An Arabic numeral is a number represented by Arabic digits in a base 10 positional number system. We say 'positional' because each digit has a different value based upon its position in the numeral. So > > The `1` has a value of one thousand because its the first digit in a four digit numeral. > > Roman are built using a reduced number of digits (`I`, `V` etc...) mainly as values to produce the numeral. There's a bit of positional stuff but it's mostly `I` always representing 'one'. > > So, given this, is `int` an 'Arabic number'? The idea of a number is not at all tied to its representation - we can see this if we ask ourselves what the correct representation of this number is: > > Yes, this is a trick question. They're all correct. They're the representation of the same number in the decimal, binary, English, hexadecimal and octal number systems respectively. > > The representation of a number as a numeral is _independent_ of its properties as a number - and we can see this when we look at integer literals in Go: > > And how we can print integers in a format string: > > We can write the same integer both as a hexadecimal and an Arabic (decimal) numeral. > > So when the function signature looks like `ConvertToRoman(arabic int) string` it's making a bit of an assumption about how it's being called. Because sometimes `arabic` will be written as a decimal integer literal > > But it could just as well be written > > Really, we're not 'converting' from an Arabic numeral at all, we're 'printing' - representing - an `int` as a Roman numeral - and `int`s are not numerals, Arabic or otherwise; they're just numbers. The `ConvertToRoman` function is more like `strconv.Itoa` in that it's turning an `int` into a `string`. > > But every other version of the kata doesn't care about this distinction so :shrug: [PreviousContext](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/context) [NextMaths](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/math) Last updated 6 years ago * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-the-test-first) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#try-to-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-enough-code-to-make-it-pass) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#refactor) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-the-test-first-1) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#try-to-run-the-test-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-enough-code-to-make-it-pass-1) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#refactor-1) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-the-test-first-2) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#try-to-run-the-test-2) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-enough-code-to-make-it-pass-2) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#refactor-2) * [The Romans were into DRY too...](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#the-romans-were-into-dry-too) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-the-test-first-3) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#try-to-run-the-test-3) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-enough-code-to-make-it-pass-3) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#refactor-3) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-the-test-first-4) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#try-to-run-the-test-4) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-enough-code-to-make-it-pass-4) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#refactor-4) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-the-test-first-5) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#try-to-run-the-test-5) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-enough-code-to-make-it-pass-5) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#refactor-5) * [And the rest!](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#and-the-rest) * [Parsing Roman Numerals](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#parsing-roman-numerals) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-the-test-first-6) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#try-to-run-the-test-6) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-enough-code-to-make-it-pass-6) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-the-test-first-7) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-enough-code-to-make-it-pass-7) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#refactor-6) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-the-test-first-8) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#write-enough-code-to-make-it-pass-8) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#refactor-7) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#refactor-8) * [An intro to property based tests](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#an-intro-to-property-based-tests) * [Rationale of property](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#rationale-of-property) * [Technical explanation](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#technical-explanation) * [Run our test](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#run-our-test) * [uint16](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#uint16) * [Further work](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#further-work) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#wrapping-up) * [More TDD practice with iterative development](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#more-tdd-practice-with-iterative-development) * [Property based tests](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#property-based-tests) * [Postscript](https://quii.gitbook.io/learn-go-with-tests/master/go-fundamentals/roman-numerals#postscript) Copy func ConvertToRoman(arabic int) string { return "" } Copy === RUN TestRomanNumerals --- FAIL: TestRomanNumerals (0.00s) numeral_test.go:10: got '', want 'I' FAIL Copy func ConvertToRoman(arabic int) string { return "I" } Copy func TestRomanNumerals(t *testing.T) { t.Run("1 gets converted to I", func(t *testing.T) { got := ConvertToRoman(1) want := "I" if got != want { t.Errorf("got %q, want %q", got, want) } }) t.Run("2 gets converted to II", func(t *testing.T) { got := ConvertToRoman(2) want := "II" if got != want { t.Errorf("got %q, want %q", got, want) } }) } Copy === RUN TestRomanNumerals/2_gets_converted_to_II --- FAIL: TestRomanNumerals/2_gets_converted_to_II (0.00s) numeral_test.go:20: got 'I', want 'II' Copy func ConvertToRoman(arabic int) string { if arabic == 2 { return "II" } return "I" } Copy func TestRomanNumerals(t *testing.T) { cases := []struct { Description string Arabic int Want string }{ {"1 gets converted to I", 1, "I"}, {"2 gets converted to II", 2, "II"}, } for _, test := range cases { t.Run(test.Description, func(t *testing.T) { got := ConvertToRoman(test.Arabic) if got != test.Want { t.Errorf("got %q, want %q", got, test.Want) } }) } } Copy {"3 gets converted to III", 3, "III"}, Copy === RUN TestRomanNumerals/3_gets_converted_to_III --- FAIL: TestRomanNumerals/3_gets_converted_to_III (0.00s) numeral_test.go:20: got 'I', want 'III' Copy func ConvertToRoman(arabic int) string { if arabic == 3 { return "III" } if arabic == 2 { return "II" } return "I" } Copy func ConvertToRoman(arabic int) string { var result strings.Builder for i:=0; i 0; i-- { if i == 4 { result.WriteString("IV") break } result.WriteString("I") } return result.String() } Copy {"5 gets converted to V", 5, "V"}, Copy === RUN TestRomanNumerals/5_gets_converted_to_V --- FAIL: TestRomanNumerals/5_gets_converted_to_V (0.00s) numeral_test.go:25: got 'IIV', want 'V' Copy func ConvertToRoman(arabic int) string { var result strings.Builder for i := arabic; i > 0; i-- { if i == 5 { result.WriteString("V") break } if i == 4 { result.WriteString("IV") break } result.WriteString("I") } return result.String() } Copy func ConvertToRoman(arabic int) string { var result strings.Builder for arabic > 0 { switch { case arabic > 4: result.WriteString("V") arabic -= 5 case arabic > 3: result.WriteString("IV") arabic -= 4 default: result.WriteString("I") arabic-- } } return result.String() } Copy {"9 gets converted to IX", 9, "IX"} Copy === RUN TestRomanNumerals/9_gets_converted_to_IX --- FAIL: TestRomanNumerals/9_gets_converted_to_IX (0.00s) numeral_test.go:29: got 'VIV', want 'IX' Copy case arabic > 8: result.WriteString("IX") arabic -= 9 Copy {"10 gets converted to X", 10, "X"}, {"14 gets converted to XIV", 14, "XIV"}, {"18 gets converted to XVIII", 18, "XVIII"}, {"20 gets converted to XX", 20, "XX"}, {"39 gets converted to XXXIX", 39, "XXXIX"}, Copy type RomanNumeral struct { Value int Symbol string } var allRomanNumerals = []RomanNumeral { {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}, } func ConvertToRoman(arabic int) string { var result strings.Builder for _, numeral := range allRomanNumerals { for arabic >= numeral.Value { result.WriteString(numeral.Symbol) arabic -= numeral.Value } } return result.String() } Copy {"40 gets converted to XL", 40, "XL"}, {"47 gets converted to XLVII", 47, "XLVII"}, {"49 gets converted to XLIX", 49, "XLIX"}, {"50 gets converted to L", 50, "L"}, Copy func TestRomanNumerals(t *testing.T) { cases := []struct { Arabic int Roman string }{ {Arabic: 1, Roman: "I"}, {Arabic: 2, Roman: "II"}, {Arabic: 3, Roman: "III"}, {Arabic: 4, Roman: "IV"}, {Arabic: 5, Roman: "V"}, {Arabic: 6, Roman: "VI"}, {Arabic: 7, Roman: "VII"}, {Arabic: 8, Roman: "VIII"}, {Arabic: 9, Roman: "IX"}, {Arabic: 10, Roman: "X"}, {Arabic: 14, Roman: "XIV"}, {Arabic: 18, Roman: "XVIII"}, {Arabic: 20, Roman: "XX"}, {Arabic: 39, Roman: "XXXIX"}, {Arabic: 40, Roman: "XL"}, {Arabic: 47, Roman: "XLVII"}, {Arabic: 49, Roman: "XLIX"}, {Arabic: 50, Roman: "L"}, {Arabic: 100, Roman: "C"}, {Arabic: 90, Roman: "XC"}, {Arabic: 400, Roman: "CD"}, {Arabic: 500, Roman: "D"}, {Arabic: 900, Roman: "CM"}, {Arabic: 1000, Roman: "M"}, {Arabic: 1984, Roman: "MCMLXXXIV"}, {Arabic: 3999, Roman: "MMMCMXCIX"}, {Arabic: 2014, Roman: "MMXIV"}, {Arabic: 1006, Roman: "MVI"}, {Arabic: 798, Roman: "DCCXCVIII"}, } for _, test := range cases { t.Run(fmt.Sprintf("%d gets converted to %q", test.Arabic, test.Roman), func(t *testing.T) { got := ConvertToRoman(test.Arabic) if got != test.Roman { t.Errorf("got %q, want %q", got, test.Roman) } }) } } Copy var allRomanNumerals = []RomanNumeral{ {1000, "M"}, {900, "CM"}, {500, "D"}, {400, "CD"}, {100, "C"}, {90, "XC"}, {50, "L"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}, } Copy func TestConvertingToArabic(t *testing.T) { for _, test := range cases[:1] { t.Run(fmt.Sprintf("%q gets converted to %d", test.Roman, test.Arabic), func(t *testing.T) { got := ConvertToArabic(test.Roman) if got != test.Arabic { t.Errorf("got %d, want %d", got, test.Arabic) } }) } } Copy ./numeral_test.go:60:11: undefined: ConvertToArabic Copy func ConvertToArabic(roman string) int { return 0 } Copy --- FAIL: TestConvertingToArabic (0.00s) --- FAIL: TestConvertingToArabic/'I'_gets_converted_to_1 (0.00s) numeral_test.go:62: got 0, want 1 Copy func ConvertToArabic(roman string) int { return 1 } Copy func ConvertToArabic(roman string) int { if roman == "III" { return 3 } if roman == "II" { return 2 } return 1 } Copy func ConvertToArabic(roman string) int { total := 0 for range roman { total++ } return total } Copy // earlier.. type RomanNumerals []RomanNumeral func (r RomanNumerals) ValueOf(symbol string) int { for _, s := range r { if s.Symbol == symbol { return s.Value } } return 0 } // later.. func ConvertToArabic(roman string) int { total := 0 for i := 0; i < len(roman); i++ { symbol := roman[i] // look ahead to next symbol if we can and, the current symbol is base 10 (only valid subtractors) if i+1 < len(roman) && symbol == 'I' { nextSymbol := roman[i+1] // build the two character string potentialNumber := string([]byte{symbol, nextSymbol}) // get the value of the two character string value := allRomanNumerals.ValueOf(potentialNumber) if value != 0 { total += value i++ // move past this character too for the next loop } else { total++ } } else { total++ } } return total } Copy func ConvertToArabic(roman string) int { total := 0 for i := 0; i < len(roman); i++ { symbol := roman[i] if couldBeSubtractive(i, symbol, roman) { nextSymbol := roman[i+1] // build the two character string potentialNumber := string([]byte{symbol, nextSymbol}) // get the value of the two character string value := allRomanNumerals.ValueOf(potentialNumber) if value != 0 { total += value i++ // move past this character too for the next loop } else { total++ } } else { total++ } } return total } func couldBeSubtractive(index int, currentSymbol uint8, roman string) bool { return index+1 < len(roman) && currentSymbol == 'I' } Copy === RUN TestConvertingToArabic/'V'_gets_converted_to_5 --- FAIL: TestConvertingToArabic/'V'_gets_converted_to_5 (0.00s) numeral_test.go:62: got 1, want 5 Copy func ConvertToArabic(roman string) int { total := 0 for i := 0; i < len(roman); i++ { symbol := roman[i] // look ahead to next symbol if we can and, the current symbol is base 10 (only valid subtractors) if couldBeSubtractive(i, symbol, roman) { nextSymbol := roman[i+1] // build the two character string potentialNumber := string([]byte{symbol, nextSymbol}) if value := allRomanNumerals.ValueOf(potentialNumber); value != 0 { total += value i++ // move past this character too for the next loop } else { total++ // this is fishy... } } else { total+=allRomanNumerals.ValueOf(string([]byte{symbol})) } } return total } Copy func (r RomanNumerals) ValueOf(symbols ...byte) int { symbol := string(symbols) for _, s := range r { if s.Symbol == symbol { return s.Value } } return 0 } Copy func ConvertToArabic(roman string) int { total := 0 for i := 0; i < len(roman); i++ { symbol := roman[i] if couldBeSubtractive(i, symbol, roman) { if value := allRomanNumerals.ValueOf(symbol, roman[i+1]); value != 0 { total += value i++ // move past this character too for the next loop } else { total++ // this is fishy... } } else { total+=allRomanNumerals.ValueOf(symbol) } } return total } Copy === RUN TestConvertingToArabic/'XL'_gets_converted_to_40 --- FAIL: TestConvertingToArabic/'XL'_gets_converted_to_40 (0.00s) numeral_test.go:62: got 60, want 40 === RUN TestConvertingToArabic/'XLVII'_gets_converted_to_47 --- FAIL: TestConvertingToArabic/'XLVII'_gets_converted_to_47 (0.00s) numeral_test.go:62: got 67, want 47 === RUN TestConvertingToArabic/'XLIX'_gets_converted_to_49 --- FAIL: TestConvertingToArabic/'XLIX'_gets_converted_to_49 (0.00s) numeral_test.go:62: got 69, want 49 Copy func couldBeSubtractive(index int, currentSymbol uint8, roman string) bool { isSubtractiveSymbol := currentSymbol == 'I' || currentSymbol == 'X' || currentSymbol =='C' return index+1 < len(roman) && isSubtractiveSymbol } Copy total++ // this is fishy... Copy total += allRomanNumerals.ValueOf(symbol) Copy import "strings" func ConvertToArabic(roman string) (total int) { for _, symbols := range windowedRoman(roman).Symbols() { total += allRomanNumerals.ValueOf(symbols...) } return } func ConvertToRoman(arabic int) string { var result strings.Builder for _, numeral := range allRomanNumerals { for arabic >= numeral.Value { result.WriteString(numeral.Symbol) arabic -= numeral.Value } } return result.String() } type romanNumeral struct { Value int Symbol string } type romanNumerals []romanNumeral func (r romanNumerals) ValueOf(symbols ...byte) int { symbol := string(symbols) for _, s := range r { if s.Symbol == symbol { return s.Value } } return 0 } func (r romanNumerals) Exists(symbols ...byte) bool { symbol := string(symbols) for _, s := range r { if s.Symbol == symbol { return true } } return false } var allRomanNumerals = romanNumerals{ {1000, "M"}, {900, "CM"}, {500, "D"}, {400, "CD"}, {100, "C"}, {90, "XC"}, {50, "L"}, {40, "XL"}, {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"}, } type windowedRoman string func (w windowedRoman) Symbols() (symbols [][]byte) { for i := 0; i < len(w); i++ { symbol := w[i] notAtEnd := i+1 < len(w) if notAtEnd && isSubtractive(symbol) && allRomanNumerals.Exists(symbol, w[i+1]) { symbols = append(symbols, []byte{byte(symbol), byte(w[i+1])}) i++ } else { symbols = append(symbols, []byte{byte(symbol)}) } } return } func isSubtractive(symbol uint8) bool { return symbol == 'I' || symbol == 'X' || symbol == 'C' } Copy func TestPropertiesOfConversion(t *testing.T) { assertion := func(arabic int) bool { roman := ConvertToRoman(arabic) fromRoman := ConvertToArabic(roman) return fromRoman == arabic } if err := quick.Check(assertion, nil); err != nil { t.Error("failed checks", err) } } Copy assertion := func(arabic int) bool { if arabic <0 || arabic > 3999 { log.Println(arabic) return true } roman := ConvertToRoman(arabic) fromRoman := ConvertToArabic(roman) return fromRoman == arabic } Copy === RUN TestPropertiesOfConversion 2019/07/09 14:41:27 6849766357708982977 2019/07/09 14:41:27 -7028152357875163913 2019/07/09 14:41:27 -6752532134903680693 2019/07/09 14:41:27 4051793897228170080 2019/07/09 14:41:27 -1111868396280600429 2019/07/09 14:41:27 8851967058300421387 2019/07/09 14:41:27 562755830018219185 Copy assertion := func(arabic uint16) bool { if arabic > 3999 { return true } t.Log("testing", arabic) roman := ConvertToRoman(arabic) fromRoman := ConvertToArabic(roman) return fromRoman == arabic } Copy if err := quick.Check(assertion, &quick.Config{ MaxCount:1000, }); err != nil { t.Error("failed checks", err) } Copy 0 1 2 3 4 5 6 7 8 9 Copy 1337 Copy 255 11111111 two-hundred and fifty-five FF 377 Copy 0xFF == 255 // true Copy n := 255 fmt.Printf("%b %c %d %o %q %x %X %U", n, n, n, n, n, n, n, n) // 11111111 ÿ 255 377 'ÿ' ff FF U+00FF Copy ConvertToRoman(255) Copy ConvertToRoman(0xFF) --- # Time | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time.md) . [**You can find all the code for this chapter here**](https://github.com/quii/learn-go-with-tests/tree/master/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. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#just-enough-information-on-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. Our application will help keep track of when the blind should go up, and how much it should be. * When it starts it asks how many players are playing. This determines the amount of time there is before the "blind" bet goes up. * There is a base amount of time of 5 minutes. * For every player, 1 minute is added. * e.g 6 players equals 11 minutes for the blind. * After the blind time expires the game should alert the players the new amount the blind bet is. * The blind starts at 100 chips, then 200, 400, 600, 1000, 2000 and continue to double until the game ends (our previous functionality of "Ruth wins" should still finish the game) [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#reminder-of-the-code) Reminder of the code ------------------------------------------------------------------------------------------------------------------------------- In the previous chapter we made our start to the command line application which already accepts a command of `{name} wins`. Here is what the current `CLI` code looks like, but be sure to familiarise yourself with the other code too before starting. Copy type CLI struct { playerStore PlayerStore in *bufio.Scanner } func NewCLI(store PlayerStore, in io.Reader) *CLI { return &CLI{ playerStore: store, in: bufio.NewScanner(in), } } func (cli *CLI) PlayPoker() { userInput := cli.readLine() cli.playerStore.RecordWin(extractWinner(userInput)) } func extractWinner(userInput string) string { return strings.Replace(userInput, " wins", "", 1) } func (cli *CLI) readLine() string { cli.in.Scan() return cli.in.Text() } ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#time.afterfunc) `time.AfterFunc` We want to be able to schedule our program to print the blind bet values at certain durations dependant on the number of players. To limit the scope of what we need to do, we'll forget about the number of players part for now and just assume there are 5 players so we'll test that _every 10 minutes the new value of the blind bet is printed_. As usual the standard library has us covered with [`func AfterFunc(d Duration, f func()) *Timer`](https://golang.org/pkg/time/#AfterFunc) > `AfterFunc` waits for the duration to elapse and then calls f in its own goroutine. It returns a `Timer` that can be used to cancel the call using its Stop method. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#time.duration) [`time.Duration`](https://golang.org/pkg/time/#Duration) > A Duration represents the elapsed time between two instants as an int64 nanosecond count. The time library has a number of constants to let you multiply those nanoseconds so they're a bit more readable for the kind of scenarios we'll be doing When we call `PlayPoker` we'll schedule all of our blind alerts. Testing this may be a little tricky though. We'll want to verify that each time period is scheduled with the correct blind amount but if you look at the signature of `time.AfterFunc` its second argument is the function it will run. You cannot compare functions in Go so we'd be unable to test what function has been sent in. So we'll need to write some kind of wrapper around `time.AfterFunc` which will take the time to run and the amount to print so we can spy on that. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-the-test-first) Write the test first ------------------------------------------------------------------------------------------------------------------------------- Add a new test to our suite You'll notice we've made a `SpyBlindAlerter` which we are trying to inject into our `CLI` and then checking that after we call `PlayerPoker` that an alert is scheduled. (Remember we are just going for the simplest scenario first and then we'll iterate.) Here's the definition of `SpyBlindAlerter` [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#try-to-run-the-test) Try to run the test ----------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- We have added a new argument and the compiler is complaining. _Strictly speaking_ the minimal amount of code is to make `NewCLI` accept a `*SpyBlindAlerter` but let's cheat a little and just define the dependency as an interface. And then add it to the constructor 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 Then use that in the other tests to fix the compilation problems. By labelling it as a "dummy" it is clear to the reader of the test that it is not important. [\> Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists.](https://martinfowler.com/articles/mocksArentStubs.html) The tests should now compile and our new test fails. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-enough-code-to-make-it-pass) Write enough code to make it pass --------------------------------------------------------------------------------------------------------------------------------------------------------- We'll need to add the `BlindAlerter` as a field on our `CLI` so we can reference it in our `PlayPoker` method. To make the test pass, we can call our `BlindAlerter` with anything we like Next we'll want to check it schedules all the alerts we'd hope for, for 5 players [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-the-test-first-1) Write the test first --------------------------------------------------------------------------------------------------------------------------------- Table-based test works nicely here and clearly illustrate what our requirements are. We run through the table and check the `SpyBlindAlerter` to see if the alert has been scheduled with the correct values. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#try-to-run-the-test-1) Try to run the test ------------------------------------------------------------------------------------------------------------------------------- You should have a lot of failures looking like this [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-enough-code-to-make-it-pass-1) Write enough code to make it pass ----------------------------------------------------------------------------------------------------------------------------------------------------------- It's not a lot more complicated than what we already had. We're just now iterating over an array of `blinds` and calling the scheduler on an increasing `blindTime` [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#refactor) Refactor ------------------------------------------------------------------------------------------------------- We can encapsulate our scheduled alerts into a method just to make `PlayPoker` read a little clearer. Finally our tests are looking a little clunky. We have two anonymous structs representing the same thing, a `ScheduledAlert`. Let's refactor that into a new type and then make some helpers to compare them. We've added a `String()` method to our type so it prints nicely if the test fails Update our test to use our new type Implement `assertScheduledAlert` yourself. We've spent a fair amount of time here writing tests and have been somewhat naughty not integrating with our application. Let's address that before we pile on any more requirements. Try running the app and it won't compile, complaining about not enough args to `NewCLI`. Let's create an implementation of `BlindAlerter` that we can use in our application. Create `BlindAlerter.go` and move our `BlindAlerter` interface and add the new things below 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. We then create the function `StdOutAlerter` which has the same signature as the function and just use `time.AfterFunc` to schedule it to print to `os.Stdout`. Update `main` where we create `NewCLI` to see this in action Before running you might want to change the `blindTime` increment in `CLI` to be 10 seconds rather than 10 minutes just so you can see it in action. 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. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-the-test-first-2) 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 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. Here is our new test We pass in what will be `os.Stdout` in `main` and see what is written. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#try-to-run-the-test-2) Try to run the test ------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) Write the minimal amount of code for the test to run and check the failing test output --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- We have a new dependency so we'll have to update `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. The new test should fail like so [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-enough-code-to-make-it-pass-2) Write enough code to make it pass ----------------------------------------------------------------------------------------------------------------------------------------------------------- We need to add our new dependency to our `CLI` so we can reference it in `PlayPoker` Then finally we can write our prompt at the start of the game [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#refactor-1) Refactor --------------------------------------------------------------------------------------------------------- We have a duplicate string for the prompt which we should extract into a constant Use this in both the test code and `CLI`. Now we need to send in a number and extract it out. The only way we'll know if it has had the desired effect is by seeing what blind alerts were scheduled. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-the-test-first-3) Write the test first --------------------------------------------------------------------------------------------------------------------------------- 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 * We test what alerts are scheduled [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#try-to-run-the-test-3) Try to run the test ------------------------------------------------------------------------------------------------------------------------------- The test should still compile and fail reporting that the scheduled times are wrong because we've hard-coded for the game to be based on having 5 players [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-enough-code-to-make-it-pass-3) Write enough code to make it pass ----------------------------------------------------------------------------------------------------------------------------------------------------------- Remember, we are free to commit whatever sins we need to make this work. Once we have working software we can then work on refactoring the mess we're about to make! * We read in the `numberOfPlayersInput` into a string * We use `cli.readLine()` to get the input from the user and then call `Atoi` to convert it into an integer - ignoring any error scenarios. We'll need to write a test for that scenario later. * From here we change `scheduleBlindAlerts` to accept a number of players. We then calculate a `blindIncrement` time to use to add to `blindTime` as we iterate over the blind amounts While our new test has been fixed, a lot of others have failed because now our system only works if the game starts with a user entering a number. You'll need to fix the tests by changing the user inputs so that a number followed by a newline is added (this is highlighting yet more flaws in our approach right now). [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#refactor-2) Refactor --------------------------------------------------------------------------------------------------------- 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_. * 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. 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. This is what I came up with: 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. 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: * Constructing `Game` with its existing dependencies (which we'll refactor next) * Interpreting user input as method invocations for `Game` We want to try to avoid doing "big" refactors which leave us in a state of failing tests for extended periods as that increases the chances of mistakes. (If you are working in a large/distributed team this is extra important) The first thing we'll do is refactor `Game` so that we inject it into `CLI`. We'll do the smallest changes in our tests to facilitate that and then we'll see how we can break up the tests into the themes of parsing user input and game management. All we need to do right now is change `NewCLI` This feels like an improvement already. We have less dependencies and _our dependency list is reflecting our overall design goal_ of CLI being concerned with input/output and delegating game specific actions to a `Game`. If you try and compile there are problems. You should be able to fix these problems yourself. Don't worry about making any mocks for `Game` right now, just initialise _real_ `Game`s just to get everything compiling and tests green. To do this you'll need to make a constructor Here's an example of one of the setups for the tests being fixed It shouldn't take much effort to fix the tests and be back to green again (that's the point!) but make sure you fix `main.go` too before the next stage. 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 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. 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 * Process user input and call `Game`'s methods when appropriate * Send output * Crucially it doesn't know about the actual workings of how games work To do this we'll have to make it so `CLI` no longer relies on a concrete `Game` type but instead accepts an interface with `Start(numberOfPlayers)` and `Finish(winner)`. We can then create a spy of that type and verify the correct calls are made. It's here we realise that naming is awkward sometimes. Rename `Game` to `TexasHoldem` (as that's the _kind_ of game we're playing) and the new interface will be called `Game`. This keeps faithful to the notion that our CLI is oblivious to the actual game we're playing and what happens when you `Start` and `Finish`. Replace all references to `*Game` inside `CLI` and replace them with `Game` (our new interface). As always keep re-running tests to check everything is green while we are refactoring. Now that we have decoupled `CLI` from `TexasHoldem` we can use spies to check that `Start` and `Finish` are called when we expect them to, with the correct arguments. Create a spy that implements `Game` Replace any `CLI` test which is testing any game specific logic with checks on how our `GameSpy` is called. This will then reflect the responsibilities of CLI in our tests clearly. Here is an example of one of the tests being fixed; try and do the rest yourself and check the source code if you get stuck. 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. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-the-test-first-4) Write the test first --------------------------------------------------------------------------------------------------------------------------------- We'll start by making sure the game doesn't start You'll need to add to our `GameSpy` a field `StartCalled` which only gets set if `Start` is called [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#try-to-run-the-test-4) Try to run the test ------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-enough-code-to-make-it-pass-4) Write enough code to make it pass ----------------------------------------------------------------------------------------------------------------------------------------------------------- Around where we call `Atoi` we just need to check for the error Next we need to inform the user of what they did wrong so we'll assert on what is printed to `stdout`. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-the-test-first-5) Write the test first --------------------------------------------------------------------------------------------------------------------------------- We've asserted on what was printed to `stdout` before so we can copy that code for now We are storing _everything_ that gets written to stdout so we still expect the `poker.PlayerPrompt`. We then just check an additional thing gets printed. We're not too bothered about the exact wording for now, we'll address it when we refactor. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#try-to-run-the-test-5) Try to run the test ------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-enough-code-to-make-it-pass-5) Write enough code to make it pass ----------------------------------------------------------------------------------------------------------------------------------------------------------- Change the error handling code [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#refactor-3) Refactor --------------------------------------------------------------------------------------------------------- Now refactor the message into a constant like `PlayerPrompt` and put in a more appropriate message Finally our testing around what has been sent to `stdout` is quite verbose, let's write an assert function to clean it up. Using the vararg syntax (`...string`) is handy here because we need to assert on varying amounts of messages. 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. 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_ ? Here is what I came up with 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` ? Finish this chapter by writing a test for this scenario and making it pass. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#wrapping-up) Wrapping up ------------------------------------------------------------------------------------------------------------- ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#a-quick-project-recap) A quick project recap 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. * 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. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#time.afterfunc-1) time.Afterfunc A very handy way of scheduling a function call after a specific duration. It is well worth investing time [looking at the documentation for `time`](https://golang.org/pkg/time/) as it has a lot of time saving functions and methods for you to work with. 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`. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#more-examples-of-good-separation-of-concerns) 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. 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. 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. After refactoring the dependency list reflected our design goal. This is another benefit of DI in that it often documents intent. When you rely on global variables responsibilities become very unclear. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#an-example-of-a-function-implementing-an-interface) An example of a function implementing an interface ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- When you define an interface with one method in it you might want to consider defining a `MyInterfaceFunc` type to complement it so users can implement your interface with just a function [PreviousCommand line & package structure](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line) [NextWebSockets](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/websockets) Last updated 6 years ago * [Just enough information on poker](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#just-enough-information-on-poker) * [Reminder of the code](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#reminder-of-the-code) * [time.AfterFunc](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#time.afterfunc) * [time.Duration](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#time.duration) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-the-test-first) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#try-to-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-enough-code-to-make-it-pass) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-the-test-first-1) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#try-to-run-the-test-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-enough-code-to-make-it-pass-1) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#refactor) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-the-test-first-2) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#try-to-run-the-test-2) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-enough-code-to-make-it-pass-2) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#refactor-1) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-the-test-first-3) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#try-to-run-the-test-3) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-enough-code-to-make-it-pass-3) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#refactor-2) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-the-test-first-4) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#try-to-run-the-test-4) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-enough-code-to-make-it-pass-4) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-the-test-first-5) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#try-to-run-the-test-5) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#write-enough-code-to-make-it-pass-5) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#refactor-3) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#wrapping-up) * [A quick project recap](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#a-quick-project-recap) * [time.Afterfunc](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#time.afterfunc-1) * [More examples of good separation of concerns](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#more-examples-of-good-separation-of-concerns) * [An example of a function implementing an interface](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/time#an-example-of-a-function-implementing-an-interface) Copy 5 * time.Second Copy t.Run("it schedules printing of blind values", func(t *testing.T) { in := strings.NewReader("Chris wins\n") playerStore := &poker.StubPlayerStore{} blindAlerter := &SpyBlindAlerter{} cli := poker.NewCLI(playerStore, in, blindAlerter) cli.PlayPoker() if len(blindAlerter.alerts) != 1 { t.Fatal("expected a blind alert to be scheduled") } }) Copy type SpyBlindAlerter struct { alerts []struct{ scheduledAt time.Duration amount int } } func (s *SpyBlindAlerter) ScheduleAlertAt(duration time.Duration, amount int) { s.alerts = append(s.alerts, struct { scheduledAt time.Duration amount int }{duration, amount}) } Copy ./CLI_test.go:32:27: too many arguments in call to poker.NewCLI have (*poker.StubPlayerStore, *strings.Reader, *SpyBlindAlerter) want (poker.PlayerStore, io.Reader) Copy type BlindAlerter interface { ScheduleAlertAt(duration time.Duration, amount int) } Copy func NewCLI(store PlayerStore, in io.Reader, alerter BlindAlerter) *CLI Copy var dummySpyAlerter = &SpyBlindAlerter{} Copy === RUN TestCLI === RUN TestCLI/it_schedules_printing_of_blind_values --- FAIL: TestCLI (0.00s) --- FAIL: TestCLI/it_schedules_printing_of_blind_values (0.00s) CLI_test.go:38: expected a blind alert to be scheduled Copy type CLI struct { playerStore PlayerStore in *bufio.Scanner alerter BlindAlerter } func NewCLI(store PlayerStore, in io.Reader, alerter BlindAlerter) *CLI { return &CLI{ playerStore: store, in: bufio.NewScanner(in), alerter: alerter, } } Copy func (cli *CLI) PlayPoker() { cli.alerter.ScheduleAlertAt(5 * time.Second, 100) userInput := cli.readLine() cli.playerStore.RecordWin(extractWinner(userInput)) } Copy t.Run("it schedules printing of blind values", func(t *testing.T) { in := strings.NewReader("Chris wins\n") playerStore := &poker.StubPlayerStore{} blindAlerter := &SpyBlindAlerter{} cli := poker.NewCLI(playerStore, in, blindAlerter) cli.PlayPoker() cases := []struct{ expectedScheduleTime time.Duration expectedAmount int } { {0 * time.Second, 100}, {10 * time.Minute, 200}, {20 * time.Minute, 300}, {30 * time.Minute, 400}, {40 * time.Minute, 500}, {50 * time.Minute, 600}, {60 * time.Minute, 800}, {70 * time.Minute, 1000}, {80 * time.Minute, 2000}, {90 * time.Minute, 4000}, {100 * time.Minute, 8000}, } for i, c := range cases { t.Run(fmt.Sprintf("%d scheduled for %v", c.expectedAmount, c.expectedScheduleTime), func(t *testing.T) { if len(blindAlerter.alerts) <= i { t.Fatalf("alert %d was not scheduled %v", i, blindAlerter.alerts) } alert := blindAlerter.alerts[i] amountGot := alert.amount if amountGot != c.expectedAmount { t.Errorf("got amount %d, want %d", amountGot, c.expectedAmount) } gotScheduledTime := alert.scheduledAt if gotScheduledTime != c.expectedScheduleTime { t.Errorf("got scheduled time of %v, want %v", gotScheduledTime, c.expectedScheduleTime) } }) } }) Copy === RUN TestCLI --- FAIL: TestCLI (0.00s) === RUN TestCLI/it_schedules_printing_of_blind_values --- FAIL: TestCLI/it_schedules_printing_of_blind_values (0.00s) === RUN TestCLI/it_schedules_printing_of_blind_values/100_scheduled_for_0s --- FAIL: TestCLI/it_schedules_printing_of_blind_values/100_scheduled_for_0s (0.00s) CLI_test.go:71: got scheduled time of 5s, want 0s === RUN TestCLI/it_schedules_printing_of_blind_values/200_scheduled_for_10m0s --- FAIL: TestCLI/it_schedules_printing_of_blind_values/200_scheduled_for_10m0s (0.00s) CLI_test.go:59: alert 1 was not scheduled [{5000000000 100}] Copy func (cli *CLI) PlayPoker() { blinds := []int{100, 200, 300, 400, 500, 600, 800, 1000, 2000, 4000, 8000} blindTime := 0 * time.Second for _, blind := range blinds { cli.alerter.ScheduleAlertAt(blindTime, blind) blindTime = blindTime + 10 * time.Minute } userInput := cli.readLine() cli.playerStore.RecordWin(extractWinner(userInput)) } Copy func (cli *CLI) PlayPoker() { cli.scheduleBlindAlerts() userInput := cli.readLine() cli.playerStore.RecordWin(extractWinner(userInput)) } func (cli *CLI) scheduleBlindAlerts() { blinds := []int{100, 200, 300, 400, 500, 600, 800, 1000, 2000, 4000, 8000} blindTime := 0 * time.Second for _, blind := range blinds { cli.alerter.ScheduleAlertAt(blindTime, blind) blindTime = blindTime + 10*time.Minute } } Copy type scheduledAlert struct { at time.Duration amount int } func (s scheduledAlert) String() string { return fmt.Sprintf("%d chips at %v", s.amount, s.at) } type SpyBlindAlerter struct { alerts []scheduledAlert } func (s *SpyBlindAlerter) ScheduleAlertAt(at time.Duration, amount int) { s.alerts = append(s.alerts, scheduledAlert{at, amount}) } Copy t.Run("it schedules printing of blind values", func(t *testing.T) { in := strings.NewReader("Chris wins\n") playerStore := &poker.StubPlayerStore{} blindAlerter := &SpyBlindAlerter{} cli := poker.NewCLI(playerStore, in, blindAlerter) cli.PlayPoker() cases := []scheduledAlert { {0 * time.Second, 100}, {10 * time.Minute, 200}, {20 * time.Minute, 300}, {30 * time.Minute, 400}, {40 * time.Minute, 500}, {50 * time.Minute, 600}, {60 * time.Minute, 800}, {70 * time.Minute, 1000}, {80 * time.Minute, 2000}, {90 * time.Minute, 4000}, {100 * time.Minute, 8000}, } for i, want := range cases { t.Run(fmt.Sprint(want), func(t *testing.T) { if len(blindAlerter.alerts) <= i { t.Fatalf("alert %d was not scheduled %v", i, blindAlerter.alerts) } got := blindAlerter.alerts[i] assertScheduledAlert(t, got, want) }) } }) Copy package poker import ( "time" "fmt" "os" ) type BlindAlerter interface { ScheduleAlertAt(duration time.Duration, amount int) } type BlindAlerterFunc func(duration time.Duration, amount int) func (a BlindAlerterFunc) ScheduleAlertAt(duration time.Duration, amount int) { a(duration, amount) } func StdOutAlerter(duration time.Duration, amount int) { time.AfterFunc(duration, func() { fmt.Fprintf(os.Stdout, "Blind is now %d\n", amount) }) } Copy poker.NewCLI(store, os.Stdin, poker.BlindAlerterFunc(poker.StdOutAlerter)).PlayPoker() Copy var dummyBlindAlerter = &SpyBlindAlerter{} var dummyPlayerStore = &poker.StubPlayerStore{} var dummyStdIn = &bytes.Buffer{} var dummyStdOut = &bytes.Buffer{} Copy t.Run("it prompts the user to enter the number of players", func(t *testing.T) { stdout := &bytes.Buffer{} cli := poker.NewCLI(dummyPlayerStore, dummyStdIn, stdout, dummyBlindAlerter) cli.PlayPoker() got := stdout.String() want := "Please enter the number of players: " if got != want { t.Errorf("got %q, want %q", got, want) } }) Copy ./CLI_test.go:38:27: too many arguments in call to poker.NewCLI have (*poker.StubPlayerStore, *bytes.Buffer, *bytes.Buffer, *SpyBlindAlerter) want (poker.PlayerStore, io.Reader, poker.BlindAlerter) Copy func NewCLI(store PlayerStore, in io.Reader, out io.Writer, alerter BlindAlerter) *CLI Copy === RUN TestCLI --- FAIL: TestCLI (0.00s) === RUN TestCLI/it_prompts_the_user_to_enter_the_number_of_players --- FAIL: TestCLI/it_prompts_the_user_to_enter_the_number_of_players (0.00s) CLI_test.go:46: got '', want 'Please enter the number of players: ' FAIL Copy type CLI struct { playerStore PlayerStore in *bufio.Scanner out io.Writer alerter BlindAlerter } func NewCLI(store PlayerStore, in io.Reader, out io.Writer, alerter BlindAlerter) *CLI { return &CLI{ playerStore: store, in: bufio.NewScanner(in), out: out, alerter: alerter, } } Copy func (cli *CLI) PlayPoker() { fmt.Fprint(cli.out, "Please enter the number of players: ") cli.scheduleBlindAlerts() userInput := cli.readLine() cli.playerStore.RecordWin(extractWinner(userInput)) } Copy const PlayerPrompt = "Please enter the number of players: " Copy t.Run("it prompts the user to enter the number of players", func(t *testing.T) { stdout := &bytes.Buffer{} in := strings.NewReader("7\n") blindAlerter := &SpyBlindAlerter{} cli := poker.NewCLI(dummyPlayerStore, in, stdout, blindAlerter) cli.PlayPoker() got := stdout.String() want := poker.PlayerPrompt if got != want { t.Errorf("got %q, want %q", got, want) } cases := []scheduledAlert{ {0 * time.Second, 100}, {12 * time.Minute, 200}, {24 * time.Minute, 300}, {36 * time.Minute, 400}, } for i, want := range cases { t.Run(fmt.Sprint(want), func(t *testing.T) { if len(blindAlerter.alerts) <= i { t.Fatalf("alert %d was not scheduled %v", i, blindAlerter.alerts) } got := blindAlerter.alerts[i] assertScheduledAlert(t, got, want) }) } }) Copy === RUN TestCLI --- FAIL: TestCLI (0.00s) === RUN TestCLI/it_prompts_the_user_to_enter_the_number_of_players --- FAIL: TestCLI/it_prompts_the_user_to_enter_the_number_of_players (0.00s) === RUN TestCLI/it_prompts_the_user_to_enter_the_number_of_players/100_chips_at_0s --- PASS: TestCLI/it_prompts_the_user_to_enter_the_number_of_players/100_chips_at_0s (0.00s) === RUN TestCLI/it_prompts_the_user_to_enter_the_number_of_players/200_chips_at_12m0s Copy func (cli *CLI) PlayPoker() { fmt.Fprint(cli.out, PlayerPrompt) numberOfPlayers, _ := strconv.Atoi(cli.readLine()) cli.scheduleBlindAlerts(numberOfPlayers) userInput := cli.readLine() cli.playerStore.RecordWin(extractWinner(userInput)) } func (cli *CLI) scheduleBlindAlerts(numberOfPlayers int) { blindIncrement := time.Duration(5 + numberOfPlayers) * time.Minute blinds := []int{100, 200, 300, 400, 500, 600, 800, 1000, 2000, 4000, 8000} blindTime := 0 * time.Second for _, blind := range blinds { cli.alerter.ScheduleAlertAt(blindTime, blind) blindTime = blindTime + blindIncrement } } Copy // game.go type Game struct { alerter BlindAlerter store PlayerStore } func (p *Game) Start(numberOfPlayers int) { blindIncrement := time.Duration(5+numberOfPlayers) * time.Minute blinds := []int{100, 200, 300, 400, 500, 600, 800, 1000, 2000, 4000, 8000} blindTime := 0 * time.Second for _, blind := range blinds { p.alerter.ScheduleAlertAt(blindTime, blind) blindTime = blindTime + blindIncrement } } func (p *Game) Finish(winner string) { p.store.RecordWin(winner) } // cli.go type CLI struct { in *bufio.Scanner out io.Writer game *Game } func NewCLI(store PlayerStore, in io.Reader, out io.Writer, alerter BlindAlerter) *CLI { return &CLI{ in: bufio.NewScanner(in), out: out, game: &Game{ alerter: alerter, store: store, }, } } const PlayerPrompt = "Please enter the number of players: " func (cli *CLI) PlayPoker() { fmt.Fprint(cli.out, PlayerPrompt) numberOfPlayersInput := cli.readLine() numberOfPlayers, _ := strconv.Atoi(strings.Trim(numberOfPlayersInput, "\n")) cli.game.Start(numberOfPlayers) winnerInput := cli.readLine() winner := extractWinner(winnerInput) cli.game.Finish(winner) } func extractWinner(userInput string) string { return strings.Replace(userInput, " wins\n", "", 1) } func (cli *CLI) readLine() string { cli.in.Scan() return cli.in.Text() } Copy func NewCLI(in io.Reader, out io.Writer, game *Game) *CLI { return &CLI{ in: bufio.NewScanner(in), out: out, game: game, } } Copy func NewGame(alerter BlindAlerter, store PlayerStore) *Game { return &Game{ alerter:alerter, store:store, } } Copy stdout := &bytes.Buffer{} in := strings.NewReader("7\n") blindAlerter := &SpyBlindAlerter{} game := poker.NewGame(blindAlerter, dummyPlayerStore) cli := poker.NewCLI(in, stdout, game) cli.PlayPoker() Copy // main.go game := poker.NewGame(poker.BlindAlerterFunc(poker.StdOutAlerter), store) cli := poker.NewCLI(os.Stdin, os.Stdout, game) cli.PlayPoker() Copy func TestGame_Start(t *testing.T) { t.Run("schedules alerts on game start for 5 players", func(t *testing.T) { blindAlerter := &poker.SpyBlindAlerter{} game := poker.NewGame(blindAlerter, dummyPlayerStore) game.Start(5) cases := []poker.ScheduledAlert{ {At: 0 * time.Second, Amount: 100}, {At: 10 * time.Minute, Amount: 200}, {At: 20 * time.Minute, Amount: 300}, {At: 30 * time.Minute, Amount: 400}, {At: 40 * time.Minute, Amount: 500}, {At: 50 * time.Minute, Amount: 600}, {At: 60 * time.Minute, Amount: 800}, {At: 70 * time.Minute, Amount: 1000}, {At: 80 * time.Minute, Amount: 2000}, {At: 90 * time.Minute, Amount: 4000}, {At: 100 * time.Minute, Amount: 8000}, } checkSchedulingCases(cases, t, blindAlerter) }) t.Run("schedules alerts on game start for 7 players", func(t *testing.T) { blindAlerter := &poker.SpyBlindAlerter{} game := poker.NewGame(blindAlerter, dummyPlayerStore) game.Start(7) cases := []poker.ScheduledAlert{ {At: 0 * time.Second, Amount: 100}, {At: 12 * time.Minute, Amount: 200}, {At: 24 * time.Minute, Amount: 300}, {At: 36 * time.Minute, Amount: 400}, } checkSchedulingCases(cases, t, blindAlerter) }) } func TestGame_Finish(t *testing.T) { store := &poker.StubPlayerStore{} game := poker.NewGame(dummyBlindAlerter, store) winner := "Ruth" game.Finish(winner) poker.AssertPlayerWin(t, store, winner) } Copy type Game interface { Start(numberOfPlayers int) Finish(winner string) } Copy type GameSpy struct { StartedWith int FinishedWith string } func (g *GameSpy) Start(numberOfPlayers int) { g.StartedWith = numberOfPlayers } func (g *GameSpy) Finish(winner string) { g.FinishedWith = winner } Copy t.Run("it prompts the user to enter the number of players and starts the game", func(t *testing.T) { stdout := &bytes.Buffer{} in := strings.NewReader("7\n") game := &GameSpy{} cli := poker.NewCLI(in, stdout, game) cli.PlayPoker() gotPrompt := stdout.String() wantPrompt := poker.PlayerPrompt if gotPrompt != wantPrompt { t.Errorf("got %q, want %q", gotPrompt, wantPrompt) } if game.StartedWith != 7 { t.Errorf("wanted Start called with 7 but got %d", game.StartedWith) } }) Copy t.Run("it prints an error when a non numeric value is entered and does not start the game", func(t *testing.T) { stdout := &bytes.Buffer{} in := strings.NewReader("Pies\n") game := &GameSpy{} cli := poker.NewCLI(in, stdout, game) cli.PlayPoker() if game.StartCalled { t.Errorf("game should not have started") } }) Copy === RUN TestCLI/it_prints_an_error_when_a_non_numeric_value_is_entered_and_does_not_start_the_game --- FAIL: TestCLI/it_prints_an_error_when_a_non_numeric_value_is_entered_and_does_not_start_the_game (0.00s) CLI_test.go:62: game should not have started Copy numberOfPlayers, err := strconv.Atoi(cli.readLine()) if err != nil { return } Copy gotPrompt := stdout.String() wantPrompt := poker.PlayerPrompt + "you're so silly" if gotPrompt != wantPrompt { t.Errorf("got %q, want %q", gotPrompt, wantPrompt) } Copy === RUN TestCLI/it_prints_an_error_when_a_non_numeric_value_is_entered_and_does_not_start_the_game --- FAIL: TestCLI/it_prints_an_error_when_a_non_numeric_value_is_entered_and_does_not_start_the_game (0.00s) CLI_test.go:70: got 'Please enter the number of players: ', want 'Please enter the number of players: you're so silly' Copy if err != nil { fmt.Fprint(cli.out, "you're so silly") return } Copy wantPrompt := poker.PlayerPrompt + poker.BadPlayerInputErrMsg Copy const BadPlayerInputErrMsg = "Bad value received for number of players, please try again with a number" Copy func assertMessagesSentToUser(t *testing.T, stdout *bytes.Buffer, messages ...string) { t.Helper() want := strings.Join(messages, "") got := stdout.String() if got != want { t.Errorf("got %q sent to stdout but expected %+v", got, messages) } } Copy func TestCLI(t *testing.T) { t.Run("start game with 3 players and finish game with 'Chris' as winner", func(t *testing.T) { game := &GameSpy{} stdout := &bytes.Buffer{} in := userSends("3", "Chris wins") cli := poker.NewCLI(in, stdout, game) cli.PlayPoker() assertMessagesSentToUser(t, stdout, poker.PlayerPrompt) assertGameStartedWith(t, game, 3) assertFinishCalledWith(t, game, "Chris") }) t.Run("start game with 8 players and record 'Cleo' as winner", func(t *testing.T) { game := &GameSpy{} in := userSends("8", "Cleo wins") cli := poker.NewCLI(in, dummyStdOut, game) cli.PlayPoker() assertGameStartedWith(t, game, 8) assertFinishCalledWith(t, game, "Cleo") }) t.Run("it prints an error when a non numeric value is entered and does not start the game", func(t *testing.T) { game := &GameSpy{} stdout := &bytes.Buffer{} in := userSends("pies") cli := poker.NewCLI(in, stdout, game) cli.PlayPoker() assertGameNotStarted(t, game) assertMessagesSentToUser(t, stdout, poker.PlayerPrompt, poker.BadPlayerInputErrMsg) }) } Copy type BlindAlerter interface { ScheduleAlertAt(duration time.Duration, amount int) } // BlindAlerterFunc allows you to implement BlindAlerter with a function type BlindAlerterFunc func(duration time.Duration, amount int) // ScheduleAlertAt is BlindAlerterFunc implementation of BlindAlerter func (a BlindAlerterFunc) ScheduleAlertAt(duration time.Duration, amount int) { a(duration, amount) } --- # Dependency Injection | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/dependency-injection.md) . [**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](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces) before as some understanding of interfaces will be needed for this. There are _a lot_ of misunderstandings around dependency injection around the programming community. Hopefully, this guide will show you how * You don't need a framework * It does not overcomplicate your design * It facilitates testing * It allows you to write great, general-purpose functions. We want to write a function that greets someone, just like we did in the hello-world chapter but this time we are going to be testing the _actual printing_. Just to recap, here is what that function could look like Copy func Greet(name string) { fmt.Printf("Hello, %s", name) } But how can we test this? Calling `fmt.Printf` prints to stdout, which is pretty hard for us to capture using the testing framework. What we need to do is to be able to **inject** (which is just a fancy word for pass in) the dependency of printing. **Our function doesn't need to care** _**where**_ **or** _**how**_ **the printing happens, so we should accept an** _**interface**_ **rather than a concrete type.** If we do that, we can then change the implementation to print to something we control so that we can test it. In "real life" you would inject in something that writes to stdout. If you look at the source code of [`fmt.Printf`](https://pkg.go.dev/fmt#Printf) you can see a way for us to hook in Copy // It returns the number of bytes written and any write error encountered. func Printf(format string, a ...interface{}) (n int, err error) { return Fprintf(os.Stdout, format, a...) } Interesting! Under the hood `Printf` just calls `Fprintf` passing in `os.Stdout`. What exactly _is_ an `os.Stdout`? What does `Fprintf` expect to get passed to it for the 1st argument? An `io.Writer` From this we can infer that `os.Stdout` implements `io.Writer`; `Printf` passes `os.Stdout` to `Fprintf` which expects an `io.Writer`. As you write more Go code you will find this interface popping up a lot because it's a great general purpose interface for "put this data somewhere". So we know under the covers we're ultimately using `Writer` to send our greeting somewhere. Let's use this existing abstraction to make our code testable and more reusable. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/dependency-injection#write-the-test-first) Write the test first ----------------------------------------------------------------------------------------------------------------------------------- The `Buffer` type from the `bytes` package implements the `Writer` interface, because it has the method `Write(p []byte) (n int, err error)`. So we'll use it in our test to send in as our `Writer` and then we can check what was written to it after we invoke `Greet` [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/dependency-injection#try-and-run-the-test) Try and run the test ----------------------------------------------------------------------------------------------------------------------------------- The test will not compile [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/dependency-injection#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- _Listen to the compiler_ and fix the problem. `Hello, Chris di_test.go:16: got '' want 'Hello, Chris'` The test fails. Notice that the name is getting printed out, but it's going to stdout. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/dependency-injection#write-enough-code-to-make-it-pass) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------- Use the writer to send the greeting to the buffer in our test. Remember `fmt.Fprintf` is like `fmt.Printf` but instead takes a `Writer` to send the string to, whereas `fmt.Printf` defaults to stdout. The test now passes. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/dependency-injection#refactor) Refactor ----------------------------------------------------------------------------------------------------------- Earlier the compiler told us to pass in a pointer to a `bytes.Buffer`. This is technically correct but not very useful. To demonstrate this, try wiring up the `Greet` function into a Go application where we want it to print to stdout. `./di.go:14:7: cannot use os.Stdout (type *os.File) as type *bytes.Buffer in argument to Greet` As discussed earlier `fmt.Fprintf` allows you to pass in an `io.Writer` which we know both `os.Stdout` and `bytes.Buffer` implement. If we change our code to use the more general purpose interface we can now use it in both tests and in our application. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/dependency-injection#more-on-io.writer) More on io.Writer ----------------------------------------------------------------------------------------------------------------------------- What other places can we write data to using `io.Writer`? Just how general purpose is our `Greet` function? ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/dependency-injection#the-internet) The Internet Run the following Run the program and go to [http://localhost:5001](http://localhost:5001/) . You'll see your greeting function being used. HTTP servers will be covered in a later chapter so don't worry too much about the details. When you write an HTTP handler, you are given an `http.ResponseWriter` and the `http.Request` that was used to make the request. When you implement your server you _write_ your response using the writer. You can probably guess that `http.ResponseWriter` also implements `io.Writer` so this is why we could re-use our `Greet` function inside our handler. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/dependency-injection#wrapping-up) Wrapping up ----------------------------------------------------------------------------------------------------------------- Our first round of code was not easy to test because it wrote data to somewhere we couldn't control. _Motivated by our tests_ we refactored the code so we could control _where_ the data was written by **injecting a dependency** which allowed us to: * **Test our code** If you can't test a function _easily_, it's usually because of dependencies hard-wired into a function _or_ global state. If you have a global database connection pool for instance that is used by some kind of service layer, it is likely going to be difficult to test and they will be slow to run. DI will motivate you to inject in a database dependency (via an interface) which you can then mock out with something you can control in your tests. * **Separate our concerns**, decoupling _where the data goes_ from _how to generate it_. If you ever feel like a method/function has too many responsibilities (generating data _and_ writing to a db? handling HTTP requests _and_ doing domain level logic?) DI is probably going to be the tool you need. * **Allow our code to be re-used in different contexts** The first "new" context our code can be used in is inside tests. But further on if someone wants to try something new with your function they can inject their own dependencies. ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/dependency-injection#what-about-mocking-i-hear-you-need-that-for-di-and-also-its-evil) What about mocking? I hear you need that for DI and also it's evil Mocking will be covered in detail later (and it's not evil). You use mocking to replace real things you inject with a pretend version that you can control and inspect in your tests. In our case though, the standard library had something ready for us to use. ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/dependency-injection#the-go-standard-library-is-really-good-take-time-to-study-it) The Go standard library is really good, take time to study it By having some familiarity with the `io.Writer` interface we are able to use `bytes.Buffer` in our test as our `Writer` and then we can use other `Writer`s from the standard library to use our function in a command line app or in web server. The more familiar you are with the standard library the more you'll see these general purpose interfaces which you can then re-use in your own code to make your software reusable in a number of contexts. This example is heavily influenced by a chapter in [The Go Programming language](https://www.amazon.co.uk/Programming-Language-Addison-Wesley-Professional-Computing/dp/0134190440) , so if you enjoyed this, go buy it! [PreviousMaps](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/maps) [NextMocking](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/mocking) Last updated 1 year ago * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/dependency-injection#write-the-test-first) * [Try and run the test](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/dependency-injection#try-and-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/dependency-injection#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/dependency-injection#write-enough-code-to-make-it-pass) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/dependency-injection#refactor) * [More on io.Writer](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/dependency-injection#more-on-io.writer) * [The Internet](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/dependency-injection#the-internet) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/dependency-injection#wrapping-up) * [What about mocking? I hear you need that for DI and also it's evil](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/dependency-injection#what-about-mocking-i-hear-you-need-that-for-di-and-also-its-evil) * [The Go standard library is really good, take time to study it](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/dependency-injection#the-go-standard-library-is-really-good-take-time-to-study-it) Copy func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { p := newPrinter() p.doPrintf(format, a) n, err = w.Write(p.buf) p.free() return } Copy type Writer interface { Write(p []byte) (n int, err error) } Copy func TestGreet(t *testing.T) { buffer := bytes.Buffer{} Greet(&buffer, "Chris") got := buffer.String() want := "Hello, Chris" if got != want { t.Errorf("got %q want %q", got, want) } } Copy ./di_test.go:10:2: undefined: Greet Copy func Greet(writer *bytes.Buffer, name string) { fmt.Printf("Hello, %s", name) } Copy func Greet(writer *bytes.Buffer, name string) { fmt.Fprintf(writer, "Hello, %s", name) } Copy func main() { Greet(os.Stdout, "Elodie") } Copy package main import ( "fmt" "io" "os" ) func Greet(writer io.Writer, name string) { fmt.Fprintf(writer, "Hello, %s", name) } func main() { Greet(os.Stdout, "Elodie") } Copy package main import ( "fmt" "io" "log" "net/http" ) func Greet(writer io.Writer, name string) { fmt.Fprintf(writer, "Hello, %s", name) } func MyGreeterHandler(w http.ResponseWriter, r *http.Request) { Greet(w, "world") } func main() { log.Fatal(http.ListenAndServe(":5001", http.HandlerFunc(MyGreeterHandler))) } --- # Intro | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/build-an-application/app-intro.md) . Now that you have hopefully digested the _Go Fundamentals_ section you have a solid grounding of a majority of Go's language features and how to do TDD. This next section will involve building an application. Each chapter will iterate on the previous one, expanding the application's functionality as our product owner dictates. New concepts will be introduced to help facilitate writing great code but most of the new material will be learning what can be accomplished from Go's standard library. By the end of this, you should have a strong grasp as to how to iteratively write an application in Go, backed by tests. * [HTTP server](https://quii.gitbook.io/learn-go-with-tests/build-an-application/http-server) - We will create an application which listens to HTTP requests and responds to them. * [JSON, routing and embedding](https://quii.gitbook.io/learn-go-with-tests/build-an-application/json) - We will make our endpoints return JSON and explore how to do routing. * [IO and sorting](https://quii.gitbook.io/learn-go-with-tests/build-an-application/io) - We will persist and read our data from disk and we'll cover sorting data. * [Command line & project structure](https://quii.gitbook.io/learn-go-with-tests/build-an-application/command-line) - Support multiple applications from one code base and read input from command line. * [Time](https://quii.gitbook.io/learn-go-with-tests/build-an-application/time) - using the `time` package to schedule activities. * [WebSockets](https://quii.gitbook.io/learn-go-with-tests/build-an-application/websockets) - learn how to write and test a server that uses WebSockets. [PreviousRefactoring Checklist](https://quii.gitbook.io/learn-go-with-tests/testing-fundamentals/refactoring-checklist) [NextHTTP server](https://quii.gitbook.io/learn-go-with-tests/build-an-application/http-server) Last updated 4 years ago --- # Concurrency | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/concurrency.md) . [**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. Copy package concurrency type WebsiteChecker func(string) bool func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { results := make(map[string]bool) for _, url := range urls { results[url] = wc(url) } return results } It returns a map of each URL checked to a boolean value: `true` for a good response; `false` for a bad response. You also have to pass in a `WebsiteChecker` which takes a single URL and returns a boolean. This is used by the function to check all the websites. Using [dependency injection](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/dependency-injection) has allowed them to test the function without making real HTTP calls, making it reliable and fast. Here's the test they've written: The function is in production and being used to check hundreds of websites. But your colleague has started to get complaints that it's slow, so they've asked you to help speed it up. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/concurrency#write-a-test) Write a test ---------------------------------------------------------------------------------------------------------- Let's use a benchmark to test the speed of `CheckWebsites` so that we can see the effect of our changes. The benchmark tests `CheckWebsites` using a slice of one hundred urls and uses a new fake implementation of `WebsiteChecker`. `slowStubWebsiteChecker` is deliberately slow. It uses `time.Sleep` to wait exactly twenty milliseconds and then it returns true. When we run the benchmark using `go test -bench=.` (or if you're in Windows Powershell `go test -bench="."`): `CheckWebsites` has been benchmarked at 2249228637 nanoseconds - about two and a quarter seconds. Let's try and make this faster. ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/concurrency#write-enough-code-to-make-it-pass) Write enough code to make it pass Now we can finally talk about concurrency which, for the purposes of the following, means "having more than one thing in progress." This is something that we do naturally everyday. For instance, this morning I made a cup of tea. I put the kettle on and then, while I was waiting for it to boil, I got the milk out of the fridge, got the tea out of the cupboard, found my favourite mug, put the teabag into the cup and then, when the kettle had boiled, I put the water in the cup. What I _didn't_ do was put the kettle on and then stand there blankly staring at the kettle until it boiled, then do everything else once the kettle had boiled. If you can understand why it's faster to make tea the first way, then you can understand how we will make `CheckWebsites` faster. Instead of waiting for a website to respond before sending a request to the next website, we will tell our computer to make the next request while it is waiting. Normally in Go when we call a function `doSomething()` we wait for it to return (even if it has no value to return, we still wait for it to finish). We say that this operation is _blocking_ - it makes us wait for it to finish. An operation that does not block in Go will run in a separate _process_ called a _goroutine_. Think of a process as reading down the page of Go code from top to bottom, going 'inside' each function when it gets called to read what it does. When a separate process starts, it's like another reader begins reading inside the function, leaving the original reader to carry on going down the page. To tell Go to start a new goroutine we turn a function call into a `go` statement by putting the keyword `go` in front of it: `go doSomething()`. Because the only way to start a goroutine is to put `go` in front of a function call, we often use _anonymous functions_ when we want to start a goroutine. An anonymous function literal looks just the same as a normal function declaration, but without a name (unsurprisingly). You can see one above in the body of the `for` loop. Anonymous functions have a number of features which make them useful, two of which we're using above. Firstly, they can be executed at the same time that they're declared - this is what the `()` at the end of the anonymous function is doing. Secondly they maintain access to the lexical scope in which they are defined - all the variables that are available at the point when you declare the anonymous function are also available in the body of the function. The body of the anonymous function above is just the same as the loop body was before. The only difference is that each iteration of the loop will start a new goroutine, concurrent with the current process (the `WebsiteChecker` function). Each goroutine will add its result to the results map. But when we run `go test`: ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/concurrency#a-quick-aside-into-the-concurrency-universe) A quick aside into the concurrency universe... You might not get this result. You might get a panic message that we're going to talk about in a bit. Don't worry if you got that, just keep running the test until you _do_ get the result above. Or pretend that you did. Up to you. Welcome to concurrency: when it's not handled correctly it's hard to predict what's going to happen. Don't worry - that's why we're writing tests, to help us know when we're handling concurrency predictably. ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/concurrency#and-were-back) ... and we're back. We are caught by the original test `CheckWebsites`, it's now returning an empty map. What went wrong? None of the goroutines that our `for` loop started had enough time to add their result to the `results` map; the `CheckWebsites` function is too fast for them, and it returns the still empty map. To fix this we can just wait while all the goroutines do their work, and then return. Two seconds ought to do it, right? Now if you're lucky you'll get: But if you're unlucky (this is more likely if you run them with the benchmark as you'll get more tries) This is long and scary, but all we need to do is take a breath and read the stacktrace: `fatal error: concurrent map writes`. Sometimes, when we run our tests, two of the goroutines write to the results map at exactly the same time. Maps in Go don't like it when more than one thing tries to write to them at once, and so `fatal error`. This is a _data race_, a bug that occurs when two or more goroutines access the same memory location concurrently, and at least one of those accesses is a write. Because we cannot control exactly when each goroutine executes, we are vulnerable to multiple goroutines trying to write to the `results` map at the exact same time. Go maps are not safe for concurrent writes, so the runtime throws a fatal error to prevent memory corruption. Go can help us to spot race conditions with its built in [_race detector_](https://blog.golang.org/race-detector) . To enable this feature, run the tests with the `race` flag: `go test -race`. You should get some output that looks like this: The details are, again, hard to read - but `WARNING: DATA RACE` is pretty unambiguous. Reading into the body of the error we can see two different goroutines performing writes on a map: `Write at 0x00c420084d20 by goroutine 8:` is writing to the same block of memory as `Previous write at 0x00c420084d20 by goroutine 7:` On top of that, we can see the line of code where the write is happening: `/Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:12` and the line of code where goroutines 7 and 8 are started: `/Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:11` Everything you need to know is printed to your terminal - all you have to do is be patient enough to read it. ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/concurrency#channels) Channels We can solve this data race by coordinating our goroutines using _channels_. Channels are a Go data structure that can both receive and send values. These operations, along with their details, allow communication between different processes. In this case we want to think about the communication between the parent process and each of the goroutines that it makes to do the work of running the `WebsiteChecker` function with the url. Alongside the `results` map we now have a `resultChannel`, which we `make` in the same way. `chan result` is the type of the channel - a channel of `result`. The new type, `result` has been made to associate the return value of the `WebsiteChecker` with the url being checked - it's a struct of `string` and `bool`. As we don't need either value to be named, each of them is anonymous within the struct; this can be useful when it's hard to know what to name a value. Now when we iterate over the urls, instead of writing to the `map` directly we're sending a `result` struct for each call to `wc` to the `resultChannel` with a _send statement_. This uses the `<-` operator, taking a channel on the left and a value on the right: The next `for` loop iterates once for each of the urls. Inside we're using a _receive expression_, which assigns a value received from a channel to a variable. This also uses the `<-` operator, but with the two operands now reversed: the channel is now on the right and the variable that we're assigning to is on the left: We then use the `result` received to update the map. By sending the results into a channel, we can control the timing of each write into the results map, ensuring that it happens one at a time. Although each of the calls of `wc`, and each send to the result channel, is happening concurrently inside its own process, each of the results is being dealt with one at a time as we take values out of the result channel with the receive expression. We have used concurrency for the part of the code that we wanted to make faster, while making sure that the part that cannot happen simultaneously still happens linearly. And we have communicated across the multiple processes involved by using channels. When we run the benchmark: 23406615 nanoseconds - 0.023 seconds, about one hundred times as fast as original function. A great success. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/concurrency#wrapping-up) Wrapping up -------------------------------------------------------------------------------------------------------- This exercise has been a little lighter on the TDD than usual. In a way we've been taking part in one long refactoring of the `CheckWebsites` function; the inputs and outputs never changed, it just got faster. But the tests we had in place, as well as the benchmark we wrote, allowed us to refactor `CheckWebsites` in a way that maintained confidence that the software was still working, while demonstrating that it had actually become faster. In making it faster we learned about * _goroutines_, the basic unit of concurrency in Go, which let us manage more than one website check request. * _anonymous functions_, which we used to start each of the concurrent processes that check websites. * _channels_, to help organize and control the communication between the different processes, allowing us to avoid a _race condition_ bug. * _the race detector_ which helped us debug problems with concurrent code ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/concurrency#make-it-fast) Make it fast One formulation of an agile way of building software, often misattributed to Kent Beck, is: > [Make it work, make it right, make it fast](http://wiki.c2.com/?MakeItWorkMakeItRightMakeItFast) Where 'work' is making the tests pass, 'right' is refactoring the code, and 'fast' is optimizing the code to make it, for example, run quickly. We can only 'make it fast' once we've made it work and made it right. We were lucky that the code we were given was already demonstrated to be working, and didn't need to be refactored. We should never try to 'make it fast' before the other two steps have been performed because > [Premature optimization is the root of all evil](http://wiki.c2.com/?PrematureOptimization) > -- Donald Knuth [PreviousMocking](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/mocking) [NextSelect](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select) Last updated 2 months ago * [Write a test](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/concurrency#write-a-test) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/concurrency#write-enough-code-to-make-it-pass) * [A quick aside into the concurrency universe...](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/concurrency#a-quick-aside-into-the-concurrency-universe) * [... and we're back.](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/concurrency#and-were-back) * [Channels](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/concurrency#channels) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/concurrency#wrapping-up) * [Make it fast](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/concurrency#make-it-fast) Copy package concurrency import ( "reflect" "testing" ) func mockWebsiteChecker(url string) bool { return url != "waat://furhurterwe.geds" } func TestCheckWebsites(t *testing.T) { websites := []string{ "http://google.com", "http://blog.gypsydave5.com", "waat://furhurterwe.geds", } want := map[string]bool{ "http://google.com": true, "http://blog.gypsydave5.com": true, "waat://furhurterwe.geds": false, } got := CheckWebsites(mockWebsiteChecker, websites) if !reflect.DeepEqual(want, got) { t.Fatalf("wanted %v, got %v", want, got) } } Copy package concurrency import ( "testing" "time" ) func slowStubWebsiteChecker(_ string) bool { time.Sleep(20 * time.Millisecond) return true } func BenchmarkCheckWebsites(b *testing.B) { urls := make([]string, 100) for i := 0; i < len(urls); i++ { urls[i] = "a url" } for b.Loop() { CheckWebsites(slowStubWebsiteChecker, urls) } } Copy pkg: github.com/gypsydave5/learn-go-with-tests/concurrency/v0 BenchmarkCheckWebsites-4 1 2249228637 ns/op PASS ok github.com/gypsydave5/learn-go-with-tests/concurrency/v0 2.268s Copy package concurrency type WebsiteChecker func(string) bool func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { results := make(map[string]bool) for _, url := range urls { go func() { results[url] = wc(url) }() } return results } Copy --- FAIL: TestCheckWebsites (0.00s) CheckWebsites_test.go:31: Wanted map[http://google.com:true http://blog.gypsydave5.com:true waat://furhurterwe.geds:false], got map[] FAIL exit status 1 FAIL github.com/gypsydave5/learn-go-with-tests/concurrency/v1 0.010s Copy package concurrency import "time" type WebsiteChecker func(string) bool func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { results := make(map[string]bool) for _, url := range urls { go func() { results[url] = wc(url) }() } time.Sleep(2 * time.Second) return results } Copy PASS ok github.com/gypsydave5/learn-go-with-tests/concurrency/v1 2.012s Copy fatal error: concurrent map writes goroutine 8 [running]: runtime.throw(0x12c5895, 0x15) /usr/local/Cellar/go/1.9.3/libexec/src/runtime/panic.go:605 +0x95 fp=0xc420037700 sp=0xc4200376e0 pc=0x102d395 runtime.mapassign_faststr(0x1271d80, 0xc42007acf0, 0x12c6634, 0x17, 0x0) /usr/local/Cellar/go/1.9.3/libexec/src/runtime/hashmap_fast.go:783 +0x4f5 fp=0xc420037780 sp=0xc420037700 pc=0x100eb65 github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker.func1(0xc42007acf0, 0x12d3938, 0x12c6634, 0x17) /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:12 +0x71 fp=0xc4200377c0 sp=0xc420037780 pc=0x12308f1 runtime.goexit() /usr/local/Cellar/go/1.9.3/libexec/src/runtime/asm_amd64.s:2337 +0x1 fp=0xc4200377c8 sp=0xc4200377c0 pc=0x105cf01 created by github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:11 +0xa1 ... many more scary lines of text ... Copy ================== WARNING: DATA RACE Write at 0x00c420084d20 by goroutine 8: runtime.mapassign_faststr() /usr/local/Cellar/go/1.9.3/libexec/src/runtime/hashmap_fast.go:774 +0x0 github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker.func1() /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:12 +0x82 Previous write at 0x00c420084d20 by goroutine 7: runtime.mapassign_faststr() /usr/local/Cellar/go/1.9.3/libexec/src/runtime/hashmap_fast.go:774 +0x0 github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker.func1() /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:12 +0x82 Goroutine 8 (running) created at: github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker() /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:11 +0xc4 github.com/gypsydave5/learn-go-with-tests/concurrency/v3.TestWebsiteChecker() /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker_test.go:27 +0xad testing.tRunner() /usr/local/Cellar/go/1.9.3/libexec/src/testing/testing.go:746 +0x16c Goroutine 7 (finished) created at: github.com/gypsydave5/learn-go-with-tests/concurrency/v3.WebsiteChecker() /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker.go:11 +0xc4 github.com/gypsydave5/learn-go-with-tests/concurrency/v3.TestWebsiteChecker() /Users/gypsydave5/go/src/github.com/gypsydave5/learn-go-with-tests/concurrency/v3/websiteChecker_test.go:27 +0xad testing.tRunner() /usr/local/Cellar/go/1.9.3/libexec/src/testing/testing.go:746 +0x16c ================== Copy package concurrency type WebsiteChecker func(string) bool type result struct { string bool } func CheckWebsites(wc WebsiteChecker, urls []string) map[string]bool { results := make(map[string]bool) resultChannel := make(chan result) for _, url := range urls { go func() { resultChannel <- result{url, wc(url)} }() } for i := 0; i < len(urls); i++ { r := <-resultChannel results[r.string] = r.bool } return results } Copy // Send statement resultChannel <- result{url, wc(url)} Copy // Receive expression r := <-resultChannel Copy pkg: github.com/gypsydave5/learn-go-with-tests/concurrency/v2 BenchmarkCheckWebsites-8 100 23406615 ns/op PASS ok github.com/gypsydave5/learn-go-with-tests/concurrency/v2 2.377s --- # Contributing | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/meta/contributing.md) . Contributions are very welcome. I hope for this to become a great home for guides of how to learn Go by writing tests. Consider submitting a PR or creating an issue which you can do [here](https://github.com/quii/learn-go-with-tests/issues) . [](https://quii.gitbook.io/learn-go-with-tests/meta/contributing#what-were-looking-for) What we're looking for ------------------------------------------------------------------------------------------------------------------- * Teaching Go features (e.g things like `if`, `select`, structs, methods, etc). * Showcase interesting functionality within the standard library. Show off how easy it is to TDD a HTTP server for instance. * Show how Go's tooling, like benchmarking, race detectors, etc can help you arrive at great software. If you don't feel confident to submit your own guide, submitting an issue for something you want to learn is still a valuable contribution. ### [](https://quii.gitbook.io/learn-go-with-tests/meta/contributing#get-feedback-quickly-for-new-content) ⚠️ Get feedback quickly for new content ⚠️ * TDD teaches us to work iteratively and get feedback and I strongly suggest you do the same if you wish to contribute * Open a PR with your first test and implementation, discuss your approach so I can offer feedback and course correct * This is of course open-source but I do have strong opinions on the content. The sooner you talk to me the better. [](https://quii.gitbook.io/learn-go-with-tests/meta/contributing#style-guide) Style guide ---------------------------------------------------------------------------------------------- * Always be reinforcing the TDD cycle. Take a look at the [Chapter Template](https://quii.gitbook.io/learn-go-with-tests/meta/template) . * Emphasis on iterating over functionality driven by tests. The Hello, world example works well because we gradually make it more sophisticated and learning new techniques _driven_ by the tests. For example: * `Hello()` <- how to write functions, return types. * `Hello(name string)` <- arguments, constants. * `Hello(name string)` <- default to "world" using `if`. * `Hello(name, language string)` <- `switch`. * Try and minimise the surface area of required knowledge. * Thinking of examples that showcase what you're trying to teach without confusing the reader with other features is important. * For example you can learn about `struct`s without understanding pointers. * Brevity is king. * Follow the [Code Review Comments style guide](https://go.dev/wiki/CodeReviewComments) . It's important for a consistent style across all the sections. * Your section should have a runnable application at the end (e.g `package main` with a `main` func) so users can see it in action and play with it. * All tests should pass. * Run `./build.sh` before raising PR. [PreviousAnti-patterns](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns) [NextChapter Template](https://quii.gitbook.io/learn-go-with-tests/meta/template) Last updated 2 years ago * [What we're looking for](https://quii.gitbook.io/learn-go-with-tests/meta/contributing#what-were-looking-for) * [⚠️ Get feedback quickly for new content ⚠️](https://quii.gitbook.io/learn-go-with-tests/meta/contributing#get-feedback-quickly-for-new-content) * [Style guide](https://quii.gitbook.io/learn-go-with-tests/meta/contributing#style-guide) --- # Sync | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync.md) . [**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'll start with an unsafe counter and verify its behaviour works in a single-threaded environment. Then we'll exercise its unsafeness, with multiple goroutines trying to use the counter via a test, and fix it. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#write-the-test-first) Write the test first ------------------------------------------------------------------------------------------------------------------- We want our API to give us a method to increment the counter and then retrieve its value. Copy func TestCounter(t *testing.T) { t.Run("incrementing the counter 3 times leaves it at 3", func(t *testing.T) { counter := Counter{} counter.Inc() counter.Inc() counter.Inc() if counter.Value() != 3 { t.Errorf("got %d, want %d", counter.Value(), 3) } }) } [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#try-to-run-the-test) Try to run the test ----------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Let's define `Counter`. Try again and it fails with the following So to finally make the test run we can define those methods It should now run and fail [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#write-enough-code-to-make-it-pass) Write enough code to make it pass --------------------------------------------------------------------------------------------------------------------------------------------- This should be trivial for Go experts like us. We need to keep some state for the counter in our datatype and then increment it on every `Inc` call [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#refactor) Refactor ------------------------------------------------------------------------------------------- There's not a lot to refactor but given we're going to write more tests around `Counter` we'll write a small assertion function `assertCount` so the test reads a bit clearer. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#next-steps) 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. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#write-the-test-first-1) Write the test first --------------------------------------------------------------------------------------------------------------------- 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. > A WaitGroup waits for a collection of goroutines to finish. The main goroutine calls Add to set the number of goroutines to wait for. Then each of the goroutines runs and calls Done when finished. At the same time, Wait can be used to block until all goroutines have finished. By waiting for `wg.Wait()` to finish before making our assertions we can be sure all of our goroutines have attempted to `Inc` the `Counter`. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#try-to-run-the-test-1) Try to run the test ------------------------------------------------------------------------------------------------------------------- The test will _probably_ fail with a different number, but nonetheless it demonstrates it does not work when multiple goroutines are trying to mutate the value of the counter at the same time. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#write-enough-code-to-make-it-pass-1) Write enough code to make it pass ----------------------------------------------------------------------------------------------------------------------------------------------- A simple solution is to add a lock to our `Counter`, ensuring only one goroutine can increment the counter at a time. Go's [`Mutex`](https://golang.org/pkg/sync/#Mutex) provides such a lock: > A Mutex is a mutual exclusion lock. The zero value for a Mutex is an unlocked mutex. 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. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#ive-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 It can be argued that it can make the code a bit more elegant. 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 become _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. ![](https://quii.gitbook.io/learn-go-with-tests/~gitbook/image?url=https%3A%2F%2Fi.imgur.com%2FSWYNpwm.png&width=768&dpr=3&quality=100&sign=b1b75cda&sv=2) Showing how a user of this API can wrongly change the state of the lock _This seems like a really bad idea_ [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#copying-mutexes) Copying mutexes --------------------------------------------------------------------------------------------------------- Our test passes but our code is still a bit dangerous If you run `go vet` on your code you should get an error like the following A look at the documentation of [`sync.Mutex`](https://golang.org/pkg/sync/#Mutex) tells us why > 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. To solve this we should pass in a pointer to our `Counter` instead, so change the signature of `assertCounter` Our tests will no longer compile because we are trying to pass in a `Counter` rather than a `*Counter`. To solve this I prefer to create a constructor which shows readers of your API that it would be better to not initialise the type yourself. Use this function in your tests when initialising `Counter`. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#wrapping-up) Wrapping up ------------------------------------------------------------------------------------------------- We've covered a few things from the [sync package](https://golang.org/pkg/sync/) * `Mutex` allows us to add locks to our data * `WaitGroup` is a means of waiting for goroutines to finish jobs ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#when-to-use-locks-over-channels-and-goroutines) When to use locks over channels and goroutines? [We've previously covered goroutines in the first concurrency chapter](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/concurrency) 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://go.dev/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 mutexes for managing state** ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#go-vet) go vet Remember to use go vet in your build scripts as it can alert you to some subtle bugs in your code before they hit your poor users. ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#dont-use-embedding-because-its-convenient) Don't use embedding because it's convenient * Think about the effect embedding has on your public API. * Do you _really_ want to expose these methods and have people coupling their own code to them? * With respect to mutexes, this could be potentially disastrous in very unpredictable and weird ways, imagine some nefarious code unlocking a mutex when it shouldn't be; this would cause some very strange bugs that will be hard to track down. [PreviousReflection](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/reflection) [NextContext](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/context) Last updated 2 years ago * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#write-the-test-first) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#try-to-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#write-enough-code-to-make-it-pass) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#refactor) * [Next steps](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#next-steps) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#write-the-test-first-1) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#try-to-run-the-test-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#write-enough-code-to-make-it-pass-1) * [I've seen other examples where the sync.Mutex is embedded into the struct.](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#ive-seen-other-examples-where-the-sync.mutex-is-embedded-into-the-struct) * [Copying mutexes](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#copying-mutexes) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#wrapping-up) * [When to use locks over channels and goroutines?](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#when-to-use-locks-over-channels-and-goroutines) * [go vet](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#go-vet) * [Don't use embedding because it's convenient](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/sync#dont-use-embedding-because-its-convenient) Copy ./sync_test.go:9:14: undefined: Counter Copy type Counter struct { } Copy ./sync_test.go:14:10: counter.Inc undefined (type Counter has no field or method Inc) ./sync_test.go:18:13: counter.Value undefined (type Counter has no field or method Value) Copy func (c *Counter) Inc() { } func (c *Counter) Value() int { return 0 } Copy === RUN TestCounter === RUN TestCounter/incrementing_the_counter_3_times_leaves_it_at_3 --- FAIL: TestCounter (0.00s) --- FAIL: TestCounter/incrementing_the_counter_3_times_leaves_it_at_3 (0.00s) sync_test.go:27: got 0, want 3 Copy type Counter struct { value int } func (c *Counter) Inc() { c.value++ } func (c *Counter) Value() int { return c.value } Copy t.Run("incrementing the counter 3 times leaves it at 3", func(t *testing.T) { counter := Counter{} counter.Inc() counter.Inc() counter.Inc() assertCounter(t, counter, 3) }) Copy func assertCounter(t testing.TB, got Counter, want int) { t.Helper() if got.Value() != want { t.Errorf("got %d, want %d", got.Value(), want) } } Copy t.Run("it runs safely concurrently", func(t *testing.T) { wantedCount := 1000 counter := Counter{} var wg sync.WaitGroup wg.Add(wantedCount) for i := 0; i < wantedCount; i++ { go func() { counter.Inc() wg.Done() }() } wg.Wait() assertCounter(t, counter, wantedCount) }) Copy === RUN TestCounter/it_runs_safely_in_a_concurrent_envionment --- FAIL: TestCounter (0.00s) --- FAIL: TestCounter/it_runs_safely_in_a_concurrent_envionment (0.00s) sync_test.go:26: got 939, want 1000 FAIL Copy type Counter struct { mu sync.Mutex value int } func (c *Counter) Inc() { c.mu.Lock() defer c.mu.Unlock() c.value++ } Copy type Counter struct { sync.Mutex value int } Copy func (c *Counter) Inc() { c.Lock() defer c.Unlock() c.value++ } Copy sync/v2/sync_test.go:16: call of assertCounter copies lock value: v1.Counter contains sync.Mutex sync/v2/sync_test.go:39: assertCounter passes lock by value: v1.Counter contains sync.Mutex Copy func assertCounter(t testing.TB, got *Counter, want int) Copy func NewCounter() *Counter { return &Counter{} } --- # Error types | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/error-types.md) . [**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.** Pedro on the Gopher Slack asks > If I’m creating an error like `fmt.Errorf("%s must be foo, got %s", bar, baz)`, is there a way to test equality without comparing the string value? Let's make up a function to help explore this idea. Copy // DumbGetter will get the string body of url if it gets a 200 func DumbGetter(url string) (string, error) { res, err := http.Get(url) if err != nil { return "", fmt.Errorf("problem fetching from %s, %v", url, err) } if res.StatusCode != http.StatusOK { return "", fmt.Errorf("did not get 200 from %s, got %d", url, res.StatusCode) } defer res.Body.Close() body, _ := io.ReadAll(res.Body) // ignoring err for brevity return string(body), nil } It's not uncommon to write a function that might fail for different reasons and we want to make sure we handle each scenario correctly. As Pedro says, we _could_ write a test for the status error like so. This test creates a server which always returns `StatusTeapot` and then we use its URL as the argument to `DumbGetter` so we can see it handles non `200` responses correctly. [](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/error-types#problems-with-this-way-of-testing) Problems with this way of testing ---------------------------------------------------------------------------------------------------------------------------------------------------------- This book tries to emphasise _listen to your tests_ and this test doesn't _feel_ good: * We're constructing the same string as production code does to test it * It's annoying to read and write * Is the exact error message string what we're _actually concerned with_ ? What does this tell us? The ergonomics of our test would be reflected on another bit of code trying to use our code. How does a user of our code react to the specific kind of errors we return? The best they can do is look at the error string which is extremely error prone and horrible to write. [](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/error-types#what-we-should-do) What we should do -------------------------------------------------------------------------------------------------------------------------- With TDD we have the benefit of getting into the mindset of: > How would _I_ want to use this code? What we could do for `DumbGetter` is provide a way for users to use the type system to understand what kind of error has happened. What if `DumbGetter` could return us something like Rather than a magical string, we have actual _data_ to work with. Let's change our existing test to reflect this need We'll have to make `BadStatusError` implement the error interface. ### [](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/error-types#what-does-the-test-do) What does the test do? Instead of checking the exact string of the error, we are doing a [type assertion](https://tour.golang.org/methods/15) on the error to see if it is a `BadStatusError`. This reflects our desire for the _kind_ of error clearer. Assuming the assertion passes we can then check the properties of the error are correct. When we run the test, it tells us we didn't return the right kind of error Let's fix `DumbGetter` by updating our error handling code to use our type This change has had some _real positive effects_ * Our `DumbGetter` function has become simpler, it's no longer concerned with the intricacies of an error string, it just creates a `BadStatusError`. * Our tests now reflect (and document) what a user of our code _could_ do if they decided they wanted to do some more sophisticated error handling than just logging. Just do a type assertion and then you get easy access to the properties of the error. * It is still "just" an `error`, so if they choose to they can pass it up the call stack or log it like any other `error`. [](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/error-types#wrapping-up) Wrapping up -------------------------------------------------------------------------------------------------------------- If you find yourself testing for multiple error conditions don't fall in to the trap of comparing the error messages. This leads to flaky and difficult to read/write tests and it reflects the difficulties the users of your code will have if they also need to start doing things differently depending on the kind of errors that have occurred. Always make sure your tests reflect how _you'd_ like to use your code, so in this respect consider creating error types to encapsulate your kinds of errors. This makes handling different kinds of errors easier for users of your code and also makes writing your error handling code simpler and easier to read. [](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/error-types#addendum) Addendum -------------------------------------------------------------------------------------------------------- As of Go 1.13 there are new ways to work with errors in the standard library which is covered in the [Go Blog](https://blog.golang.org/go1.13-errors) In this case we are using [`errors.As`](https://pkg.go.dev/errors#example-As) to try and extract our error into our custom type. It returns a `bool` to denote success and extracts it into `got` for us. [PreviousOS Exec](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/os-exec) [NextContext-aware Reader](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/context-aware-reader) Last updated 1 year ago * [Problems with this way of testing](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/error-types#problems-with-this-way-of-testing) * [What we should do](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/error-types#what-we-should-do) * [What does the test do?](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/error-types#what-does-the-test-do) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/error-types#wrapping-up) * [Addendum](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/error-types#addendum) Copy t.Run("when you don't get a 200 you get a status error", func(t *testing.T) { svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { res.WriteHeader(http.StatusTeapot) })) defer svr.Close() _, err := DumbGetter(svr.URL) if err == nil { t.Fatal("expected an error") } want := fmt.Sprintf("did not get 200 from %s, got %d", svr.URL, http.StatusTeapot) got := err.Error() if got != want { t.Errorf(`got "%v", want "%v"`, got, want) } }) Copy type BadStatusError struct { URL string Status int } Copy t.Run("when you don't get a 200 you get a status error", func(t *testing.T) { svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { res.WriteHeader(http.StatusTeapot) })) defer svr.Close() _, err := DumbGetter(svr.URL) if err == nil { t.Fatal("expected an error") } got, isStatusErr := err.(BadStatusError) if !isStatusErr { t.Fatalf("was not a BadStatusError, got %T", err) } want := BadStatusError{URL: svr.URL, Status: http.StatusTeapot} if got != want { t.Errorf("got %v, want %v", got, want) } }) Copy func (b BadStatusError) Error() string { return fmt.Sprintf("did not get 200 from %s, got %d", b.URL, b.Status) } Copy --- FAIL: TestDumbGetter (0.00s) --- FAIL: TestDumbGetter/when_you_dont_get_a_200_you_get_a_status_error (0.00s) error-types_test.go:56: was not a BadStatusError, got *errors.errorString Copy if res.StatusCode != http.StatusOK { return "", BadStatusError{URL: url, Status: res.StatusCode} } Copy t.Run("when you don't get a 200 you get a status error", func(t *testing.T) { svr := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { res.WriteHeader(http.StatusTeapot) })) defer svr.Close() _, err := DumbGetter(svr.URL) if err == nil { t.Fatal("expected an error") } var got BadStatusError isBadStatusError := errors.As(err, &got) want := BadStatusError{URL: svr.URL, Status: http.StatusTeapot} if !isBadStatusError { t.Fatalf("was not a BadStatusError, got %T", err) } if got != want { t.Errorf("got %v, want %v", got, want) } }) --- # Arrays and slices | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices.md) . [**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. When you have arrays, it is very common to have to iterate over them. So let's use [our new-found knowledge of `for`](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/iteration) to make a `Sum` function. `Sum` will take an array of numbers and return the total. Let's use our TDD skills [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-the-test-first) Write the test first -------------------------------------------------------------------------------------------------------------------------------- Create a new folder to work in. Create a new file called `sum_test.go` and insert the following: Copy package main import "testing" func TestSum(t *testing.T) { numbers := [5]int{1, 2, 3, 4, 5} got := Sum(numbers) want := 15 if got != want { t.Errorf("got %d want %d given, %v", got, want, numbers) } } Arrays have a _fixed capacity_ which you define when you declare the variable. We can initialize an array in two ways: * \[N\]type{value1, value2, ..., valueN} e.g. `numbers := [5]int{1, 2, 3, 4, 5}` * \[...\]type{value1, value2, ..., valueN} e.g. `numbers := [...]int{1, 2, 3, 4, 5}` It is sometimes useful to also print the inputs to the function in the error message. Here, we are using the `%v` placeholder to print the "default" format, which works well for arrays. [Read more about the format strings](https://golang.org/pkg/fmt/) [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#try-to-run-the-test) Try to run the test ------------------------------------------------------------------------------------------------------------------------------ If you had initialized go mod with `go mod init main` you will be presented with an error `_testmain.go:13:2: cannot import "main"`. This is because according to common practice, package main will only contain integration of other packages and not unit-testable code and hence Go will not allow you to import a package with name `main`. To fix this, you can rename the main module in `go.mod` to any other name. Once the above error is fixed, if you run `go test` the compiler will fail with the familiar `./sum_test.go:10:15: undefined: Sum` error. Now we can proceed with writing the actual method to be tested. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- In `sum.go` Your test should now fail with _a clear error message_ `sum_test.go:13: got 0 want 15 given, [1 2 3 4 5]` [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-enough-code-to-make-it-pass) Write enough code to make it pass ---------------------------------------------------------------------------------------------------------------------------------------------------------- To get the value out of an array at a particular index, just use `array[index]` syntax. In this case, we are using `for` to iterate 5 times to work through the array and add each item onto `sum`. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#refactor) Refactor -------------------------------------------------------------------------------------------------------- Let's introduce [`range`](https://gobyexample.com/range) to help clean up our code `range` lets you iterate over an array. On each iteration, `range` returns two values - the index and the value. We are choosing to ignore the index value by using `_` [blank identifier](https://golang.org/doc/effective_go.html#blank) . ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#arrays-and-their-type) Arrays and their type An interesting property of arrays is that the size is encoded in its type. If you try to pass an `[4]int` into a function that expects `[5]int`, it won't compile. They are different types so it's just the same as trying to pass a `string` into a function that wants an `int`. You may be thinking it's quite cumbersome that arrays have a fixed length, and most of the time you probably won't be using them! Go has _slices_ which do not encode the size of the collection and instead can have any size. The next requirement will be to sum collections of varying sizes. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-the-test-first-1) Write the test first ---------------------------------------------------------------------------------------------------------------------------------- We will now use the [slice type](https://golang.org/doc/effective_go.html#slices) which allows us to have collections of any size. The syntax is very similar to arrays, you just omit the size when declaring them `mySlice := []int{1,2,3}` rather than `myArray := [3]int{1,2,3}` [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#try-and-run-the-test) Try and run the test -------------------------------------------------------------------------------------------------------------------------------- This does not compile `./sum_test.go:22:13: cannot use numbers (type []int) as type [5]int in argument to Sum` [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) Write the minimal amount of code for the test to run and check the failing test output ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- The problem here is we can either * Break the existing API by changing the argument to `Sum` to be a slice rather than an array. When we do this, we will potentially ruin someone's day because our _other_ test will no longer compile! * Create a new function In our case, no one else is using our function, so rather than having two functions to maintain, let's have just one. If you try to run the tests they will still not compile, you will have to change the first test to pass in a slice rather than an array. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-enough-code-to-make-it-pass-1) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------ It turns out that fixing the compiler problems were all we need to do here and the tests pass! [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#refactor-1) Refactor ---------------------------------------------------------------------------------------------------------- We already refactored `Sum` - all we did was replace arrays with slices, so no extra changes are required. Remember that we must not neglect our test code in the refactoring stage - we can further improve our `Sum` tests. It is important to question the value of your tests. It should not be a goal to have as many tests as possible, but rather to have as much _confidence_ as possible in your code base. Having too many tests can turn in to a real problem and it just adds more overhead in maintenance. **Every test has a cost**. In our case, you can see that having two tests for this function is redundant. If it works for a slice of one size it's very likely it'll work for a slice of any size (within reason). Go's built-in testing toolkit features a [coverage tool](https://blog.golang.org/cover) . Whilst striving for 100% coverage should not be your end goal, the coverage tool can help identify areas of your code not covered by tests. If you have been strict with TDD, it's quite likely you'll have close to 100% coverage anyway. Try running `go test -cover` You should see Now delete one of the tests and check the coverage again. Now that we are happy we have a well-tested function you should commit your great work before taking on the next challenge. We need a new function called `SumAll` which will take a varying number of slices, returning a new slice containing the totals for each slice passed in. For example `SumAll([]int{1,2}, []int{0,9})` would return `[]int{3, 9}` or `SumAll([]int{1,1,1})` would return `[]int{3}` [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-the-test-first-2) Write the test first ---------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#try-and-run-the-test-1) Try and run the test ---------------------------------------------------------------------------------------------------------------------------------- `./sum_test.go:23:9: undefined: SumAll` [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-2) Write the minimal amount of code for the test to run and check the failing test output ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- We need to define `SumAll` according to what our test wants. Go can let you write [_variadic functions_](https://gobyexample.com/variadic-functions) that can take a variable number of arguments. This is valid, but our tests still won't compile! `./sum_test.go:26:9: invalid operation: got != want (slice can only be compared to nil)` Go does not let you use equality operators with slices. You _could_ write a function to iterate over each `got` and `want` slice and check their values, but what if we had a more convenient way to do this? From Go 1.21, [slices](https://pkg.go.dev/slices#pkg-overview) standard package is available, which has [slices.Equal](https://pkg.go.dev/slices#Equal) function to do a simple shallow compare on slices, where you don't need to worry about the types like the above case. Note that this function expects the elements to be [comparable](https://pkg.go.dev/builtin#comparable) . So, it can't be applied to slices with non-comparable elements like 2D slices. Let's go ahead and put this into practice! You should have test output like the following: `sum_test.go:30: got [] want [3 9]` [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-enough-code-to-make-it-pass-2) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------ What we need to do is iterate over the varargs, calculate the sum using our existing `Sum` function, then add it to the slice we will return Lots of new things to learn! There's a new way to create a slice. `make` allows you to create a slice with a starting capacity of the `len` of the `numbersToSum` we need to work through. The length of a slice is the number of elements it holds `len(mySlice)`, while the capacity is the number of elements it can hold in the underlying array `cap(mySlice)`, e.g., `make([]int, 0, 5)` creates a slice with length 0 and capacity 5. You can index slices like arrays with `mySlice[N]` to get the value out or assign it a new value with `=` The tests should now pass. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#refactor-2) Refactor ---------------------------------------------------------------------------------------------------------- As mentioned, slices have a capacity. If you have a slice with a capacity of 2 and try to do `mySlice[10] = 1` you will get a _runtime_ error. However, you can use the `append` function which takes a slice and a new value, then returns a new slice with all the items in it. In this implementation, we are worrying less about capacity. We start with an empty slice `sums` and append to it the result of `Sum` as we work through the varargs. Our next requirement is to change `SumAll` to `SumAllTails`, where it will calculate the totals of the "tails" of each slice. The tail of a collection is all items in the collection except the first one (the "head"). [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-the-test-first-3) Write the test first ---------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#try-and-run-the-test-2) Try and run the test ---------------------------------------------------------------------------------------------------------------------------------- `./sum_test.go:26:9: undefined: SumAllTails` [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-3) Write the minimal amount of code for the test to run and check the failing test output ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Rename the function to `SumAllTails` and re-run the test `sum_test.go:30: got [3 9] want [2 9]` [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-enough-code-to-make-it-pass-3) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------ Slices can be sliced! The syntax is `slice[low:high]`. If you omit the value on one of the sides of the `:` it captures everything to that side of it. In our case, we are saying "take from 1 to the end" with `numbers[1:]`. You may wish to spend some time writing other tests around slices and experiment with the slice operator to get more familiar with it. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#refactor-3) Refactor ---------------------------------------------------------------------------------------------------------- Not a lot to refactor this time. What do you think would happen if you passed in an empty slice into our function? What is the "tail" of an empty slice? What happens when you tell Go to capture all elements from `myEmptySlice[1:]`? [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-the-test-first-4) Write the test first ---------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#try-and-run-the-test-3) Try and run the test ---------------------------------------------------------------------------------------------------------------------------------- Oh no! It's important to note that while the test _has compiled_, it _has a runtime error_. Compile time errors are our friend because they help us write software that works, runtime errors are our enemies because they affect our users. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-enough-code-to-make-it-pass-4) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------ [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#refactor-4) Refactor ---------------------------------------------------------------------------------------------------------- Our tests have some repeated code around the assertions again, so let's extract those into a function. We could've created a new function `checkSums` like we normally do, but in this case, we're showing a new technique, assigning a function to a variable. It might look strange but, it's no different to assigning a variable to a `string`, or an `int`, functions in effect are values too. It's not shown here, but this technique can be useful when you want to bind a function to other local variables in "scope" (e.g between some `{}`). It also allows you to reduce the surface area of your API. By defining this function inside the test, it cannot be used by other functions in this package. Hiding variables and functions that don't need to be exported is an important design consideration. A handy side-effect of this is this adds a little type-safety to our code. If a developer mistakenly adds a new test with `checkSums(t, got, "dave")` the compiler will stop them in their tracks. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#wrapping-up) Wrapping up -------------------------------------------------------------------------------------------------------------- We have covered * Arrays * Slices * The various ways to make them * How they have a _fixed_ capacity but you can create new slices from old ones using `append` * How to slice, slices! * `len` to get the length of an array or slice * Test coverage tool * `reflect.DeepEqual` and why it's useful but can reduce the type-safety of your code We've used slices and arrays with integers but they work with any other type too, including arrays/slices themselves. So you can declare a variable of `[][]string` if you need to. [Check out the Go blog post on slices](https://blog.golang.org/go-slices-usage-and-internals) for an in-depth look into slices. Try writing more tests to solidify what you learn from reading it. 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 will not affect the original array. [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. [PreviousIteration](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/iteration) [NextStructs, methods & interfaces](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces) Last updated 7 months ago * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-the-test-first) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#try-to-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-enough-code-to-make-it-pass) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#refactor) * [Arrays and their type](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#arrays-and-their-type) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-the-test-first-1) * [Try and run the test](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#try-and-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-enough-code-to-make-it-pass-1) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#refactor-1) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-the-test-first-2) * [Try and run the test](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#try-and-run-the-test-1) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-2) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-enough-code-to-make-it-pass-2) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#refactor-2) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-the-test-first-3) * [Try and run the test](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#try-and-run-the-test-2) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-3) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-enough-code-to-make-it-pass-3) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#refactor-3) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-the-test-first-4) * [Try and run the test](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#try-and-run-the-test-3) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#write-enough-code-to-make-it-pass-4) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#refactor-4) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices#wrapping-up) Copy package main func Sum(numbers [5]int) int { return 0 } Copy func Sum(numbers [5]int) int { sum := 0 for i := 0; i < 5; i++ { sum += numbers[i] } return sum } Copy func Sum(numbers [5]int) int { sum := 0 for _, number := range numbers { sum += number } return sum } Copy func TestSum(t *testing.T) { t.Run("collection of 5 numbers", func(t *testing.T) { numbers := [5]int{1, 2, 3, 4, 5} got := Sum(numbers) want := 15 if got != want { t.Errorf("got %d want %d given, %v", got, want, numbers) } }) t.Run("collection of any size", func(t *testing.T) { numbers := []int{1, 2, 3} got := Sum(numbers) want := 6 if got != want { t.Errorf("got %d want %d given, %v", got, want, numbers) } }) } Copy func Sum(numbers []int) int { sum := 0 for _, number := range numbers { sum += number } return sum } Copy func TestSum(t *testing.T) { t.Run("collection of 5 numbers", func(t *testing.T) { numbers := []int{1, 2, 3, 4, 5} got := Sum(numbers) want := 15 if got != want { t.Errorf("got %d want %d given, %v", got, want, numbers) } }) t.Run("collection of any size", func(t *testing.T) { numbers := []int{1, 2, 3} got := Sum(numbers) want := 6 if got != want { t.Errorf("got %d want %d given, %v", got, want, numbers) } }) } Copy PASS coverage: 100.0% of statements Copy func TestSumAll(t *testing.T) { got := SumAll([]int{1, 2}, []int{0, 9}) want := []int{3, 9} if got != want { t.Errorf("got %v want %v", got, want) } } Copy func SumAll(numbersToSum ...[]int) []int { return nil } Copy func TestSumAll(t *testing.T) { got := SumAll([]int{1, 2}, []int{0, 9}) want := []int{3, 9} if !slices.Equal(got, want) { t.Errorf("got %v want %v", got, want) } } Copy func SumAll(numbersToSum ...[]int) []int { lengthOfNumbers := len(numbersToSum) sums := make([]int, lengthOfNumbers) for i, numbers := range numbersToSum { sums[i] = Sum(numbers) } return sums } Copy func SumAll(numbersToSum ...[]int) []int { var sums []int for _, numbers := range numbersToSum { sums = append(sums, Sum(numbers)) } return sums } Copy func TestSumAllTails(t *testing.T) { got := SumAllTails([]int{1, 2}, []int{0, 9}) want := []int{2, 9} if !reflect.DeepEqual(got, want) { t.Errorf("got %v want %v", got, want) } } Copy func SumAllTails(numbersToSum ...[]int) []int { var sums []int for _, numbers := range numbersToSum { tail := numbers[1:] sums = append(sums, Sum(tail)) } return sums } Copy func TestSumAllTails(t *testing.T) { t.Run("make the sums of some slices", func(t *testing.T) { got := SumAllTails([]int{1, 2}, []int{0, 9}) want := []int{2, 9} if !reflect.DeepEqual(got, want) { t.Errorf("got %v want %v", got, want) } }) t.Run("safely sum empty slices", func(t *testing.T) { got := SumAllTails([]int{}, []int{3, 4, 5}) want := []int{0, 9} if !reflect.DeepEqual(got, want) { t.Errorf("got %v want %v", got, want) } }) } Copy panic: runtime error: slice bounds out of range [recovered] panic: runtime error: slice bounds out of range Copy func SumAllTails(numbersToSum ...[]int) []int { var sums []int for _, numbers := range numbersToSum { if len(numbers) == 0 { sums = append(sums, 0) } else { tail := numbers[1:] sums = append(sums, Sum(tail)) } } return sums } Copy func TestSumAllTails(t *testing.T) { checkSums := func(t testing.TB, got, want []int) { t.Helper() if !reflect.DeepEqual(got, want) { t.Errorf("got %v want %v", got, want) } } t.Run("make the sums of tails of", func(t *testing.T) { got := SumAllTails([]int{1, 2}, []int{0, 9}) want := []int{2, 9} checkSums(t, got, want) }) t.Run("safely sum empty slices", func(t *testing.T) { got := SumAllTails([]int{}, []int{3, 4, 5}) want := []int{0, 9} checkSums(t, got, want) }) } Copy $ go test ./sum_test.go:52:21: cannot use "dave" (type string) as type []int in argument to checkSums --- # Select | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select.md) . [**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`. For this, we will be using: * `net/http` to make the HTTP calls. * `net/http/httptest` to help us test them. * goroutines. * `select` to synchronise processes. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#write-the-test-first) Write the test first --------------------------------------------------------------------------------------------------------------------- Let's start with something naive to get us going. Copy func TestRacer(t *testing.T) { slowURL := "http://www.facebook.com" fastURL := "http://www.quii.dev" want := fastURL got := Racer(slowURL, fastURL) if got != want { t.Errorf("got %q, want %q", got, want) } } We know this isn't perfect and has problems, but it's a start. It's important not to get too hung-up on getting things perfect first time. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#try-to-run-the-test) Try to run the test ------------------------------------------------------------------------------------------------------------------- `./racer_test.go:14:9: undefined: Racer` [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- `racer_test.go:25: got '', want 'http://www.quii.dev'` [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#write-enough-code-to-make-it-pass) Write enough code to make it pass ----------------------------------------------------------------------------------------------------------------------------------------------- For each URL: 1. We use `time.Now()` to record just before we try and get the `URL`. 2. Then we use [`http.Get`](https://golang.org/pkg/net/http/#Client.Get) to try and perform an HTTP `GET` request against the `URL`. This function returns an [`http.Response`](https://golang.org/pkg/net/http/#Response) and an `error` but so far we are not interested in these values. 3. `time.Since` takes the start time and returns a `time.Duration` of the difference. Once we have done this we simply compare the durations to see which is the quickest. ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#problems) Problems This may or may not make the test pass for you. The problem is we're reaching out to real websites to test our own logic. Testing code that uses HTTP is so common that Go has tools in the standard library to help you test it. In the mocking and dependency injection chapters, we covered how ideally we don't want to be relying on external services to test our code because they can be * Slow * Flaky * Can't test edge cases In the standard library, there is a package called [`net/http/httptest`](https://golang.org/pkg/net/http/httptest/) which enables users to easily create a mock HTTP server. Let's change our tests to use mocks so we have reliable servers to test against that we can control. The syntax may look a bit busy but just take your time. `httptest.NewServer` takes an `http.HandlerFunc` which we are sending in via an _anonymous function_. `http.HandlerFunc` is a type that looks like this: `type HandlerFunc func(ResponseWriter, *Request)`. All it's really saying is it needs a function that takes a `ResponseWriter` and a `Request`, which is not too surprising for an HTTP server. It turns out there's really no extra magic here, **this is also how you would write a** _**real**_ **HTTP server in Go**. The only difference is we are wrapping it in an `httptest.NewServer` which makes it easier to use with testing, as it finds an open port to listen on and then you can close it when you're done with your test. Inside our two servers, we make the slow one have a short `time.Sleep` when we get a request to make it slower than the other one. Both servers then write an `OK` response with `w.WriteHeader(http.StatusOK)` back to the caller. If you re-run the test it will definitely pass now and should be faster. Play with these sleeps to deliberately break the test. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#refactor) Refactor --------------------------------------------------------------------------------------------- We have some duplication in both our production code and test code. This DRY-ing up makes our `Racer` code a lot easier to read. We've refactored creating our fake servers into a function called `makeDelayedServer` to move some uninteresting code out of the test and reduce repetition. ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#defer) `defer` By prefixing a function call with `defer` it will now call that function _at the end of the containing function_. Sometimes you will need to clean up resources, such as closing a file or in our case closing a server so that it does not continue to listen to a port. You want this to execute at the end of the function, but keep the instruction near where you created the server for the benefit of future readers of the code. Our refactoring is an improvement and is a reasonable solution given the Go features covered so far, but we can make the solution simpler. ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#synchronising-processes) Synchronising processes * Why are we testing the speeds of the websites one after another when Go is great at concurrency? We should be able to check both at the same time. * We don't really care about _the exact response times_ of the requests, we just want to know which one comes back first. To do this, we're going to introduce a new construct called `select` which helps us synchronise processes really easily and clearly. #### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#ping) `ping` 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! Why `struct{}` and not another type like a `bool`? Well, a `chan struct{}` is the smallest data type available from a memory perspective so we get no allocation versus a `bool`. Since we are closing and not sending anything on the chan, why allocate anything? Inside the same function, we start a goroutine which will send a signal into that channel once we have completed `http.Get(url)`. **Always** `**make**` **channels** Notice how we have to use `make` when creating a channel; rather than say `var ch chan struct{}`. When you use `var` the variable will be initialised with the "zero" value of the type. So for `string` it is `""`, `int` it is 0, etc. For channels the zero value is `nil` and if you try and send to it with `<-` it will block forever because you cannot send to `nil` channels [You can see this in action in The Go Playground](https://play.golang.org/p/IIbeAox5jKA) #### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#select) `select` You'll recall from the concurrency chapter that you can wait for values to be sent to a channel with `myVar := <-ch`. This is a _blocking_ call, as you're waiting for a value. `select` allows you to wait on _multiple_ channels. The first one to send a value "wins" and the code underneath the `case` is executed. We use `ping` in our `select` to set up two channels, one for each of our `URL`s. Whichever one writes to its channel first will have its code executed in the `select`, which results in its `URL` being returned (and being the winner). After these changes, the intent behind our code is very clear and the implementation is actually simpler. ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#timeouts) Timeouts Our final requirement was to return an error if `Racer` takes longer than 10 seconds. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#write-the-test-first-1) Write the test first ----------------------------------------------------------------------------------------------------------------------- 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`. Note that we've also handled the error return in our original test, we're using `_` for now to ensure the tests will run. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#try-to-run-the-test-1) Try to run the test --------------------------------------------------------------------------------------------------------------------- `./racer_test.go:37:10: assignment mismatch: 2 variables but Racer returns 1 value` [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) Write the minimal amount of code for the test to run and check the failing test output ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Change the signature of `Racer` to return the winner and an `error`. Return `nil` for our happy cases. The compiler will complain about your _first test_ only looking for one value so change that line to `got, err := Racer(slowURL, fastURL)`, knowing that we should check we _don't_ get an error in our happy scenario. If you run it now after 11 seconds it will fail. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#write-enough-code-to-make-it-pass-1) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------- `time.After` is a very handy function when using `select`. Although it didn't happen in our case you can potentially write code that blocks forever if the channels you're listening on never return a value. `time.After` returns a `chan` (like `ping`) and will send a signal down it after the amount of time you define. For us this is perfect; if `a` or `b` manage to return they win, but if we get to 10 seconds then our `time.After` will send a signal and we'll return an `error`. ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#slow-tests) Slow tests The problem we have is that this test takes 10 seconds to run. For such a simple bit of logic, this doesn't feel great. What we can do is make the timeout configurable. So in our test, we can have a very short timeout and then when the code is used in the real world it can be set to 10 seconds. Our tests now won't compile because we're not supplying a timeout. Before rushing in to add this default value to both our tests let's _listen to them_. * Do we care about the timeout in the "happy" test? * The requirements were explicit about the timeout. Given this knowledge, let's do a little refactoring to be sympathetic to both our tests and the users of our code. Our users and our first test can use `Racer` (which uses `ConfigurableRacer` under the hood) and our sad path test can use `ConfigurableRacer`. I added one final check on the first test to verify we don't get an `error`. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#wrapping-up) Wrapping up --------------------------------------------------------------------------------------------------- ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#select-1) `select` * Helps you wait on multiple channels. * Sometimes you'll want to include `time.After` in one of your `cases` to prevent your system blocking forever. ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#httptest) `httptest` * A convenient way of creating test servers so you can have reliable and controllable tests. * Uses the same interfaces as the "real" `net/http` servers which is consistent and less for you to learn. [PreviousConcurrency](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/concurrency) [NextReflection](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/reflection) Last updated 2 years ago * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#write-the-test-first) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#try-to-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#write-enough-code-to-make-it-pass) * [Problems](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#problems) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#refactor) * [defer](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#defer) * [Synchronising processes](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#synchronising-processes) * [Timeouts](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#timeouts) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#write-the-test-first-1) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#try-to-run-the-test-1) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#write-enough-code-to-make-it-pass-1) * [Slow tests](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#slow-tests) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#wrapping-up) * [select](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#select-1) * [httptest](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/select#httptest) Copy func Racer(a, b string) (winner string) { return } Copy func Racer(a, b string) (winner string) { startA := time.Now() http.Get(a) aDuration := time.Since(startA) startB := time.Now() http.Get(b) bDuration := time.Since(startB) if aDuration < bDuration { return a } return b } Copy func TestRacer(t *testing.T) { slowServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(20 * time.Millisecond) w.WriteHeader(http.StatusOK) })) fastServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) slowURL := slowServer.URL fastURL := fastServer.URL want := fastURL got := Racer(slowURL, fastURL) if got != want { t.Errorf("got %q, want %q", got, want) } slowServer.Close() fastServer.Close() } Copy func Racer(a, b string) (winner string) { aDuration := measureResponseTime(a) bDuration := measureResponseTime(b) if aDuration < bDuration { return a } return b } func measureResponseTime(url string) time.Duration { start := time.Now() http.Get(url) return time.Since(start) } Copy func TestRacer(t *testing.T) { slowServer := makeDelayedServer(20 * time.Millisecond) fastServer := makeDelayedServer(0 * time.Millisecond) defer slowServer.Close() defer fastServer.Close() slowURL := slowServer.URL fastURL := fastServer.URL want := fastURL got := Racer(slowURL, fastURL) if got != want { t.Errorf("got %q, want %q", got, want) } } func makeDelayedServer(delay time.Duration) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(delay) w.WriteHeader(http.StatusOK) })) } Copy func Racer(a, b string) (winner string) { select { case <-ping(a): return a case <-ping(b): return b } } func ping(url string) chan struct{} { ch := make(chan struct{}) go func() { http.Get(url) close(ch) }() return ch } Copy 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) defer slowServer.Close() defer fastServer.Close() slowURL := slowServer.URL fastURL := fastServer.URL want := fastURL got, _ := Racer(slowURL, fastURL) if got != want { t.Errorf("got %q, want %q", got, want) } }) t.Run("returns an error if a server doesn't respond within 10s", func(t *testing.T) { serverA := makeDelayedServer(11 * time.Second) serverB := makeDelayedServer(12 * time.Second) defer serverA.Close() defer serverB.Close() _, err := Racer(serverA.URL, serverB.URL) if err == nil { t.Error("expected an error but didn't get one") } }) } Copy func Racer(a, b string) (winner string, error error) { select { case <-ping(a): return a, nil case <-ping(b): return b, nil } } Copy --- FAIL: TestRacer (12.00s) --- FAIL: TestRacer/returns_an_error_if_a_server_doesn't_respond_within_10s (12.00s) racer_test.go:40: expected an error but didn't get one Copy func Racer(a, b string) (winner string, error error) { select { case <-ping(a): return a, nil case <-ping(b): return b, nil case <-time.After(10 * time.Second): return "", fmt.Errorf("timed out waiting for %s and %s", a, b) } } Copy func Racer(a, b string, timeout time.Duration) (winner string, error error) { select { case <-ping(a): return a, nil case <-ping(b): return b, nil case <-time.After(timeout): return "", fmt.Errorf("timed out waiting for %s and %s", a, b) } } Copy var tenSecondTimeout = 10 * time.Second func Racer(a, b string) (winner string, error error) { return ConfigurableRacer(a, b, tenSecondTimeout) } func ConfigurableRacer(a, b string, timeout time.Duration) (winner string, error error) { select { case <-ping(a): return a, nil case <-ping(b): return b, nil case <-time.After(timeout): return "", fmt.Errorf("timed out waiting for %s and %s", a, b) } } Copy 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) defer slowServer.Close() defer fastServer.Close() slowURL := slowServer.URL fastURL := fastServer.URL want := fastURL got, err := Racer(slowURL, fastURL) if err != nil { t.Fatalf("did not expect an error but got one %v", err) } if got != want { t.Errorf("got %q, want %q", got, want) } }) t.Run("returns an error if a server doesn't respond within the specified time", func(t *testing.T) { server := makeDelayedServer(25 * time.Millisecond) defer server.Close() _, err := ConfigurableRacer(server.URL, server.URL, 20*time.Millisecond) if err == nil { t.Error("expected an error but didn't get one") } }) } --- # Structs, methods & interfaces | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces.md) . [**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`. The TDD cycle should be pretty familiar to you by now. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#write-the-test-first) Write the test first --------------------------------------------------------------------------------------------------------------------------------------------- Copy func TestPerimeter(t *testing.T) { got := Perimeter(10.0, 10.0) want := 40.0 if got != want { t.Errorf("got %.2f want %.2f", got, want) } } Notice the new format string? The `f` is for our `float64` and the `.2` means print 2 decimal places. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#try-to-run-the-test) Try to run the test ------------------------------------------------------------------------------------------------------------------------------------------- `./shapes_test.go:6:9: undefined: Perimeter` [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Results in `shapes_test.go:10: got 0.00 want 40.00`. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#write-enough-code-to-make-it-pass) Write enough code to make it pass ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- So far, so easy. Now let's create a function called `Area(width, height float64)` which returns the area of a rectangle. Try to do it yourself, following the TDD cycle. You should end up with tests like this And code like this [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#refactor) Refactor --------------------------------------------------------------------------------------------------------------------- Our code does the job, but it doesn't contain anything explicit about rectangles. An unwary developer might try to supply the width and height of a triangle to these functions without realising they will return the wrong answer. We could just give the functions more specific names like `RectangleArea`. A neater solution is to define our own _type_ called `Rectangle` which encapsulates this concept for us. We can create a simple type using a **struct**. [A struct](https://golang.org/ref/spec#Struct_types) is just a named collection of fields where you can store data. Declare a struct in your `shapes.go` file like this Now let's refactor the tests to use `Rectangle` instead of plain `float64`s. Remember to run your tests before attempting to fix. The tests should show a helpful error like You can access the fields of a struct with the syntax of `myStruct.field`. Change the two functions to fix the test. I hope you'll agree that passing a `Rectangle` to a function conveys our intent more clearly, but there are more benefits of using structs that we will cover later. Our next requirement is to write an `Area` function for circles. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#write-the-test-first-1) Write the test first ----------------------------------------------------------------------------------------------------------------------------------------------- As you can see, the `f` has been replaced by `g`, with good reason. Use of `g` will print a more precise decimal number in the error message ([fmt options](https://golang.org/pkg/fmt/) ). For example, using a radius of 1.5 in a circle area calculation, `f` would show `7.068583` whereas `g` would show `7.0685834705770345`. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#try-to-run-the-test-1) Try to run the test --------------------------------------------------------------------------------------------------------------------------------------------- `./shapes_test.go:28:13: undefined: Circle` [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) Write the minimal amount of code for the test to run and check the failing test output ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- We need to define our `Circle` type. Now try to run the tests again `./shapes_test.go:29:14: cannot use circle (type Circle) as type Rectangle in argument to Area` Some programming languages allow you to do something like this: But you cannot in Go `./shapes.go:20:32: Area redeclared in this block` We have two choices: * You can have functions with the same name declared in different _packages_. So we could create our `Area(Circle)` in a new package, but that feels overkill here. * We can define [_methods_](https://golang.org/ref/spec#Method_declarations) on our newly defined types instead. ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#what-are-methods) What are methods? So far we have only been writing _functions_ but we have been using some methods. When we call `t.Errorf` we are calling the method `Errorf` on the instance of our `t` (`testing.T`). A method is a function with a receiver. A method declaration binds an identifier, the method name, to a method, and associates the method with the receiver's base type. Methods are very similar to functions but they are called by invoking them on an instance of a particular type. Where you can just call functions wherever you like, such as `Area(rectangle)` you can only call methods on "things". An example will help so let's change our tests first to call methods instead and then fix the code. If we try to run the tests, we get > type Circle has no field or method Area I would like to reiterate how great the compiler is here. It is so important to take the time to slowly read the error messages you get, it will help you in the long run. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-2) Write the minimal amount of code for the test to run and check the failing test output ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Let's add some methods to our types The syntax for declaring methods is almost the same as functions and that's because they're so similar. The only difference is the syntax of the method receiver `func (receiverName ReceiverType) MethodName(args)`. When your method is called on a variable of that type, you get your reference to its data via the `receiverName` variable. In many other programming languages this is done implicitly and you access the receiver via `this`. It is a convention in Go to have the receiver variable be the first letter of the type. If you try to re-run the tests they should now compile and give you some failing output. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#write-enough-code-to-make-it-pass-1) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Now let's make our rectangle tests pass by fixing our new method If you re-run the tests the rectangle tests should be passing but circle should still be failing. To make circle's `Area` function pass we will borrow the `Pi` constant from the `math` package (remember to import it). [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#refactor-1) Refactor ----------------------------------------------------------------------------------------------------------------------- There is some duplication in our tests. All we want to do is take a collection of _shapes_, call the `Area()` method on them and then check the result. We want to be able to write some kind of `checkArea` function that we can pass both `Rectangle`s and `Circle`s to, but fail to compile if we try to pass in something that isn't a shape. With Go, we can codify this intent with **interfaces**. [Interfaces](https://golang.org/ref/spec#Interface_types) are a very powerful concept in statically typed languages like Go because they allow you to make functions that can be used with different types and create highly-decoupled code whilst still maintaining type-safety. Let's introduce this by refactoring our tests. We are creating a helper function like we have in other exercises but this time we are asking for a `Shape` to be passed in. If we try to call this with something that isn't a shape, then it will not compile. How does something become a shape? We just tell Go what a `Shape` is using an interface declaration We're creating a new `type` just like we did with `Rectangle` and `Circle` but this time it is an `interface` rather than a `struct`. Once you add this to the code, the tests will pass. ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#wait-what) Wait, what? This is quite different to interfaces in most other programming languages. Normally you have to write code to say `My type Foo implements interface Bar`. But in our case * `Rectangle` has a method called `Area` that returns a `float64` so it satisfies the `Shape` interface * `Circle` has a method called `Area` that returns a `float64` so it satisfies the `Shape` interface * `string` does not have such a method, so it doesn't satisfy the interface * etc. In Go **interface resolution is implicit**. If the type you pass in matches what the interface is asking for, it will compile. ### [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#decoupling) Decoupling Notice how our helper does not need to concern itself with whether the shape is a `Rectangle` or a `Circle` or a `Triangle`. By declaring an interface, the helper is _decoupled_ from the concrete types and only has the method it needs to do its job. This kind of approach of using interfaces to declare **only what you need** is very important in software design and will be covered in more detail in later sections. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#further-refactoring) Further refactoring ------------------------------------------------------------------------------------------------------------------------------------------- Now that you have some understanding of structs we can introduce "table driven tests". [Table driven tests](https://go.dev/wiki/TableDrivenTests) are useful when you want to build a list of test cases that can be tested in the same manner. The only new syntax here is creating an "anonymous struct", `areaTests`. We are declaring a slice of structs by using `[]struct` with two fields, the `shape` and the `want`. Then we fill the slice with cases. We then iterate over them just like we do any other slice, using the struct fields to run our tests. You can see how it would be very easy for a developer to introduce a new shape, implement `Area` and then add it to the test cases. In addition, if a bug is found with `Area` it is very easy to add a new test case to exercise it before fixing it. Table driven tests can be a great item in your toolbox, but be sure that you have a need for the extra noise in the tests. They are a great fit when you wish to test various implementations of an interface, or if the data being passed in to a function has lots of different requirements that need testing. Let's demonstrate all this by adding another shape and testing it; a triangle. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#write-the-test-first-2) Write the test first ----------------------------------------------------------------------------------------------------------------------------------------------- Adding a new test for our new shape is very easy. Just add `{Triangle{12, 6}, 36.0},` to our list. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#try-to-run-the-test-2) Try to run the test --------------------------------------------------------------------------------------------------------------------------------------------- Remember, keep trying to run the test and let the compiler guide you toward a solution. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-3) Write the minimal amount of code for the test to run and check the failing test output ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- `./shapes_test.go:25:4: undefined: Triangle` We have not defined `Triangle` yet Try again It's telling us we cannot use a `Triangle` as a shape because it does not have an `Area()` method, so add an empty implementation to get the test working Finally the code compiles and we get our error `shapes_test.go:31: got 0.00 want 36.00` [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#write-enough-code-to-make-it-pass-2) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- And our tests pass! [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#refactor-2) Refactor ----------------------------------------------------------------------------------------------------------------------- Again, the implementation is fine but our tests could do with some improvement. When you scan this It's not immediately clear what all the numbers represent and you should be aiming for your tests to be easily understood. So far you've only been shown syntax for creating instances of structs `MyStruct{val1, val2}` but you can optionally name the fields. Let's see what it looks like In [Test-Driven Development by Example](https://g.co/kgs/yCzDLF) Kent Beck refactors some tests to a point and asserts: > The test speaks to us more clearly, as if it were an assertion of truth, **not a sequence of operations** (emphasis in the quote is mine) Now our tests - rather, the list of test cases - make assertions of truth about shapes and their areas. [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#make-sure-your-test-output-is-helpful) Make sure your test output is helpful ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Remember earlier when we were implementing `Triangle` and we had the failing test? It printed `shapes_test.go:31: got 0.00 want 36.00`. We knew this was in relation to `Triangle` because we were just working with it. But what if a bug slipped in to the system in one of 20 cases in the table? How would a developer know which case failed? This is not a great experience for the developer, they will have to manually look through the cases to find out which case actually failed. We can change our error message into `%#v got %g want %g`. The `%#v` format string will print out our struct with the values in its field, so the developer can see at a glance the properties that are being tested. To increase the readability of our test cases further, we can rename the `want` field into something more descriptive like `hasArea`. One final tip with table driven tests is to use `t.Run` and to name the test cases. By wrapping each case in a `t.Run` you will have clearer test output on failures as it will print the name of the case And you can run specific tests within your table with `go test -run TestArea/Rectangle`. Here is our final test code which captures this [](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#wrapping-up) Wrapping up --------------------------------------------------------------------------------------------------------------------------- This was more TDD practice, iterating over our solutions to basic mathematic problems and learning new language features motivated by our tests. * Declaring structs to create your own data types which lets you bundle related data together and make the intent of your code clearer * Declaring interfaces so you can define functions that can be used by different types ([ad hoc polymorphism](https://en.wikipedia.org/wiki/Ad_hoc_polymorphism) ) * Adding methods so you can add functionality to your data types and so you can implement interfaces * Table driven tests to make your assertions clearer and your test suites easier to extend & maintain This was an important chapter because we are now starting to define our own types. In statically typed languages like Go, being able to design your own types is essential for building software that is easy to understand, to piece together and to test. Interfaces are a great tool for hiding complexity away from other parts of the system. In our case our test helper _code_ did not need to know the exact shape it was asserting on, only how to "ask" for its area. As you become more familiar with Go you will start to see the real strength of interfaces and the standard library. You'll learn about interfaces defined in the standard library that are used _everywhere_ and by implementing them against your own types, you can very quickly re-use a lot of great functionality. [PreviousArrays and slices](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/arrays-and-slices) [NextPointers & errors](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/pointers-and-errors) Last updated 4 months ago * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#write-the-test-first) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#try-to-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#write-enough-code-to-make-it-pass) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#refactor) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#write-the-test-first-1) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#try-to-run-the-test-1) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) * [What are methods?](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#what-are-methods) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-2) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#write-enough-code-to-make-it-pass-1) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#refactor-1) * [Wait, what?](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#wait-what) * [Decoupling](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#decoupling) * [Further refactoring](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#further-refactoring) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#write-the-test-first-2) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#try-to-run-the-test-2) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-3) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#write-enough-code-to-make-it-pass-2) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#refactor-2) * [Make sure your test output is helpful](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#make-sure-your-test-output-is-helpful) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/structs-methods-and-interfaces#wrapping-up) Copy func Perimeter(width float64, height float64) float64 { return 0 } Copy func Perimeter(width float64, height float64) float64 { return 2 * (width + height) } Copy func TestPerimeter(t *testing.T) { got := Perimeter(10.0, 10.0) want := 40.0 if got != want { t.Errorf("got %.2f want %.2f", got, want) } } func TestArea(t *testing.T) { got := Area(12.0, 6.0) want := 72.0 if got != want { t.Errorf("got %.2f want %.2f", got, want) } } Copy func Perimeter(width float64, height float64) float64 { return 2 * (width + height) } func Area(width float64, height float64) float64 { return width * height } Copy type Rectangle struct { Width float64 Height float64 } Copy func TestPerimeter(t *testing.T) { rectangle := Rectangle{10.0, 10.0} got := Perimeter(rectangle) want := 40.0 if got != want { t.Errorf("got %.2f want %.2f", got, want) } } func TestArea(t *testing.T) { rectangle := Rectangle{12.0, 6.0} got := Area(rectangle) want := 72.0 if got != want { t.Errorf("got %.2f want %.2f", got, want) } } Copy ./shapes_test.go:7:18: not enough arguments in call to Perimeter have (Rectangle) want (float64, float64) Copy func Perimeter(rectangle Rectangle) float64 { return 2 * (rectangle.Width + rectangle.Height) } func Area(rectangle Rectangle) float64 { return rectangle.Width * rectangle.Height } Copy func TestArea(t *testing.T) { t.Run("rectangles", func(t *testing.T) { rectangle := Rectangle{12, 6} got := Area(rectangle) want := 72.0 if got != want { t.Errorf("got %g want %g", got, want) } }) t.Run("circles", func(t *testing.T) { circle := Circle{10} got := Area(circle) want := 314.1592653589793 if got != want { t.Errorf("got %g want %g", got, want) } }) } Copy type Circle struct { Radius float64 } Copy func Area(circle Circle) float64 {} func Area(rectangle Rectangle) float64 {} Copy func TestArea(t *testing.T) { t.Run("rectangles", func(t *testing.T) { rectangle := Rectangle{12, 6} got := rectangle.Area() want := 72.0 if got != want { t.Errorf("got %g want %g", got, want) } }) t.Run("circles", func(t *testing.T) { circle := Circle{10} got := circle.Area() want := 314.1592653589793 if got != want { t.Errorf("got %g want %g", got, want) } }) } Copy ./shapes_test.go:19:19: rectangle.Area undefined (type Rectangle has no field or method Area) ./shapes_test.go:29:16: circle.Area undefined (type Circle has no field or method Area) Copy type Rectangle struct { Width float64 Height float64 } func (r Rectangle) Area() float64 { return 0 } type Circle struct { Radius float64 } func (c Circle) Area() float64 { return 0 } Copy r Rectangle Copy func (r Rectangle) Area() float64 { return r.Width * r.Height } Copy func (c Circle) Area() float64 { return math.Pi * c.Radius * c.Radius } Copy func TestArea(t *testing.T) { checkArea := func(t testing.TB, shape Shape, want float64) { t.Helper() got := shape.Area() if got != want { t.Errorf("got %g want %g", got, want) } } t.Run("rectangles", func(t *testing.T) { rectangle := Rectangle{12, 6} checkArea(t, rectangle, 72.0) }) t.Run("circles", func(t *testing.T) { circle := Circle{10} checkArea(t, circle, 314.1592653589793) }) } Copy type Shape interface { Area() float64 } Copy func TestArea(t *testing.T) { areaTests := []struct { shape Shape want float64 }{ {Rectangle{12, 6}, 72.0}, {Circle{10}, 314.1592653589793}, } for _, tt := range areaTests { got := tt.shape.Area() if got != tt.want { t.Errorf("got %g want %g", got, tt.want) } } } Copy func TestArea(t *testing.T) { areaTests := []struct { shape Shape want float64 }{ {Rectangle{12, 6}, 72.0}, {Circle{10}, 314.1592653589793}, {Triangle{12, 6}, 36.0}, } for _, tt := range areaTests { got := tt.shape.Area() if got != tt.want { t.Errorf("got %g want %g", got, tt.want) } } } Copy type Triangle struct { Base float64 Height float64 } Copy ./shapes_test.go:25:8: cannot use Triangle literal (type Triangle) as type Shape in field value: Triangle does not implement Shape (missing Area method) Copy func (t Triangle) Area() float64 { return 0 } Copy func (t Triangle) Area() float64 { return (t.Base * t.Height) * 0.5 } Copy {Rectangle{12, 6}, 72.0}, {Circle{10}, 314.1592653589793}, {Triangle{12, 6}, 36.0}, Copy {shape: Rectangle{Width: 12, Height: 6}, want: 72.0}, {shape: Circle{Radius: 10}, want: 314.1592653589793}, {shape: Triangle{Base: 12, Height: 6}, want: 36.0}, Copy --- FAIL: TestArea (0.00s) --- FAIL: TestArea/Rectangle (0.00s) shapes_test.go:33: main.Rectangle{Width:12, Height:6} got 72.00 want 72.10 Copy func TestArea(t *testing.T) { areaTests := []struct { name string shape Shape hasArea float64 }{ {name: "Rectangle", shape: Rectangle{Width: 12, Height: 6}, hasArea: 72.0}, {name: "Circle", shape: Circle{Radius: 10}, hasArea: 314.1592653589793}, {name: "Triangle", shape: Triangle{Base: 12, Height: 6}, hasArea: 36.0}, } for _, tt := range areaTests { // using tt.name from the case to use it as the `t.Run` test name t.Run(tt.name, func(t *testing.T) { got := tt.shape.Area() if got != tt.hasArea { t.Errorf("%#v got %g want %g", tt.shape, got, tt.hasArea) } }) } } --- # IO and sorting | Primary version | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io.md) . [**You can find all the code for this chapter here**](https://github.com/quii/learn-go-with-tests/tree/master/io) [In the previous chapter](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json) 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. Our product owner is somewhat perturbed by the software losing the scores when the server was restarted. This is because our implementation of our store is in-memory. She is also not pleased that we didn't interpret the `/league` endpoint should return the players ordered by the number of wins! [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#the-code-so-far) The code so far ------------------------------------------------------------------------------------------------------------------- Copy // server.go package main import ( "encoding/json" "fmt" "net/http" ) // PlayerStore stores score information about players type PlayerStore interface { GetPlayerScore(name string) int RecordWin(name string) GetLeague() []Player } // Player stores a name with a number of wins type Player struct { Name string Wins int } // PlayerServer is a HTTP interface for player information type PlayerServer struct { store PlayerStore http.Handler } const jsonContentType = "application/json" // NewPlayerServer creates a PlayerServer with routing configured func NewPlayerServer(store PlayerStore) *PlayerServer { p := new(PlayerServer) p.store = store router := http.NewServeMux() router.Handle("/league", http.HandlerFunc(p.leagueHandler)) router.Handle("/players/", http.HandlerFunc(p.playersHandler)) p.Handler = router return p } func (p *PlayerServer) leagueHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("content-type", jsonContentType) json.NewEncoder(w).Encode(p.store.GetLeague()) } func (p *PlayerServer) playersHandler(w http.ResponseWriter, r *http.Request) { player := r.URL.Path[len("/players/"):] switch r.Method { case http.MethodPost: p.processWin(w, player) case http.MethodGet: p.showScore(w, player) } } func (p *PlayerServer) showScore(w http.ResponseWriter, player string) { score := p.store.GetPlayerScore(player) if score == 0 { w.WriteHeader(http.StatusNotFound) } fmt.Fprint(w, score) } func (p *PlayerServer) processWin(w http.ResponseWriter, player string) { p.store.RecordWin(player) w.WriteHeader(http.StatusAccepted) } You can find the corresponding tests in the link at the top of the chapter. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#store-the-data) Store the data ----------------------------------------------------------------------------------------------------------------- There are dozens of databases we could use for this but we're going to go for a very simple approach. We're going to store the data for this application in a file as JSON. This keeps the data very portable and is relatively simple to implement. It won't scale especially well but given this is a prototype it'll be fine for now. If our circumstances change and it's no longer appropriate it'll be simple to swap it out for something different because of the `PlayerStore` abstraction we have used. We will keep the `InMemoryPlayerStore` for now so that the integration tests keep passing as we develop our new store. Once we are confident our new implementation is sufficient to make the integration test pass we will swap it in and then delete `InMemoryPlayerStore`. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-the-test-first) Write the test first ----------------------------------------------------------------------------------------------------------------------------- By now you should be familiar with the interfaces around the standard library for reading data (`io.Reader`), writing data (`io.Writer`) and how we can use the standard library to test these functions without having to use real files. For this work to be complete we'll need to implement `PlayerStore` so we'll write tests for our store calling the methods we need to implement. We'll start with `GetLeague`. We're using `strings.NewReader` which will return us a `Reader`, which is what our `FileSystemPlayerStore` will use to read data. In `main` we will open a file, which is also a `Reader`. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#try-to-run-the-test) Try to run the test --------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Let's define `FileSystemPlayerStore` in a new file Try again It's complaining because we're passing in a `Reader` but not expecting one and it doesn't have `GetLeague` defined yet. One more try... [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-enough-code-to-make-it-pass) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------- We've read JSON from a reader before The test should pass. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#refactor) Refactor ----------------------------------------------------------------------------------------------------- We _have_ done this before! Our test code for the server had to decode the JSON from the response. Let's try DRYing this up into a function. Create a new file called `league.go` and put this inside. Call this in our implementation and in our test helper `getLeagueFromResponse` in `server_test.go` We haven't got a strategy yet for dealing with parsing errors but let's press on. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#seeking-problems) Seeking problems There is a flaw in our implementation. First of all, let's remind ourselves how `io.Reader` is defined. With our file, you can imagine it reading through byte by byte until the end. What happens if you try to `Read` a second time? Add the following to the end of our current test. We want this to pass, but if you run the test it doesn't. The problem is our `Reader` has reached the end so there is nothing more to read. We need a way to tell it to go back to the start. [ReadSeeker](https://golang.org/pkg/io/#ReadSeeker) is another interface in the standard library that can help. Remember embedding? This is an interface comprised of `Reader` and [`Seeker`](https://golang.org/pkg/io/#Seeker) This sounds good, can we change `FileSystemPlayerStore` to take this interface instead? Try running the test, it now passes! Happily for us `string.NewReader` that we used in our test also implements `ReadSeeker` so we didn't have to make any other changes. Next we'll implement `GetPlayerScore`. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-the-test-first-1) Write the test first ------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#try-to-run-the-test-1) Try to run the test ----------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) Write the minimal amount of code for the test to run and check the failing test output ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- We need to add the method to our new type to get the test to compile. Now it compiles and the test fails [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-enough-code-to-make-it-pass-1) Write enough code to make it pass --------------------------------------------------------------------------------------------------------------------------------------------------------- We can iterate over the league to find the player and return their score [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#refactor-1) Refactor ------------------------------------------------------------------------------------------------------- You will have seen dozens of test helper refactorings so I'll leave this to you to make it work Finally, we need to start recording scores with `RecordWin`. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-the-test-first-2) Write the test first ------------------------------------------------------------------------------------------------------------------------------- Our approach is fairly short-sighted for writes. We can't (easily) just update one "row" of JSON in a file. We'll need to store the _whole_ new representation of our database on every write. How do we write? We'd normally use a `Writer` but we already have our `ReadSeeker`. Potentially we could have two dependencies but the standard library already has an interface for us `ReadWriteSeeker` which lets us do all the things we'll need to do with a file. Let's update our type See if it compiles It's not too surprising that `strings.Reader` does not implement `ReadWriteSeeker` so what do we do? We have two choices * Create a temporary file for each test. `*os.File` implements `ReadWriteSeeker`. The pro of this is it becomes more of an integration test, we're really reading and writing from the file system so it will give us a very high level of confidence. The cons are we prefer unit tests because they are faster and generally simpler. We will also need to do more work around creating temporary files and then making sure they're removed after the test. * We could use a third party library. [Mattetti](https://github.com/mattetti) has written a library [filebuffer](https://github.com/mattetti/filebuffer) which implements the interface we need and doesn't touch the file system. I don't think there's an especially wrong answer here, but by choosing to use a third party library I would have to explain dependency management! So we will use files instead. Before adding our test we need to make our other tests compile by replacing the `strings.Reader` with an `os.File`. Let's create a helper function which will create a temporary file with some data inside it [TempFile](https://golang.org/pkg/io/ioutil/#TempDir) creates a temporary file for us to use. The `"db"` value we've passed in is a prefix put on a random file name it will create. This is to ensure it won't clash with other files by accident. You'll notice we're not only returning our `ReadWriteSeeker` (the file) but also a function. We need to make sure that the file is removed once the test is finished. We don't want to leak details of the files into the test as it's prone to error and uninteresting for the reader. By returning a `removeFile` function, we can take care of the details in our helper and all the caller has to do is run `defer cleanDatabase()`. Run the tests and they should be passing! There were a fair amount of changes but now it feels like we have our interface definition complete and it should be very easy to add new tests from now. Let's get the first iteration of recording a win for an existing player [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#try-to-run-the-test-2) Try to run the test ----------------------------------------------------------------------------------------------------------------------------- `./FileSystemStore_test.go:67:8: store.RecordWin undefined (type FileSystemPlayerStore has no field or method RecordWin)` [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-2) Write the minimal amount of code for the test to run and check the failing test output ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Add the new method Our implementation is empty so the old score is getting returned. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-enough-code-to-make-it-pass-2) Write enough code to make it pass --------------------------------------------------------------------------------------------------------------------------------------------------------- You may be asking yourself why I am doing `league[i].Wins++` rather than `player.Wins++`. When you `range` over a slice you are returned the current index of the loop (in our case `i`) and a _copy_ of the element at that index. Changing the `Wins` value of a copy won't have any effect on the `league` slice that we iterate on. For that reason, we need to get the reference to the actual value by doing `league[i]` and then changing that value instead. If you run the tests, they should now be passing. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#refactor-2) Refactor ------------------------------------------------------------------------------------------------------- In `GetPlayerScore` and `RecordWin`, we are iterating over `[]Player` to find a player by name. We could refactor this common code in the internals of `FileSystemStore` but to me, it feels like this is maybe useful code we can lift into a new type. Working with a "League" so far has always been with `[]Player` but we can create a new type called `League`. This will be easier for other developers to understand and then we can attach useful methods onto that type for us to use. Inside `league.go` add the following Now if anyone has a `League` they can easily find a given player. Change our `PlayerStore` interface to return `League` rather than `[]Player`. Try to re-run the tests, you'll get a compilation problem because we've changed the interface but it's very easy to fix; just change the return type from `[]Player` to `League`. This lets us simplify our methods in `FileSystemStore`. This is looking much better and we can see how we might be able to find other useful functionality around `League` can be refactored. We now need to handle the scenario of recording wins of new players. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-the-test-first-3) Write the test first ------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#try-to-run-the-test-3) Try to run the test ----------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-enough-code-to-make-it-pass-3) Write enough code to make it pass --------------------------------------------------------------------------------------------------------------------------------------------------------- We just need to handle the scenario where `Find` returns `nil` because it couldn't find the player. The happy path is looking ok so we can now try using our new `Store` in the integration test. This will give us more confidence that the software works and then we can delete the redundant `InMemoryPlayerStore`. In `TestRecordingWinsAndRetrievingThem` replace the old store. 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. * We create a file for our database. * The 2nd argument to `os.OpenFile` lets you define the permissions for opening the file, in our case `O_RDWR` means we want to read and write _and_ `os.O_CREATE` means create the file if it doesn't exist. * The 3rd argument means sets permissions for the file, in our case, all users can read and write the file. [(See superuser.com for a more detailed explanation)](https://superuser.com/questions/295591/what-is-the-meaning-of-chmod-666) . Running the program now persists the data in a file in between restarts, hooray! [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#more-refactoring-and-performance-concerns) More refactoring and performance concerns ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- Every time someone calls `GetLeague()` or `GetPlayerScore()` we are reading the entire file and parsing it into JSON. We should not have to do that because `FileSystemStore` is entirely responsible for the state of the league; it should only need to read the file when the program starts up and only need to update the file when data changes. We can create a constructor which can do some of this initialisation for us and store the league as a value in our `FileSystemStore` to be used on the reads instead. This way we only have to read from disk once. We can now replace all of our previous calls to getting the league from disk and just use `f.league` instead. If you try to run the tests it will now complain about initialising `FileSystemPlayerStore` so just fix them by calling our new constructor. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#another-problem) Another problem There is some more naivety in the way we are dealing with files which _could_ create a very nasty bug down the line. When we `RecordWin`, we `Seek` back to the start of the file and then write the new data—but what if the new data was smaller than what was there before? In our current case, this is impossible. We never edit or delete scores so the data can only get bigger. However, it would be irresponsible for us to leave the code like this; it's not unthinkable that a delete scenario could come up. How will we test for this though? What we need to do is first refactor our code so we separate out the concern of the _kind of data we write, from the writing_. We can then test that separately to check it works how we hope. 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: Notice that we're only implementing `Write` now, as it encapsulates the `Seek` part. This means our `FileSystemStore` can just have a reference to a `Writer` instead. Update the constructor to use `Tape` 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. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-the-test-first-4) Write the test first ------------------------------------------------------------------------------------------------------------------------------- Our test will create a file with some content, try to write to it using the `tape`, and read it all again to see what's in the file. In `tape_test.go`: [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#try-to-run-the-test-4) Try to run the test ----------------------------------------------------------------------------------------------------------------------------- As we thought! It writes the data we want, but leaves the rest of the original data remaining. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-enough-code-to-make-it-pass-4) Write enough code to make it pass --------------------------------------------------------------------------------------------------------------------------------------------------------- `os.File` has a truncate function that will let us effectively empty the file. We should be able to just call this to get what we want. Change `tape` to the following: The compiler will fail in a number of places where we are expecting an `io.ReadWriteSeeker` but we are sending in `*os.File`. You should be able to fix these problems yourself by now but if you get stuck just check the source code. Once you get it refactoring our `TestTape_Write` test should be passing! ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#one-other-small-refactor) One other small refactor In `RecordWin` we have the line `json.NewEncoder(f.database).Encode(f.league)`. We don't need to create a new encoder every time we write, we can initialise one in our constructor and use that instead. Store a reference to an `Encoder` in our type: Initialise it in the constructor: Use it in `RecordWin`. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#didnt-we-just-break-some-rules-there-testing-private-things-no-interfaces) Didn't we just break some rules there? Testing private things? No interfaces? ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#on-testing-private-types) On testing private types It's true that _in general_ you should favour not testing private things as that can sometimes lead to your tests being too tightly coupled to the implementation, which can hinder refactoring in future. However, we must not forget that tests should give us _confidence_. We were not confident that our implementation would work if we added any kind of edit or delete functionality. We did not want to leave the code like that, especially if this was being worked on by more than one person who may not be aware of the shortcomings of our initial approach. Finally, it's just one test! If we decide to change the way it works it won't be a disaster to just delete the test but we have at the very least captured the requirement for future maintainers. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#interfaces) Interfaces We started off the code by using `io.Reader` as that was the easiest path for us to unit test our new `PlayerStore`. As we developed the code we moved on to `io.ReadWriter` and then `io.ReadWriteSeeker`. We then found out there was nothing in the standard library that actually implemented that apart from `*os.File`. We could've taken the decision to write our own or use an open source one but it felt pragmatic just to make temporary files for the tests. Finally, we needed `Truncate` which is also on `*os.File`. It would've been an option to create our own interface capturing these requirements. But what is this really giving us? Bear in mind we are _not mocking_ and it is unrealistic for a **file system** store to take any type other than an `*os.File` so we don't need the polymorphism that interfaces give us. Don't be afraid to chop and change types and experiment like we have here. The great thing about using a statically typed language is the compiler will help you with every change. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#error-handling) Error handling ----------------------------------------------------------------------------------------------------------------- Before we start working on sorting we should make sure we're happy with our current code and remove any technical debt we may have. It's an important principle to get to working software as quickly as possible (stay out of the red state) but that doesn't mean we should ignore error cases! If we go back to `FileSystemStore.go` we have `league, _ := NewLeague(f.database)` in our constructor. `NewLeague` can return an error if it is unable to parse the league from the `io.Reader` that we provide. It was pragmatic to ignore that at the time as we already had failing tests. If we had tried to tackle it at the same time, we would have been juggling two things at once. Let's make it so our constructor is capable of returning an error. Remember it is very important to give helpful error messages (just like your tests). People on the internet jokingly say that most Go code is: **That is 100% not idiomatic.** Adding contextual information (i.e what you were doing to cause the error) to your error messages makes operating your software far easier. If you try to compile you'll get some errors. In main we'll want to exit the program, printing the error. In the tests we should assert there is no error. We can make a helper to help with this. Work through the other compilation problems using this helper. Finally, you should have a failing test: We cannot parse the league because the file is empty. We weren't getting errors before because we always just ignored them. Let's fix our big integration test by putting some valid JSON in it: Now that all the tests are passing, we need to handle the scenario where the file is empty. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-the-test-first-5) Write the test first ------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#try-to-run-the-test-5) Try to run the test ----------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-enough-code-to-make-it-pass-5) Write enough code to make it pass --------------------------------------------------------------------------------------------------------------------------------------------------------- Change our constructor to the following `file.Stat` returns stats on our file, which lets us check the size of the file. If it's empty, we `Write` an empty JSON array and `Seek` back to the start, ready for the rest of the code. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#refactor-3) Refactor ------------------------------------------------------------------------------------------------------- Our constructor is a bit messy now, so let's extract the initialise code into a function: [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#sorting) Sorting --------------------------------------------------------------------------------------------------- Our product owner wants `/league` to return the players sorted by their scores, from highest to lowest. The main decision to make here is where in the software should this happen. If we were using a "real" database we would use things like `ORDER BY` so the sorting is super fast. For that reason, it feels like implementations of `PlayerStore` should be responsible. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-the-test-first-6) Write the test first ------------------------------------------------------------------------------------------------------------------------------- We can update the assertion on our first test in `TestFileSystemStore`: The order of the JSON coming in is in the wrong order and our `want` will check that it is returned to the caller in the correct order. [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#try-to-run-the-test-6) Try to run the test ----------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-enough-code-to-make-it-pass-6) Write enough code to make it pass --------------------------------------------------------------------------------------------------------------------------------------------------------- [`sort.Slice`](https://golang.org/pkg/sort/#Slice) > Slice sorts the provided slice given the provided less function. Easy! [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#wrapping-up) Wrapping up ----------------------------------------------------------------------------------------------------------- ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#what-weve-covered) What we've covered * The `Seeker` interface and its relation to `Reader` and `Writer`. * Working with files. * Creating an easy to use helper for testing with files that hides all the messy stuff. * `sort.Slice` for sorting slices. * Using the compiler to help us safely make structural changes to the application. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#breaking-rules) Breaking rules * Most rules in software engineering aren't really rules, just best practices that work 80% of the time. * We discovered a scenario where one of our previous "rules" of not testing internal functions was not helpful for us so we broke the rule. * It's important when breaking rules to understand the trade-off you are making. In our case, we were ok with it because it was just one test and would've been very difficult to exercise the scenario otherwise. * In order to be able to break the rules **you must understand them first**. An analogy is with learning guitar. It doesn't matter how creative you think you are, you must understand and practice the fundamentals. ### [](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#where-our-software-is-at) Where our software is at * We have an HTTP API where you can create players and increment their score. * We can return a league of everyone's scores as JSON. * The data is persisted as a JSON file. [PreviousJSON, routing and embedding](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/json) [NextCommand line & package structure](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/command-line) Last updated 6 years ago * [The code so far](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#the-code-so-far) * [Store the data](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#store-the-data) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-the-test-first) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#try-to-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-enough-code-to-make-it-pass) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#refactor) * [Seeking problems](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#seeking-problems) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-the-test-first-1) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#try-to-run-the-test-1) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-enough-code-to-make-it-pass-1) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#refactor-1) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-the-test-first-2) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#try-to-run-the-test-2) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-2) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-enough-code-to-make-it-pass-2) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#refactor-2) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-the-test-first-3) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#try-to-run-the-test-3) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-enough-code-to-make-it-pass-3) * [More refactoring and performance concerns](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#more-refactoring-and-performance-concerns) * [Another problem](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#another-problem) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-the-test-first-4) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#try-to-run-the-test-4) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-enough-code-to-make-it-pass-4) * [One other small refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#one-other-small-refactor) * [Didn't we just break some rules there? Testing private things? No interfaces?](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#didnt-we-just-break-some-rules-there-testing-private-things-no-interfaces) * [On testing private types](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#on-testing-private-types) * [Interfaces](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#interfaces) * [Error handling](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#error-handling) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-the-test-first-5) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#try-to-run-the-test-5) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-enough-code-to-make-it-pass-5) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#refactor-3) * [Sorting](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#sorting) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-the-test-first-6) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#try-to-run-the-test-6) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#write-enough-code-to-make-it-pass-6) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#wrapping-up) * [What we've covered](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#what-weve-covered) * [Breaking rules](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#breaking-rules) * [Where our software is at](https://quii.gitbook.io/learn-go-with-tests/master/build-an-application/io#where-our-software-is-at) Copy // InMemoryPlayerStore.go package main func NewInMemoryPlayerStore() *InMemoryPlayerStore { return &InMemoryPlayerStore{map[string]int{}} } type InMemoryPlayerStore struct { store map[string]int } func (i *InMemoryPlayerStore) GetLeague() []Player { var league []Player for name, wins := range i.store { league = append(league, Player{name, wins}) } return league } func (i *InMemoryPlayerStore) RecordWin(name string) { i.store[name]++ } func (i *InMemoryPlayerStore) GetPlayerScore(name string) int { return i.store[name] } Copy // main.go package main import ( "log" "net/http" ) func main() { server := NewPlayerServer(NewInMemoryPlayerStore()) if err := http.ListenAndServe(":5000", server); err != nil { log.Fatalf("could not listen on port 5000 %v", err) } } Copy func TestFileSystemStore(t *testing.T) { t.Run("/league from a reader", func(t *testing.T) { database := strings.NewReader(`[\ {"Name": "Cleo", "Wins": 10},\ {"Name": "Chris", "Wins": 33}]`) store := FileSystemPlayerStore{database} got := store.GetLeague() want := []Player{ {"Cleo", 10}, {"Chris", 33}, } assertLeague(t, got, want) }) } Copy # github.com/quii/learn-go-with-tests/json-and-io/v7 ./FileSystemStore_test.go:15:12: undefined: FileSystemPlayerStore Copy type FileSystemPlayerStore struct {} Copy # github.com/quii/learn-go-with-tests/json-and-io/v7 ./FileSystemStore_test.go:15:28: too many values in struct initializer ./FileSystemStore_test.go:17:15: store.GetLeague undefined (type FileSystemPlayerStore has no field or method GetLeague) Copy type FileSystemPlayerStore struct { database io.Reader } func (f *FileSystemPlayerStore) GetLeague() []Player { return nil } Copy === RUN TestFileSystemStore//league_from_a_reader --- FAIL: TestFileSystemStore//league_from_a_reader (0.00s) FileSystemStore_test.go:24: got [] want [{Cleo 10} {Chris 33}] Copy func (f *FileSystemPlayerStore) GetLeague() []Player { var league []Player json.NewDecoder(f.database).Decode(&league) return league } Copy func NewLeague(rdr io.Reader) ([]Player, error) { var league []Player err := json.NewDecoder(rdr).Decode(&league) if err != nil { err = fmt.Errorf("problem parsing league, %v", err) } return league, err } Copy func (f *FileSystemPlayerStore) GetLeague() []Player { league, _ := NewLeague(f.database) return league } Copy type Reader interface { Read(p []byte) (n int, err error) } Copy // read again got = store.GetLeague() assertLeague(t, got, want) Copy type ReadSeeker interface { Reader Seeker } Copy type Seeker interface { Seek(offset int64, whence int) (int64, error) } Copy type FileSystemPlayerStore struct { database io.ReadSeeker } func (f *FileSystemPlayerStore) GetLeague() []Player { f.database.Seek(0, 0) league, _ := NewLeague(f.database) return league } Copy t.Run("get player score", func(t *testing.T) { database := strings.NewReader(`[\ {"Name": "Cleo", "Wins": 10},\ {"Name": "Chris", "Wins": 33}]`) store := FileSystemPlayerStore{database} got := store.GetPlayerScore("Chris") want := 33 if got != want { t.Errorf("got %d want %d", got, want) } }) Copy ./FileSystemStore_test.go:38:15: store.GetPlayerScore undefined (type FileSystemPlayerStore has no field or method GetPlayerScore) Copy func (f *FileSystemPlayerStore) GetPlayerScore(name string) int { return 0 } Copy === RUN TestFileSystemStore/get_player_score --- FAIL: TestFileSystemStore//get_player_score (0.00s) FileSystemStore_test.go:43: got 0 want 33 Copy func (f *FileSystemPlayerStore) GetPlayerScore(name string) int { var wins int for _, player := range f.GetLeague() { if player.Name == name { wins = player.Wins break } } return wins } Copy t.Run("/get player score", func(t *testing.T) { database := strings.NewReader(`[\ {"Name": "Cleo", "Wins": 10},\ {"Name": "Chris", "Wins": 33}]`) store := FileSystemPlayerStore{database} got := store.GetPlayerScore("Chris") want := 33 assertScoreEquals(t, got, want) }) Copy type FileSystemPlayerStore struct { database io.ReadWriteSeeker } Copy ./FileSystemStore_test.go:15:34: cannot use database (type *strings.Reader) as type io.ReadWriteSeeker in field value: *strings.Reader does not implement io.ReadWriteSeeker (missing Write method) ./FileSystemStore_test.go:36:34: cannot use database (type *strings.Reader) as type io.ReadWriteSeeker in field value: *strings.Reader does not implement io.ReadWriteSeeker (missing Write method) Copy func createTempFile(t *testing.T, initialData string) (io.ReadWriteSeeker, func()) { t.Helper() tmpfile, err := ioutil.TempFile("", "db") if err != nil { t.Fatalf("could not create temp file %v", err) } tmpfile.Write([]byte(initialData)) removeFile := func() { tmpfile.Close() os.Remove(tmpfile.Name()) } return tmpfile, removeFile } Copy func TestFileSystemStore(t *testing.T) { t.Run("league from a reader", func(t *testing.T) { database, cleanDatabase := createTempFile(t, `[\ {"Name": "Cleo", "Wins": 10},\ {"Name": "Chris", "Wins": 33}]`) defer cleanDatabase() store := FileSystemPlayerStore{database} got := store.GetLeague() want := []Player{ {"Cleo", 10}, {"Chris", 33}, } assertLeague(t, got, want) // read again got = store.GetLeague() assertLeague(t, got, want) }) t.Run("get player score", func(t *testing.T) { database, cleanDatabase := createTempFile(t, `[\ {"Name": "Cleo", "Wins": 10},\ {"Name": "Chris", "Wins": 33}]`) defer cleanDatabase() store := FileSystemPlayerStore{database} got := store.GetPlayerScore("Chris") want := 33 assertScoreEquals(t, got, want) }) } Copy t.Run("store wins for existing players", func(t *testing.T) { database, cleanDatabase := createTempFile(t, `[\ {"Name": "Cleo", "Wins": 10},\ {"Name": "Chris", "Wins": 33}]`) defer cleanDatabase() store := FileSystemPlayerStore{database} store.RecordWin("Chris") got := store.GetPlayerScore("Chris") want := 34 assertScoreEquals(t, got, want) }) Copy func (f *FileSystemPlayerStore) RecordWin(name string) { } Copy === RUN TestFileSystemStore/store_wins_for_existing_players --- FAIL: TestFileSystemStore/store_wins_for_existing_players (0.00s) FileSystemStore_test.go:71: got 33 want 34 Copy func (f *FileSystemPlayerStore) RecordWin(name string) { league := f.GetLeague() for i, player := range league { if player.Name == name { league[i].Wins++ } } f.database.Seek(0,0) json.NewEncoder(f.database).Encode(league) } Copy type League []Player func (l League) Find(name string) *Player { for i, p := range l { if p.Name==name { return &l[i] } } return nil } Copy func (f *FileSystemPlayerStore) GetPlayerScore(name string) int { player := f.GetLeague().Find(name) if player != nil { return player.Wins } return 0 } func (f *FileSystemPlayerStore) RecordWin(name string) { league := f.GetLeague() player := league.Find(name) if player != nil { player.Wins++ } f.database.Seek(0, 0) json.NewEncoder(f.database).Encode(league) } Copy t.Run("store wins for new players", func(t *testing.T) { database, cleanDatabase := createTempFile(t, `[\ {"Name": "Cleo", "Wins": 10},\ {"Name": "Chris", "Wins": 33}]`) defer cleanDatabase() store := FileSystemPlayerStore{database} store.RecordWin("Pepper") got := store.GetPlayerScore("Pepper") want := 1 assertScoreEquals(t, got, want) }) Copy === RUN TestFileSystemStore/store_wins_for_new_players#01 --- FAIL: TestFileSystemStore/store_wins_for_new_players#01 (0.00s) FileSystemStore_test.go:86: got 0 want 1 Copy func (f *FileSystemPlayerStore) RecordWin(name string) { league := f.GetLeague() player := league.Find(name) if player != nil { player.Wins++ } else { league = append(league, Player{name, 1}) } f.database.Seek(0, 0) json.NewEncoder(f.database).Encode(league) } Copy database, cleanDatabase := createTempFile(t, "") defer cleanDatabase() store := &FileSystemPlayerStore{database} Copy package main import ( "log" "net/http" "os" ) const dbFileName = "game.db.json" func main() { db, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666) if err != nil { log.Fatalf("problem opening %s %v", dbFileName, err) } store := &FileSystemPlayerStore{db} server := NewPlayerServer(store) if err := http.ListenAndServe(":5000", server); err != nil { log.Fatalf("could not listen on port 5000 %v", err) } } Copy type FileSystemPlayerStore struct { database io.ReadWriteSeeker league League } func NewFileSystemPlayerStore(database io.ReadWriteSeeker) *FileSystemPlayerStore { database.Seek(0, 0) league, _ := NewLeague(database) return &FileSystemPlayerStore{ database:database, league:league, } } Copy func (f *FileSystemPlayerStore) GetLeague() League { return f.league } func (f *FileSystemPlayerStore) GetPlayerScore(name string) int { player := f.league.Find(name) if player != nil { return player.Wins } return 0 } func (f *FileSystemPlayerStore) RecordWin(name string) { player := f.league.Find(name) if player != nil { player.Wins++ } else { f.league = append(f.league, Player{name, 1}) } f.database.Seek(0, 0) json.NewEncoder(f.database).Encode(f.league) } Copy package main import "io" type tape struct { file io.ReadWriteSeeker } func (t *tape) Write(p []byte) (n int, err error) { t.file.Seek(0, 0) return t.file.Write(p) } Copy type FileSystemPlayerStore struct { database io.Writer league League } Copy func NewFileSystemPlayerStore(database io.ReadWriteSeeker) *FileSystemPlayerStore { database.Seek(0, 0) league, _ := NewLeague(database) return &FileSystemPlayerStore{ database: &tape{database}, league: league, } } Copy func TestTape_Write(t *testing.T) { file, clean := createTempFile(t, "12345") defer clean() tape := &tape{file} tape.Write([]byte("abc")) file.Seek(0, 0) newFileContents, _ := ioutil.ReadAll(file) got := string(newFileContents) want := "abc" if got != want { t.Errorf("got %q want %q", got, want) } } Copy === RUN TestTape_Write --- FAIL: TestTape_Write (0.00s) tape_test.go:23: got 'abc45' want 'abc' Copy type tape struct { file *os.File } func (t *tape) Write(p []byte) (n int, err error) { t.file.Truncate(0) t.file.Seek(0, 0) return t.file.Write(p) } Copy type FileSystemPlayerStore struct { database *json.Encoder league League } Copy func NewFileSystemPlayerStore(file *os.File) *FileSystemPlayerStore { file.Seek(0, 0) league, _ := NewLeague(file) return &FileSystemPlayerStore{ database: json.NewEncoder(&tape{file}), league: league, } } Copy type ReadWriteSeekTruncate interface { io.ReadWriteSeeker Truncate(size int64) error } Copy func NewFileSystemPlayerStore(file *os.File) (*FileSystemPlayerStore, error) { file.Seek(0, 0) league, err := NewLeague(file) if err != nil { return nil, fmt.Errorf("problem loading player store from file %s, %v", file.Name(), err) } return &FileSystemPlayerStore{ database: json.NewEncoder(&tape{file}), league: league, }, nil } Copy if err != nil { return err } Copy ./main.go:18:35: multiple-value NewFileSystemPlayerStore() in single-value context ./FileSystemStore_test.go:35:36: multiple-value NewFileSystemPlayerStore() in single-value context ./FileSystemStore_test.go:57:36: multiple-value NewFileSystemPlayerStore() in single-value context ./FileSystemStore_test.go:70:36: multiple-value NewFileSystemPlayerStore() in single-value context ./FileSystemStore_test.go:85:36: multiple-value NewFileSystemPlayerStore() in single-value context ./server_integration_test.go:12:35: multiple-value NewFileSystemPlayerStore() in single-value context Copy store, err := NewFileSystemPlayerStore(db) if err != nil { log.Fatalf("problem creating file system player store, %v ", err) } Copy func assertNoError(t *testing.T, err error) { t.Helper() if err != nil { t.Fatalf("didn't expect an error but got one, %v", err) } } Copy === RUN TestRecordingWinsAndRetrievingThem --- FAIL: TestRecordingWinsAndRetrievingThem (0.00s) server_integration_test.go:14: didn't expect an error but got one, problem loading player store from file /var/folders/nj/r_ccbj5d7flds0sf63yy4vb80000gn/T/db841037437, problem parsing league, EOF Copy func TestRecordingWinsAndRetrievingThem(t *testing.T) { database, cleanDatabase := createTempFile(t, `[]`) //etc... Copy t.Run("works with an empty file", func(t *testing.T) { database, cleanDatabase := createTempFile(t, "") defer cleanDatabase() _, err := NewFileSystemPlayerStore(database) assertNoError(t, err) }) Copy === RUN TestFileSystemStore/works_with_an_empty_file --- FAIL: TestFileSystemStore/works_with_an_empty_file (0.00s) FileSystemStore_test.go:108: didn't expect an error but got one, problem loading player store from file /var/folders/nj/r_ccbj5d7flds0sf63yy4vb80000gn/T/db019548018, problem parsing league, EOF Copy func NewFileSystemPlayerStore(file *os.File) (*FileSystemPlayerStore, error) { file.Seek(0, 0) info, err := file.Stat() if err != nil { return nil, fmt.Errorf("problem getting file info from file %s, %v", file.Name(), err) } if info.Size() == 0 { file.Write([]byte("[]")) file.Seek(0, 0) } league, err := NewLeague(file) if err != nil { return nil, fmt.Errorf("problem loading player store from file %s, %v", file.Name(), err) } return &FileSystemPlayerStore{ database: json.NewEncoder(&tape{file}), league: league, }, nil } Copy func initialisePlayerDBFile(file *os.File) error { file.Seek(0, 0) info, err := file.Stat() if err != nil { return fmt.Errorf("problem getting file info from file %s, %v", file.Name(), err) } if info.Size()==0 { file.Write([]byte("[]")) file.Seek(0, 0) } return nil } Copy func NewFileSystemPlayerStore(file *os.File) (*FileSystemPlayerStore, error) { err := initialisePlayerDBFile(file) if err != nil { return nil, fmt.Errorf("problem initialising player db file, %v", err) } league, err := NewLeague(file) if err != nil { return nil, fmt.Errorf("problem loading player store from file %s, %v", file.Name(), err) } return &FileSystemPlayerStore{ database: json.NewEncoder(&tape{file}), league: league, }, nil } Copy t.Run("league sorted", func(t *testing.T) { database, cleanDatabase := createTempFile(t, `[\ {"Name": "Cleo", "Wins": 10},\ {"Name": "Chris", "Wins": 33}]`) defer cleanDatabase() store, err := NewFileSystemPlayerStore(database) assertNoError(t, err) got := store.GetLeague() want := []Player{ {"Chris", 33}, {"Cleo", 10}, } assertLeague(t, got, want) // read again got = store.GetLeague() assertLeague(t, got, want) }) Copy === RUN TestFileSystemStore/league_from_a_reader,_sorted --- FAIL: TestFileSystemStore/league_from_a_reader,_sorted (0.00s) FileSystemStore_test.go:46: got [{Cleo 10} {Chris 33}] want [{Chris 33} {Cleo 10}] FileSystemStore_test.go:51: got [{Cleo 10} {Chris 33}] want [{Chris 33} {Cleo 10}] Copy func (f *FileSystemPlayerStore) GetLeague() League { sort.Slice(f.league, func(i, j int) bool { return f.league[i].Wins > f.league[j].Wins }) return f.league } --- # Chapter Template | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/meta/template.md) . Some intro [](https://quii.gitbook.io/learn-go-with-tests/meta/template#write-the-test-first) Write the test first ------------------------------------------------------------------------------------------------------------ [](https://quii.gitbook.io/learn-go-with-tests/meta/template#try-to-run-the-test) Try to run the test ---------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/meta/template#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ [](https://quii.gitbook.io/learn-go-with-tests/meta/template#write-enough-code-to-make-it-pass) Write enough code to make it pass -------------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/meta/template#refactor) Refactor ------------------------------------------------------------------------------------ [](https://quii.gitbook.io/learn-go-with-tests/meta/template#repeat-for-new-requirements) Repeat for new requirements -------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/meta/template#wrapping-up) Wrapping up ------------------------------------------------------------------------------------------ [PreviousContributing](https://quii.gitbook.io/learn-go-with-tests/meta/contributing) Last updated 4 years ago * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/meta/template#write-the-test-first) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/meta/template#try-to-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/meta/template#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/meta/template#write-enough-code-to-make-it-pass) * [Refactor](https://quii.gitbook.io/learn-go-with-tests/meta/template#refactor) * [Repeat for new requirements](https://quii.gitbook.io/learn-go-with-tests/meta/template#repeat-for-new-requirements) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/meta/template#wrapping-up) --- # Context-aware Reader | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/context-aware-reader.md) . [**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) . [](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/context-aware-reader#context-aware-reader) Context aware reader? ------------------------------------------------------------------------------------------------------------------------------------------ First of all, a quick primer on `io.Reader`. If you've read other chapters in this book you will have ran into `io.Reader` when we've opened files, encoded JSON and various other common tasks. It's a simple abstraction over reading data from _something_ Copy type Reader interface { Read(p []byte) (n int, err error) } By using `io.Reader` you can gain a lot of re-use from the standard library, it's a very commonly used abstraction (along with its counterpart `io.Writer`) ### [](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/context-aware-reader#context-aware) Context aware? [In a previous chapter](https://quii.gitbook.io/learn-go-with-tests/go-fundamentals/context) we discussed how we can use `context` to provide cancellation. This is especially useful if you're performing tasks which may be computationally expensive and you want to be able to stop them. When you're using an `io.Reader` you have no guarantees over speed, it could take 1 nanosecond or hundreds of hours. You might find it useful to be able to cancel these kind of tasks in your own application and that's what Mat and David wrote about. They combined two simple abstractions (`context.Context` and `io.Reader`) to solve this problem. Let's try and TDD some functionality so that we can wrap an `io.Reader` so it can be cancelled. Testing this poses an interesting challenge. Normally when using an `io.Reader` you're usually supplying it to some other function and you don't really concern yourself with the details; such as `json.NewDecoder` or `io.ReadAll`. What we want to demonstrate is something like > Given an `io.Reader` with "ABCDEF", when I send a cancel signal half-way through I when I try to continue to read I get nothing else so all I get is "ABC" Let's look at the interface again. The `Reader`'s `Read` method will read the contents it has into a `[]byte` that we supply. So rather than reading everything, we could: * Supply a fixed-size byte array that doesn't fit all the contents * Send a cancel signal * Try and read again and this should return an error with 0 bytes read For now, let's just write a "happy path" test where there is no cancellation, just so we can get familiar with the problem without having to write any production code yet. * Make an `io.Reader` from a string with some data * A byte array to read into which is smaller than the contents of the reader * Call read, check the contents, repeat. From this we can imagine sending some kind of cancel signal before the second read to change behaviour. Now we've seen how it works we'll TDD the rest of the functionality. [](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/context-aware-reader#write-the-test-first) Write the test first ----------------------------------------------------------------------------------------------------------------------------------------- We want to be able to compose an `io.Reader` with a `context.Context`. With TDD it's best to start with imagining your desired API and write a test for it. From there let the compiler and failing test output can guide us to a solution [](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/context-aware-reader#try-to-run-the-test) Try to run the test --------------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/context-aware-reader#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) Write the minimal amount of code for the test to run and check the failing test output ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- We'll need to define this function and it should return an `io.Reader` If you try and run it As expected [](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/context-aware-reader#write-enough-code-to-make-it-pass) Write enough code to make it pass ------------------------------------------------------------------------------------------------------------------------------------------------------------------- For now, we'll just return the `io.Reader` we pass in The test should now pass. I know, I know, this seems silly and pedantic but before charging in to the fancy work it is important that we have _some_ verification that we haven't broken the "normal" behaviour of an `io.Reader` and this test will give us confidence as we move forward. [](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/context-aware-reader#write-the-test-first-1) Write the test first ------------------------------------------------------------------------------------------------------------------------------------------- Next we need to try and cancel. We can more or less copy the first test but now we're: * Creating a `context.Context` with cancellation so we can `cancel` after the first read * For our code to work we'll need to pass `ctx` to our function * We then assert that post-`cancel` nothing was read [](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/context-aware-reader#try-to-run-the-test-1) Try to run the test ----------------------------------------------------------------------------------------------------------------------------------------- [](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/context-aware-reader#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) Write the minimal amount of code for the test to run and check the failing test output ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- The compiler is telling us what to do; update our signature to accept a context (You'll need to update the first test to pass in `context.Background` too) You should now see a very clear failing test output [](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/context-aware-reader#write-enough-code-to-make-it-pass-1) Write enough code to make it pass --------------------------------------------------------------------------------------------------------------------------------------------------------------------- At this point, it's copy and paste from the original post by Mat and David but we'll still take it slowly and iteratively. We know we need to have a type that encapsulates the `io.Reader` that we read from and the `context.Context` so let's create that and try and return it from our function instead of the original `io.Reader` As I have stressed many times in this book, go slowly and let the compiler help you The abstraction feels right, but it doesn't implement the interface we need (`io.Reader`) so let's add the method. Run the tests and they should _compile_ but panic. This is still progress. Let's make the first test pass by just _delegating_ the call to our underlying `io.Reader` At this point we have our happy path test passing again and it feels like we have our stuff abstracted nicely To make our second test pass we need to check the `context.Context` to see if it has been cancelled. All tests should now pass. You'll notice how we return the error from the `context.Context`. This allows callers of the code to inspect the various reasons cancellation has occurred and this is covered more in the original post. [](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/context-aware-reader#wrapping-up) Wrapping up ----------------------------------------------------------------------------------------------------------------------- * Small interfaces are good and are easily composed * When you're trying to augment one thing (e.g `io.Reader`) with another you usually want to reach for the [delegation pattern](https://en.wikipedia.org/wiki/Delegation_pattern) > In software engineering, the delegation pattern is an object-oriented design pattern that allows object composition to achieve the same code reuse as inheritance. * An easy way to start this kind of work is to wrap your delegate and write a test that asserts it behaves how the delegate normally does before you start composing other parts to change behaviour. This will help you to keep things working correctly as you code toward your goal [PreviousError types](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/error-types) [NextRevisiting HTTP Handlers](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/http-handlers-revisited) Last updated 1 year ago * [Context aware reader?](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/context-aware-reader#context-aware-reader) * [Context aware?](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/context-aware-reader#context-aware) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/context-aware-reader#write-the-test-first) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/context-aware-reader#try-to-run-the-test) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/context-aware-reader#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/context-aware-reader#write-enough-code-to-make-it-pass) * [Write the test first](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/context-aware-reader#write-the-test-first-1) * [Try to run the test](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/context-aware-reader#try-to-run-the-test-1) * [Write the minimal amount of code for the test to run and check the failing test output](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/context-aware-reader#write-the-minimal-amount-of-code-for-the-test-to-run-and-check-the-failing-test-output-1) * [Write enough code to make it pass](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/context-aware-reader#write-enough-code-to-make-it-pass-1) * [Wrapping up](https://quii.gitbook.io/learn-go-with-tests/questions-and-answers/context-aware-reader#wrapping-up) Copy type Reader interface { Read(p []byte) (n int, err error) } Copy func TestContextAwareReader(t *testing.T) { t.Run("lets just see how a normal reader works", func(t *testing.T) { rdr := strings.NewReader("123456") got := make([]byte, 3) _, err := rdr.Read(got) if err != nil { t.Fatal(err) } assertBufferHas(t, got, "123") _, err = rdr.Read(got) if err != nil { t.Fatal(err) } assertBufferHas(t, got, "456") }) } func assertBufferHas(t testing.TB, buf []byte, want string) { t.Helper() got := string(buf) if got != want { t.Errorf("got %q, want %q", got, want) } } Copy t.Run("behaves like a normal reader", func(t *testing.T) { rdr := NewCancellableReader(strings.NewReader("123456")) got := make([]byte, 3) _, err := rdr.Read(got) if err != nil { t.Fatal(err) } assertBufferHas(t, got, "123") _, err = rdr.Read(got) if err != nil { t.Fatal(err) } assertBufferHas(t, got, "456") }) Copy ./cancel_readers_test.go:12:10: undefined: NewCancellableReader Copy func NewCancellableReader(rdr io.Reader) io.Reader { return nil } Copy === RUN TestCancelReaders === RUN TestCancelReaders/behaves_like_a_normal_reader panic: runtime error: invalid memory address or nil pointer dereference [recovered] panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x10f8fb5] Copy func NewCancellableReader(rdr io.Reader) io.Reader { return rdr } Copy t.Run("stops reading when cancelled", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) rdr := NewCancellableReader(ctx, strings.NewReader("123456")) got := make([]byte, 3) _, err := rdr.Read(got) if err != nil { t.Fatal(err) } assertBufferHas(t, got, "123") cancel() n, err := rdr.Read(got) if err == nil { t.Error("expected an error after cancellation but didn't get one") } if n > 0 { t.Errorf("expected 0 bytes to be read after cancellation but %d were read", n) } }) Copy ./cancel_readers_test.go:33:30: too many arguments in call to NewCancellableReader have (context.Context, *strings.Reader) want (io.Reader) Copy func NewCancellableReader(ctx context.Context, rdr io.Reader) io.Reader { return rdr } Copy === RUN TestCancelReaders === RUN TestCancelReaders/stops_reading_when_cancelled --- FAIL: TestCancelReaders (0.00s) --- FAIL: TestCancelReaders/stops_reading_when_cancelled (0.00s) cancel_readers_test.go:48: expected an error but didn't get one cancel_readers_test.go:52: expected 0 bytes to be read after cancellation but 3 were read Copy func NewCancellableReader(ctx context.Context, rdr io.Reader) io.Reader { return &readerCtx{ ctx: ctx, delegate: rdr, } } type readerCtx struct { ctx context.Context delegate io.Reader } Copy ./cancel_readers_test.go:60:3: cannot use &readerCtx literal (type *readerCtx) as type io.Reader in return argument: *readerCtx does not implement io.Reader (missing Read method) Copy func (r *readerCtx) Read(p []byte) (n int, err error) { panic("implement me") } Copy func (r readerCtx) Read(p []byte) (n int, err error) { return r.delegate.Read(p) } Copy func (r readerCtx) Read(p []byte) (n int, err error) { if err := r.ctx.Err(); err != nil { return 0, err } return r.delegate.Read(p) } --- # Anti-patterns | Learn Go with tests For the complete documentation index, see [llms.txt](https://quii.gitbook.io/learn-go-with-tests/llms.txt) . This page is also available as [Markdown](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns.md) . From time to time it's necessary to review your TDD techniques and remind yourself of behaviours to avoid. The TDD process is conceptually simple to follow, but as you do it you'll find it challenging your design skills. **Don't mistake this for TDD being hard, it's design that's hard!** This chapter lists a number of TDD and testing anti-patterns, and how to remedy them. [](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#not-doing-tdd-at-all) Not doing TDD at all ----------------------------------------------------------------------------------------------------------------- Of course, it is possible to write great software without TDD but, a lot of problems I've seen with the design of code and the quality of tests would be very difficult to arrive at if a disciplined approach to TDD had been used. One of the strengths of TDD is that it gives you a formal process to break down problems, understand what you're trying to achieve (red), get it done (green), then have a good think about how to make it right (blue/refactor). Without this, the process is often ad-hoc and loose, which _can_ make engineering more difficult than it _could_ be. [](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#misunderstanding-the-constraints-of-the-refactoring-step) Misunderstanding the constraints of the refactoring step ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- I have been in a number of workshops, mobbing or pairing sessions where someone has made a test pass and is in the refactoring stage. After some thought, they think it would be good to abstract away some code into a new struct; a budding pedant yells: > You're not allowed to do this! You should write a test for this first, we're doing TDD! This seems to be a common misunderstanding. **You can do whatever you like to the code when the tests are green**, the only thing you're not allowed to do is **add or change behaviour**. The point of these tests are to give you the _freedom to refactor_, find the right abstractions and make the code easier to change and understand. [](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#having-tests-that-wont-fail-or-evergreen-tests) Having tests that won't fail (or, evergreen tests) ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- It's astonishing how often this comes up. You start debugging or changing some tests and realise: there are no scenarios where this test can fail. Or at least, it won't fail in the way the test is _supposed_ to be protecting against. This is _next to impossible_ with TDD if you're following **the first step**, > Write a test, see it fail This is almost always done when developers write tests _after_ code is written, and/or chasing test coverage rather than creating a useful test suite. [](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#useless-assertions) Useless assertions ------------------------------------------------------------------------------------------------------------- Ever worked on a system, and you've broken a test, then you see this? > `false was not equal to true` I know that false is not equal to true. This is not a helpful message; it doesn't tell me what I've broken. This is a symptom of not following the TDD process and not reading the failure error message. Going back to the drawing board, > Write a test, see it fail (and don't be ashamed of the error message) [](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#asserting-on-irrelevant-detail) Asserting on irrelevant detail ------------------------------------------------------------------------------------------------------------------------------------- An example of this is making an assertion on a complex object, when in practice all you care about in the test is the value of one of the fields. Additional assertions not only make your test more difficult to read by creating 'noise' in your documentation, but also needlessly couples the test with data it doesn't care about. This means if you happen to change the fields for your object, or the way they behave you may get unexpected compilation problems or failures with your tests. This is an example of not following the red stage strictly enough. * Letting an existing design influence how you write your test **rather than thinking of the desired behaviour** * Not giving enough consideration to the failing test's error message [](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#lots-of-assertions-within-a-single-scenario-for-unit-tests) Lots of assertions within a single scenario for unit tests --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Many assertions can make tests difficult to read and challenging to debug when they fail. They often creep in gradually, especially if test setup is complicated because you're reluctant to replicate the same horrible setup to assert on something else. Instead of this you should fix the problems in your design which are making it difficult to assert on new things. A helpful rule of thumb is to aim to make one assertion per test. In Go, take advantage of subtests to clearly delineate between assertions on the occasions where you need to. This is also a handy technique to separate assertions on behaviour vs implementation detail. For other tests where setup or execution time may be a constraint (e.g an acceptance test driving a web browser), you need to weigh up the pros and cons of slightly trickier to debug tests against test execution time. [](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#not-listening-to-your-tests) Not listening to your tests ------------------------------------------------------------------------------------------------------------------------------- [Dave Farley in his video "When TDD goes wrong"](https://www.youtube.com/watch?v=UWtEVKVPBQ0&feature=youtu.be) points out, > TDD gives you the fastest feedback possible on your design From my own experience, a lot of developers are trying to practice TDD but frequently ignore the signals coming back to them from the TDD process. So they're still stuck with fragile, annoying systems, with a poor test suite. Simply put, if testing your code is difficult, then _using_ your code is difficult too. Treat your tests as the first user of your code and then you'll see if your code is pleasant to work with or not. I've emphasised this a lot in the book, and I'll say it again **listen to your tests**. ### [](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#excessive-setup-too-many-test-doubles-etc) Excessive setup, too many test doubles, etc. Ever looked at a test with 20, 50, 100, 200 lines of setup code before anything interesting in the test happens? Do you then have to change the code and revisit the mess and wish you had a different career? What are the signals here? _Listen_, complicated tests `==` complicated code. Why is your code complicated? Does it have to be? * When you have lots of test doubles in your tests, that means the code you're testing has lots of dependencies - which means your design needs work. * If your test is reliant on setting up various interactions with mocks, that means your code is making lots of interactions with its dependencies. Ask yourself whether these interactions could be simpler. #### [](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#leaky-interfaces) Leaky interfaces If you have declared an `interface` that has many methods, that points to a leaky abstraction. Think about how you could define that collaboration with a more consolidated set of methods, ideally one. #### [](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#interface-pollution) Interface pollution As a Go proverb says, _the bigger the interface, the weaker the abstraction_. If you expose a huge interface to the users of your package, you force them to create in their tests a stub/mock that matches the entire API, providing an implementation also for methods they do not use (sometimes, they just panic to make clear that they should not be used). This situation is an anti-pattern known as [interface pollution](https://rakyll.org/interface-pollution/) and this is the reason why the standard library offers you just tiny little interfaces. Instead, you should expose from your package a bare struct with all relevant methods exported, leaving to the clients of your API the freedom to declare their own interfaces abstracting over the subset of the methods they need: e.g [go-redis](https://github.com/redis/go-redis) exposes a struct (`redis.Client`) to the API clients. Generally speaking, you should expose an interface to the clients only when: * the interface consists of a small and coherent set of functions. * the interface and its implementation need to be decoupled (e.g. because users can choose among multiple implementations or they need to mock an external dependency). #### [](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#think-about-the-types-of-test-doubles-you-use) Think about the types of test doubles you use * Mocks are sometimes helpful, but they're extremely powerful and therefore easy to misuse. Try giving yourself the constraint of using stubs instead. * Verifying implementation detail with spies is sometimes helpful, but try to avoid it. Remember your implementation detail is usually not important, and you don't want your tests coupled to them if possible. Look to couple your tests to **useful behaviour rather than incidental details**. * [Read my posts on naming test doubles](https://quii.dev/Start_naming_your_test_doubles_correctly) if the taxonomy of test doubles is a little unclear #### [](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#consolidate-dependencies) Consolidate dependencies Here is some code for a `http.HandlerFunc` to handle new user registrations for a website. At first pass it's reasonable to say the design isn't so bad. It only has 2 dependencies! Re-evaluate the design by considering the handler's responsibilities: * Parse the request body into a `User` ✅ * Use `UserStore` to check if the user exists ❓ * Use `UserStore` to store the user ❓ * Compose an email ❓ * Use `Emailer` to send the email ❓ * Return an appropriate http response, depending on success, errors, etc ✅ To exercise this code, you're going to have to write many tests with varying degrees of test double setups, spies, etc * What if the requirements expand? Translations for the emails? Sending an SMS confirmation too? Does it make sense to you that you have to change a HTTP handler to accommodate this change? * Does it feel right that the important rule of "we should send an email" resides within a HTTP handler? * Why do you have to go through the ceremony of creating HTTP requests and reading responses to verify that rule? **Listen to your tests**. Writing tests for this code in a TDD fashion should quickly make you feel uncomfortable (or at least, make the lazy developer in you be annoyed). If it feels painful, stop and think. What if the design was like this instead? * Simple to test the handler ✅ * Changes to the rules around registration are isolated away from HTTP, so they are also simpler to test ✅ [](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#violating-encapsulation) Violating encapsulation ----------------------------------------------------------------------------------------------------------------------- Encapsulation is very important. There's a reason we don't make everything in a package exported (or public). We want coherent APIs with a small surface area to avoid tight coupling. People will sometimes be tempted to make a function or method public in order to test something. By doing this you make your design worse and send confusing messages to maintainers and users of your code. A result of this can be developers trying to debug a test and then eventually realising the function being tested is _only called from tests_. Which is obviously **a terrible outcome, and a waste of time**. In Go, consider your default position for writing tests as _from the perspective of a consumer of your package_. You can make this a compile-time constraint by having your tests live in a test package e.g `package gocoin_test`. If you do this, you'll only have access to the exported members of the package so it won't be possible to couple yourself to implementation detail. [](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#complicated-table-tests) Complicated table tests ----------------------------------------------------------------------------------------------------------------------- Table tests are a great way of exercising a number of different scenarios when the test setup is the same, and you only wish to vary the inputs. _But_ they can be messy to read and understand when you try to shoehorn other kinds of tests under the name of having one, glorious table. **Don't be afraid to break out of your table and write new tests** rather than adding new fields and booleans to the table `struct`. A thing to bear in mind when writing software is, > [Simple is not easy](https://www.infoq.com/presentations/Simple-Made-Easy/) "Just" adding a field to a table might be easy, but it can make things far from simple. [](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#summary) Summary --------------------------------------------------------------------------------------- Most problems with unit tests can normally be traced to: * Developers not following the TDD process * Poor design So, learn about good software design! The good news is TDD can help you _improve your design skills_ because as stated in the beginning: **TDD's main purpose is to provide feedback on your design.** For the millionth time, listen to your tests, they are reflecting your design back at you. Be honest about the quality of your tests by listening to the feedback they give you, and you'll become a better developer for it. [PreviousWhy unit tests and how to make them work for you](https://quii.gitbook.io/learn-go-with-tests/meta/why) [NextContributing](https://quii.gitbook.io/learn-go-with-tests/meta/contributing) Last updated 2 years ago * [Not doing TDD at all](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#not-doing-tdd-at-all) * [Misunderstanding the constraints of the refactoring step](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#misunderstanding-the-constraints-of-the-refactoring-step) * [Having tests that won't fail (or, evergreen tests)](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#having-tests-that-wont-fail-or-evergreen-tests) * [Useless assertions](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#useless-assertions) * [Asserting on irrelevant detail](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#asserting-on-irrelevant-detail) * [Lots of assertions within a single scenario for unit tests](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#lots-of-assertions-within-a-single-scenario-for-unit-tests) * [Not listening to your tests](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#not-listening-to-your-tests) * [Excessive setup, too many test doubles, etc.](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#excessive-setup-too-many-test-doubles-etc) * [Violating encapsulation](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#violating-encapsulation) * [Complicated table tests](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#complicated-table-tests) * [Summary](https://quii.gitbook.io/learn-go-with-tests/meta/anti-patterns#summary) Copy // not this, now your test is tightly coupled to the whole object if !cmp.Equal(complexObject, want) { t.Error("got %+v, want %+v", complexObject, want) } // be specific, and loosen the coupling got := complexObject.fieldYouCareAboutForThisTest if got != want { t.Error("got %q, want %q", got, want) } Copy type User struct { // Some user fields } type UserStore interface { CheckEmailExists(email string) (bool, error) StoreUser(newUser User) error } type Emailer interface { SendEmail(to User, body string, subject string) error } func NewRegistrationHandler(userStore UserStore, emailer Emailer) http.HandlerFunc { return func(writer http.ResponseWriter, request *http.Request) { // extract out the user from the request body (handle error) // check user exists (handle duplicates, errors) // store user (handle errors) // compose and send confirmation email (handle error) // if we got this far, return 2xx response } } Copy type UserService interface { Register(newUser User) error } func NewRegistrationHandler(userService UserService) http.HandlerFunc { return func(writer http.ResponseWriter, request *http.Request) { // parse user // register user // check error, send response } } Copy cases := []struct { X int Y int Z int err error IsFullMoon bool IsLeapYear bool AtWarWithEurasia bool }{} ---