[Art by Denise](https://twitter.com/deniseyu21)
[](https://goreportcard.com/report/github.com/quii/learn-go-with-tests)
## Formats
- [Gitbook](https://quii.gitbook.io/learn-go-with-tests)
- [EPUB or PDF](https://github.com/quii/learn-go-with-tests/releases)
## 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/)
- [فارسی](https://go-yaad-begir.gitbook.io/go-ba-test/)
- [Nederlands](https://bobkosse.gitbook.io/leer-go-met-tests/)
## 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
- [Buy me a coffee :coffee:](https://www.buymeacoffee.com/quii)
- [Sponsor me on GitHub](https://github.com/sponsors/quii)
## Why
* 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
* [Watch a video, or read about why unit testing and TDD is important](why.md)
## Table of contents
### Go fundamentals
1. [Install Go](install-go.md) - Set up environment for productivity.
2. [Hello, world](hello-world.md) - Declaring variables, constants, if/else statements, switch, write your first go program and write your first test. Sub-test syntax and closures.
3. [Integers](integers.md) - Further Explore function declaration syntax and learn new ways to improve the documentation of your code.
4. [Iteration](iteration.md) - Learn about `for` and benchmarking.
5. [Arrays and slices](arrays-and-slices.md) - Learn about arrays, slices, `len`, varargs, `range` and test coverage.
6. [Structs, methods & interfaces](structs-methods-and-interfaces.md) - Learn about `struct`, methods, `interface` and table driven tests.
7. [Pointers & errors](pointers-and-errors.md) - Learn about pointers and errors.
8. [Maps](maps.md) - Learn about storing values in the map data structure.
9. [Dependency Injection](dependency-injection.md) - Learn about dependency injection, how it relates to using interfaces and a primer on io.
10. [Mocking](mocking.md) - Take some existing untested code and use DI with mocking to test it.
11. [Concurrency](concurrency.md) - Learn how to write concurrent code to make your software faster.
12. [Select](select.md) - Learn how to synchronise asynchronous processes elegantly.
13. [Reflection](reflection.md) - Learn about reflection
14. [Sync](sync.md) - Learn some functionality from the sync package including `WaitGroup` and `Mutex`
15. [Context](context.md) - Use the context package to manage and cancel long-running processes
16. [Intro to property based tests](roman-numerals.md) - Practice some TDD with the Roman Numerals kata and get a brief intro to property based tests
17. [Maths](math.md) - Use the `math` package to draw an SVG clock
18. [Reading files](reading-files.md) - Read files and process them
19. [Templating](html-templates.md) - Use Go's html/template package to render html from data, and also learn about approval testing
20. [Generics](generics.md) - Learn how to write functions that take generic arguments and make your own generic data-structure
21. [Revisiting arrays and slices with generics](revisiting-arrays-and-slices-with-generics.md) - Generics are very useful when working with collections. Learn how to write your own `Reduce` function and tidy up some common patterns.
### Build an application
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](http-server.md) - We will create an application which listens to HTTP requests and responds to them.
* [JSON, routing and embedding](json.md) - We will make our endpoints return JSON and explore how to do routing.
* [IO and sorting](io.md) - We will persist and read our data from disk and we'll cover sorting data.
* [Command line & project structure](command-line.md) - Support multiple applications from one code base and read input from command line.
* [Time](time.md) - using the `time` package to schedule activities.
* [WebSockets](websockets.md) - learn how to write and test a server that uses WebSockets.
### Testing fundamentals
Covering other subjects around testing.
* [Introduction to acceptance tests](intro-to-acceptance-tests.md) - Learn how to write acceptance tests for your code, with a real-world example for gracefully shutting down a HTTP server
* [Scaling acceptance tests](scaling-acceptance-tests.md) - Learn techniques to manage the complexity of writing acceptance tests for non-trivial systems.
* [Working without mocks, stubs and spies](working-without-mocks.md) - Learn about how to use fakes and contracts to create more realistic and maintainable tests.
* [Refactoring Checklist](refactoring-checklist.md) - Some discussion on what refactoring is, and some basic tips on how to do it.
### Questions and answers
I often run in to questions on the internets like
> How do I test my amazing function that does x, y and z
If you have such a question raise it as an issue on github and I'll try and find time to write a short chapter to tackle the issue. I feel like content like this is valuable as it is tackling people's _real_ questions around testing.
* [OS exec](os-exec.md) - An example of how we can reach out to the OS to execute commands to fetch data and keep our business logic testable/
* [Error types](error-types.md) - Example of creating your own error types to improve your tests and make your code easier to work with.
* [Context-aware Reader](context-aware-reader.md) - Learn how to TDD augmenting `io.Reader` with cancellation. Based on [Context-aware io.Reader for Go](https://pace.dev/blog/2020/02/03/context-aware-ioreader-for-golang-by-mat-ryer)
* [Revisiting HTTP Handlers](http-handlers-revisited.md) - Testing HTTP handlers seems to be the bane of many a developer's existence. This chapter explores the issues around designing handlers correctly.
### Meta / Discussion
* [Why unit tests and how to make them work for you](why.md) - Watch a video, or read about why unit testing and TDD is important
* [Anti-patterns](anti-patterns.md) - A short chapter on TDD and unit testing anti-patterns
## Contributing
* _This project is work in progress_ If you would like to contribute, please do get in touch.
* Read [contributing.md](https://github.com/quii/learn-go-with-tests/tree/842f4f24d1f1c20ba3bb23cbc376c7ca6f7ca79a/contributing.md) for guidelines
* Any ideas? Create an issue
## 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.
### What didn't work
#### 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.
#### 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_.
### 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.
### 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.
## Who this is for
* People who are interested in picking up Go.
* People who already know some Go, but want to explore testing with TDD.
## 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
## Feedback
* Add issues/submit PRs [here](https://github.com/quii/learn-go-with-tests) or [tweet me @quii](https://twitter.com/quii)
[MIT license](LICENSE.md)
[Logo is by egonelbre](https://github.com/egonelbre) What a star!
================================================
FILE: SUMMARY.md
================================================
# Table of contents
* [Learn Go with Tests](gb-readme.md)
## Go fundamentals
* [Install Go](install-go.md)
* [Hello, World](hello-world.md)
* [Integers](integers.md)
* [Iteration](iteration.md)
* [Arrays and slices](arrays-and-slices.md)
* [Structs, methods & interfaces](structs-methods-and-interfaces.md)
* [Pointers & errors](pointers-and-errors.md)
* [Maps](maps.md)
* [Dependency Injection](dependency-injection.md)
* [Mocking](mocking.md)
* [Concurrency](concurrency.md)
* [Select](select.md)
* [Reflection](reflection.md)
* [Sync](sync.md)
* [Context](context.md)
* [Intro to property based tests](roman-numerals.md)
* [Maths](math.md)
* [Reading files](reading-files.md)
* [Templating](html-templates.md)
* [Generics](generics.md)
* [Revisiting arrays and slices with generics](revisiting-arrays-and-slices-with-generics.md)
## Testing fundamentals
* [Introduction to acceptance tests](intro-to-acceptance-tests.md)
* [Scaling acceptance tests](scaling-acceptance-tests.md)
* [Working without mocks](working-without-mocks.md)
* [Refactoring Checklist](refactoring-checklist.md)
## Build an application
* [Intro](app-intro.md)
* [HTTP server](http-server.md)
* [JSON, routing and embedding](json.md)
* [IO and sorting](io.md)
* [Command line & package structure](command-line.md)
* [Time](time.md)
* [WebSockets](websockets.md)
## Questions and answers
* [OS Exec](os-exec.md)
* [Error types](error-types.md)
* [Context-aware Reader](context-aware-reader.md)
* [Revisiting HTTP Handlers](http-handlers-revisited.md)
## Meta
* [Why unit tests and how to make them work for you](why.md)
* [Anti-patterns](anti-patterns.md)
* [Contributing](contributing.md)
* [Chapter Template](template.md)
================================================
FILE: anti-patterns.md
================================================
# TDD Anti-patterns
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.
## 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.
## 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.
## 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.
## 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)
## 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.
```go
// 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)
}
```
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
## 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.
## 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**.
### 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.
#### 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.
#### 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).
#### 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
#### Consolidate dependencies
Here is some code for a `http.HandlerFunc` to handle new user registrations for a website.
```go
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
}
}
```
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` :white_check_mark:
- Use `UserStore` to check if the user exists :question:
- Use `UserStore` to store the user :question:
- Compose an email :question:
- Use `Emailer` to send the email :question:
- Return an appropriate http response, depending on success, errors, etc :white_check_mark:
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?
```go
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
}
}
```
- Simple to test the handler ✅
- Changes to the rules around registration are isolated away from HTTP, so they are also simpler to test ✅
## 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.
## 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.
```go
cases := []struct {
X int
Y int
Z int
err error
IsFullMoon bool
IsLeapYear bool
AtWarWithEurasia bool
}{}
```
**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.
## 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.
================================================
FILE: app-intro.md
================================================
# Build an application
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](http-server.md) - We will create an application which listens to HTTP requests and responds to them.
- [JSON, routing and embedding](json.md) - We will make our endpoints return JSON and explore how to do routing.
- [IO and sorting](io.md) - We will persist and read our data from disk and we'll cover sorting data.
- [Command line & project structure](command-line.md) - Support multiple applications from one code base and read input from command line.
- [Time](time.md) - using the `time` package to schedule activities.
- [WebSockets](websockets.md) - learn how to write and test a server that uses WebSockets.
================================================
FILE: arrays/v1/sum.go
================================================
package main
// Sum calculates the total from an array of numbers.
func Sum(numbers [5]int) int {
sum := 0
for i := 0; i < 5; i++ {
sum += numbers[i]
}
return sum
}
================================================
FILE: arrays/v1/sum_test.go
================================================
package main
import "testing"
func TestSum(t *testing.T) {
numbers := [5]int{1, 2, 3, 4, 5}
got := Sum(numbers)
want := 15
if want != got {
t.Errorf("got %d want %d given, %v", got, want, numbers)
}
}
================================================
FILE: arrays/v2/sum.go
================================================
package main
// Sum calculates the total from an array of numbers.
func Sum(numbers [5]int) int {
sum := 0
for _, number := range numbers {
sum += number
}
return sum
}
================================================
FILE: arrays/v2/sum_test.go
================================================
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)
}
}
================================================
FILE: arrays/v3/sum.go
================================================
package main
// Sum calculates the total from a slice of numbers.
func Sum(numbers []int) int {
sum := 0
for _, number := range numbers {
sum += number
}
return sum
}
================================================
FILE: arrays/v3/sum_test.go
================================================
package main
import "testing"
func TestSum(t *testing.T) {
t.Run("collections 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)
}
})
}
================================================
FILE: arrays/v4/sum.go
================================================
package main
// Sum calculates the total from a slice of numbers.
func Sum(numbers []int) int {
sum := 0
for _, number := range numbers {
sum += number
}
return sum
}
// SumAll calculates the respective sums of every slice passed in.
func SumAll(numbersToSum ...[]int) []int {
lengthOfNumbers := len(numbersToSum)
sums := make([]int, lengthOfNumbers)
for i, numbers := range numbersToSum {
sums[i] = Sum(numbers)
}
return sums
}
================================================
FILE: arrays/v4/sum_test.go
================================================
package main
import (
"slices"
"testing"
)
func TestSum(t *testing.T) {
t.Run("collections 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)
}
})
}
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)
}
}
================================================
FILE: arrays/v5/sum.go
================================================
package main
// Sum calculates the total from a slice of numbers.
func Sum(numbers []int) int {
sum := 0
for _, number := range numbers {
sum += number
}
return sum
}
// SumAll calculates the respective sums of every slice passed in.
func SumAll(numbersToSum ...[]int) []int {
var sums []int
for _, numbers := range numbersToSum {
sums = append(sums, Sum(numbers))
}
return sums
}
================================================
FILE: arrays/v5/sum_test.go
================================================
package main
import (
"slices"
"testing"
)
func TestSum(t *testing.T) {
t.Run("collections 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)
}
})
}
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)
}
}
================================================
FILE: arrays/v6/sum.go
================================================
package main
// Sum calculates the total from a slice of numbers.
func Sum(numbers []int) int {
sum := 0
for _, number := range numbers {
sum += number
}
return sum
}
// SumAllTails calculates the respective sums of every slice passed in.
func SumAllTails(numbersToSum ...[]int) []int {
var sums []int
for _, numbers := range numbersToSum {
tail := numbers[1:]
sums = append(sums, Sum(tail))
}
return sums
}
================================================
FILE: arrays/v6/sum_test.go
================================================
package main
import (
"slices"
"testing"
)
func TestSum(t *testing.T) {
t.Run("collections 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)
}
})
}
func TestSumAllTails(t *testing.T) {
got := SumAllTails([]int{1, 2}, []int{0, 9})
want := []int{2, 9}
if !slices.Equal(got, want) {
t.Errorf("got %v want %v", got, want)
}
}
================================================
FILE: arrays/v7/sum.go
================================================
package main
// Sum calculates the total from a slice of numbers.
func Sum(numbers []int) int {
sum := 0
for _, number := range numbers {
sum += number
}
return sum
}
// SumAllTails calculates the sums of all but the first number given a collection of slices.
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
}
================================================
FILE: arrays/v7/sum_test.go
================================================
package main
import (
"slices"
"testing"
)
func TestSum(t *testing.T) {
t.Run("collections 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)
}
})
}
func TestSumAllTails(t *testing.T) {
checkSums := func(t *testing.T, got, want []int) {
if !slices.Equal(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)
})
}
================================================
FILE: arrays/v8/assert.go
================================================
package main
import "testing"
func AssertEqual[T comparable](t *testing.T, got, want T) {
t.Helper()
if got != want {
t.Errorf("got %v, want %v", got, want)
}
}
func AssertNotEqual[T comparable](t *testing.T, got, want T) {
t.Helper()
if got == want {
t.Errorf("didn't want %v", got)
}
}
func AssertTrue(t *testing.T, got bool) {
t.Helper()
if !got {
t.Errorf("got %v, want true", got)
}
}
func AssertFalse(t *testing.T, got bool) {
t.Helper()
if got {
t.Errorf("got %v, want false", got)
}
}
================================================
FILE: arrays/v8/bad_bank.go
================================================
package main
type Transaction struct {
From string
To string
Sum float64
}
func NewTransaction(from, to Account, sum float64) Transaction {
return Transaction{From: from.Name, To: to.Name, Sum: sum}
}
type Account struct {
Name string
Balance float64
}
func NewBalanceFor(account Account, transactions []Transaction) Account {
return Reduce(
transactions,
applyTransaction,
account,
)
}
func applyTransaction(a Account, transaction Transaction) Account {
if transaction.From == a.Name {
a.Balance -= transaction.Sum
}
if transaction.To == a.Name {
a.Balance += transaction.Sum
}
return a
}
================================================
FILE: arrays/v8/bad_bank_test.go
================================================
package main
import "testing"
func TestBadBank(t *testing.T) {
var (
riya = Account{Name: "Riya", Balance: 100}
chris = Account{Name: "Chris", Balance: 75}
adil = Account{Name: "Adil", Balance: 200}
transactions = []Transaction{
NewTransaction(chris, riya, 100),
NewTransaction(adil, chris, 25),
}
)
newBalanceFor := func(account Account) float64 {
return NewBalanceFor(account, transactions).Balance
}
AssertEqual(t, newBalanceFor(riya), 200)
AssertEqual(t, newBalanceFor(chris), 0)
AssertEqual(t, newBalanceFor(adil), 175)
}
================================================
FILE: arrays/v8/collection_fun.go
================================================
package main
func Find[A any](items []A, predicate func(A) bool) (value A, found bool) {
for _, v := range items {
if predicate(v) {
return v, true
}
}
return
}
func Reduce[A, B any](collection []A, f func(B, A) B, initialValue B) B {
var result = initialValue
for _, x := range collection {
result = f(result, x)
}
return result
}
================================================
FILE: arrays/v8/sum.go
================================================
package main
// Sum calculates the total from a slice of numbers.
func Sum(numbers []int) int {
add := func(acc, x int) int { return acc + x }
return Reduce(numbers, add, 0)
}
// SumAllTails calculates the sums of all but the first number given a collection of slices.
func SumAllTails(numbers ...[]int) []int {
sumTail := func(acc, x []int) []int {
if len(x) == 0 {
return append(acc, 0)
} else {
tail := x[1:]
return append(acc, Sum(tail))
}
}
return Reduce(numbers, sumTail, []int{})
}
================================================
FILE: arrays/v8/sum_test.go
================================================
package main
import (
"slices"
"strings"
"testing"
)
func TestSum(t *testing.T) {
t.Run("collections 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)
}
})
}
func TestSumAllTails(t *testing.T) {
checkSums := func(t *testing.T, got, want []int) {
if !slices.Equal(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)
})
}
func TestReduce(t *testing.T) {
t.Run("multiplication of all elements", func(t *testing.T) {
multiply := func(x, y int) int {
return x * y
}
AssertEqual(t, Reduce([]int{1, 2, 3}, multiply, 1), 6)
})
t.Run("concatenate strings", func(t *testing.T) {
concatenate := func(x, y string) string {
return x + y
}
AssertEqual(t, Reduce([]string{"a", "b", "c"}, concatenate, ""), "abc")
})
}
func TestFind(t *testing.T) {
t.Run("find first even number", func(t *testing.T) {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
firstEvenNumber, found := Find(numbers, func(x int) bool {
return x%2 == 0
})
AssertTrue(t, found)
AssertEqual(t, firstEvenNumber, 2)
})
type Person struct {
Name string
}
t.Run("Find the best programmer", func(t *testing.T) {
people := []Person{
Person{Name: "Kent Beck"},
Person{Name: "Martin Fowler"},
Person{Name: "Chris James"},
}
king, found := Find(people, func(p Person) bool {
return strings.Contains(p.Name, "Chris")
})
AssertTrue(t, found)
AssertEqual(t, king, Person{Name: "Chris James"})
})
}
================================================
FILE: arrays-and-slices.md
================================================
# Arrays and slices
**[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`](iteration.md) to make a `Sum` function. `Sum` will
take an array of numbers and return the total.
Let's use our TDD skills
## Write the test first
Create a new folder to work in. Create a new file called `sum_test.go` and insert the following:
```go
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/)
## 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.
## Write the minimal amount of code for the test to run and check the failing test output
In `sum.go`
```go
package main
func Sum(numbers [5]int) int {
return 0
}
```
Your test should now fail with _a clear error message_
`sum_test.go:13: got 0 want 15 given, [1 2 3 4 5]`
## Write enough code to make it pass
```go
func Sum(numbers [5]int) int {
sum := 0
for i := 0; i < 5; i++ {
sum += numbers[i]
}
return sum
}
```
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`.
## Refactor
Let's introduce [`range`](https://gobyexample.com/range) to help clean up our code
```go
func Sum(numbers [5]int) int {
sum := 0
for _, number := range numbers {
sum += number
}
return sum
}
```
`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).
### 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.
## Write the test first
We will now use the [slice type][slice] 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}`
```go
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)
}
})
}
```
## 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`
## 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.
```go
func Sum(numbers []int) int {
sum := 0
for _, number := range numbers {
sum += number
}
return sum
}
```
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.
## 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!
## 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.
```go
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)
}
})
}
```
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
```bash
PASS
coverage: 100.0% of statements
```
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}`
## Write the test first
```go
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)
}
}
```
## Try and run the test
`./sum_test.go:23:9: undefined: SumAll`
## 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.
```go
func SumAll(numbersToSum ...[]int) []int {
return nil
}
```
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!
```go
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)
}
}
```
You should have test output like the following:
`sum_test.go:30: got [] want [3 9]`
## 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
```go
func SumAll(numbersToSum ...[]int) []int {
lengthOfNumbers := len(numbersToSum)
sums := make([]int, lengthOfNumbers)
for i, numbers := range numbersToSum {
sums[i] = Sum(numbers)
}
return sums
}
```
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.
## 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.
```go
func SumAll(numbersToSum ...[]int) []int {
var sums []int
for _, numbers := range numbersToSum {
sums = append(sums, Sum(numbers))
}
return sums
}
```
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"\).
## Write the test first
```go
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)
}
}
```
## Try and run the test
`./sum_test.go:26:9: undefined: SumAllTails`
## 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]`
## Write enough code to make it pass
```go
func SumAllTails(numbersToSum ...[]int) []int {
var sums []int
for _, numbers := range numbersToSum {
tail := numbers[1:]
sums = append(sums, Sum(tail))
}
return sums
}
```
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.
## 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:]`?
## Write the test first
```go
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)
}
})
}
```
## Try and run the test
```text
panic: runtime error: slice bounds out of range [recovered]
panic: runtime error: slice bounds out of range
```
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.
## Write enough code to make it pass
```go
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
}
```
## Refactor
Our tests have some repeated code around the assertions again, so let's extract those into a function.
```go
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)
})
}
```
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.
```bash
$ go test
./sum_test.go:52:21: cannot use "dave" (type string) as type []int in argument to checkSums
```
## 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][blog-slice] 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.
[for]: ../iteration.md#
[blog-slice]: https://blog.golang.org/go-slices-usage-and-internals
[deepEqual]: https://golang.org/pkg/reflect/#DeepEqual
[slice]: https://golang.org/doc/effective_go.html#slices
================================================
FILE: blogrenderer/post.go
================================================
package blogrenderer
import "strings"
// Post is a representation of a post
type Post struct {
Title, Description, Body string
Tags []string
}
// SanitisedTitle returns the title of the post with spaces replaced by dashes for pleasant URLs
func (p Post) SanitisedTitle() string {
return strings.ToLower(strings.Replace(p.Title, " ", "-", -1))
}
================================================
FILE: blogrenderer/renderer.go
================================================
package blogrenderer
import (
"embed"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/parser"
"html/template"
"io"
)
var (
//go:embed "templates/*"
postTemplates embed.FS
)
// PostRenderer renders data into HTML
type PostRenderer struct {
templ *template.Template
mdParser *parser.Parser
}
// NewPostRenderer creates a new PostRenderer
func NewPostRenderer() (*PostRenderer, error) {
templ, err := template.ParseFS(postTemplates, "templates/*.gohtml")
if err != nil {
return nil, err
}
extensions := parser.CommonExtensions | parser.AutoHeadingIDs
parser := parser.NewWithExtensions(extensions)
return &PostRenderer{templ: templ, mdParser: parser}, nil
}
// Render renders post into HTML
func (r *PostRenderer) Render(w io.Writer, p Post) error {
return r.templ.ExecuteTemplate(w, "blog.gohtml", newPostVM(p, r))
}
// RenderIndex creates an HTML index page given a collection of posts
func (r *PostRenderer) RenderIndex(w io.Writer, posts []Post) error {
return r.templ.ExecuteTemplate(w, "index.gohtml", posts)
}
type postViewModel struct {
Post
HTMLBody template.HTML
}
func newPostVM(p Post, r *PostRenderer) postViewModel {
vm := postViewModel{Post: p}
vm.HTMLBody = template.HTML(markdown.ToHTML([]byte(p.Body), r.mdParser, nil))
return vm
}
================================================
FILE: blogrenderer/renderer_test.TestRender.it_converts_a_single_post_into_HTML.approved.txt
================================================
My amazing blog!
hello world
This is a description
Tags:
go
tdd
First recipe!
Welcome to my amazing blog. I am going to write about my family recipes, and make sure I write a long, irrelevant and boring story about my family before you get to the actual instructions.
================================================
FILE: blogrenderer/renderer_test.TestRender.it_renders_an_index_of_posts.approved.txt
================================================
My amazing blog!
================================================
FILE: blogrenderer/renderer_test.go
================================================
package blogrenderer_test
import (
"bytes"
approvals "github.com/approvals/go-approval-tests"
"github.com/quii/learn-go-with-tests/blogrenderer"
"io"
"testing"
)
func TestRender(t *testing.T) {
var (
aPost = blogrenderer.Post{
Title: "hello world",
Body: `# First recipe!
Welcome to my **amazing blog**. I am going to write about my family recipes, and make sure I write a long, irrelevant and boring story about my family before you get to the actual instructions.`,
Description: "This is a description",
Tags: []string{"go", "tdd"},
}
)
postRenderer, err := blogrenderer.NewPostRenderer()
if err != nil {
t.Fatal(err)
}
t.Run("it converts a single post into HTML", func(t *testing.T) {
buf := bytes.Buffer{}
if err := postRenderer.Render(&buf, aPost); err != nil {
t.Fatal(err)
}
approvals.VerifyString(t, buf.String())
})
t.Run("it renders an index of posts", func(t *testing.T) {
buf := bytes.Buffer{}
posts := []blogrenderer.Post{{Title: "Hello World"}, {Title: "Hello World 2"}}
if err := postRenderer.RenderIndex(&buf, posts); err != nil {
t.Fatal(err)
}
approvals.VerifyString(t, buf.String())
})
}
func BenchmarkRender(b *testing.B) {
var (
aPost = blogrenderer.Post{
Title: "hello world",
Body: "This is a post",
Description: "This is a description",
Tags: []string{"go", "tdd"},
}
)
postRenderer, err := blogrenderer.NewPostRenderer()
if err != nil {
b.Fatal(err)
}
for b.Loop() {
postRenderer.Render(io.Discard, aPost)
}
}
================================================
FILE: blogrenderer/templates/blog.gohtml
================================================
{{template "top" .}}