Skip to content

Commit

Permalink
Demonstrate a custom iterator from go1.23
Browse files Browse the repository at this point in the history
  • Loading branch information
quii committed Jan 3, 2025
1 parent 2a97c2b commit dc172e1
Show file tree
Hide file tree
Showing 7 changed files with 233 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.22
go-version: 1.23

- name: Build
run: ./build.sh
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/quii/learn-go-with-tests

go 1.22
go 1.23

require (
github.com/approvals/go-approval-tests v0.0.0-20211008131110-0c40b30e0000
Expand Down
2 changes: 1 addition & 1 deletion intro-to-acceptance-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ import (

const (
port = "8080"
url = "<http://localhost:" +port
url = "<http://localhost:" + port
)

func TestGracefulShutdown(t *testing.T) {
Expand Down
55 changes: 55 additions & 0 deletions iterators/iterators_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package iterators

import (
"iter"
"slices"
"testing"
)

func Concatenate(seq iter.Seq[string]) string {
var result string
for s := range seq {
result += s
}
return result
}

// annoyingly, there is no builtin way to go from seq2, to seq (e.g just get the values)
func Values[K, V any](seq iter.Seq2[K, V]) iter.Seq[V] {
return func(yield func(V) bool) {
for _, v := range seq {
if !yield(v) {
return
}
}
}
}

// WIP!
func TestConcatenate(t *testing.T) {
t.Run("values of a slice", func(t *testing.T) {
got := Concatenate(slices.Values([]string{"a", "b", "c"}))
want := "abc"
if got != want {
t.Errorf("got %q want %q", got, want)
}
})

t.Run("values of a slice backwards", func(t *testing.T) {
backward := slices.Backward([]string{"a", "b", "c"})

got := Concatenate(Values(backward))
want := "cba"
if got != want {
t.Errorf("got %q want %q", got, want)
}
})

t.Run("values of a slice sorted", func(t *testing.T) {
got := Concatenate(slices.Values(slices.Sorted(slices.Values([]string{"c", "a", "b"}))))
want := "abc"
if got != want {
t.Errorf("got %q want %q", got, want)
}
})
}
42 changes: 42 additions & 0 deletions mocking.md
Original file line number Diff line number Diff line change
Expand Up @@ -636,3 +636,45 @@ In this post about mocking we have only covered **Spies**, which are a kind of m
> [Test Double is a generic term for any case where you replace a production object for testing purposes.](https://martinfowler.com/bliki/TestDouble.html)
Under test doubles, there are various types like stubs, spies and indeed mocks! Check out [Martin Fowler's post](https://martinfowler.com/bliki/TestDouble.html) for more detail.

## Bonus - Example of iterators from go 1.23

In Go 1.23 [iterators were introduced](https://tip.golang.org/doc/go1.23). We can use iterators in various ways, in this instance we can make a `countdownFrom` iterator, which will return the numbers to countdown in reverse order.

```go
func Countdown(out io.Writer, sleeper Sleeper) {
for i := range countDownFrom(3) {
fmt.Fprintln(out, i)
sleeper.Sleep()
}

fmt.Fprint(out, finalWord)
}
```

Before we get into how we write custom iterators, let's see how we use it. Rather than writing a fairly imperative looking loop to count down from a number, we can make this code look more expressive by `range`-ing over our custom `countdownFrom` iterator.

To write an iterator, a function that can be used in a `range` loop, you need to write a function in a particular way. From the docs:

The “range” clause in a “for-range” loop now accepts iterator functions of the following types
func(func() bool)
func(func(K) bool)
func(func(K, V) bool)

(The `K` and `V` stand for key and value types, respectively.)

In our case, we don't have keys, just values. Go also provides a convenience type `iter.Seq[T]` which is a type alias for `func(func(T) bool)`.

```go
func countDownFrom(from int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := from; i > 0; i-- {
if !yield(i) {
return
}
}
}
}
```

This is a simple iterator, which will yield the numbers in reverse order - perfect for our usecase.
81 changes: 81 additions & 0 deletions mocking/v6/countdown_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package main

import (
"bytes"
"reflect"
"testing"
"time"
)

func TestCountdown(t *testing.T) {

t.Run("prints 3 to Go!", func(t *testing.T) {
buffer := &bytes.Buffer{}
Countdown(buffer, &SpyCountdownOperations{})

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 := &SpyCountdownOperations{}
Countdown(spySleepPrinter, spySleepPrinter)

want := []string{
write,
sleep,
write,
sleep,
write,
sleep,
write,
}

if !reflect.DeepEqual(want, spySleepPrinter.Calls) {
t.Errorf("wanted calls %v got %v", want, spySleepPrinter.Calls)
}
})
}

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)
}
}

type SpyCountdownOperations struct {
Calls []string
}

func (s *SpyCountdownOperations) Sleep() {
s.Calls = append(s.Calls, sleep)
}

func (s *SpyCountdownOperations) Write(p []byte) (n int, err error) {
s.Calls = append(s.Calls, write)
return
}

const write = "write"
const sleep = "sleep"

type SpyTime struct {
durationSlept time.Duration
}

func (s *SpyTime) Sleep(duration time.Duration) {
s.durationSlept = duration
}
52 changes: 52 additions & 0 deletions mocking/v6/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package main

import (
"fmt"
"io"
"iter"
"os"
"time"
)

// Sleeper allows you to put delays.
type Sleeper interface {
Sleep()
}

// ConfigurableSleeper is an implementation of Sleeper with a defined delay.
type ConfigurableSleeper struct {
duration time.Duration
sleep func(time.Duration)
}

// Sleep will pause execution for the defined Duration.
func (c *ConfigurableSleeper) Sleep() {
c.sleep(c.duration)
}

const finalWord = "Go!"

// Countdown prints a countdown from 3 to out with a delay between count provided by Sleeper.
func Countdown(out io.Writer, sleeper Sleeper) {
for i := range countDownFrom(3) {
fmt.Fprintln(out, i)
sleeper.Sleep()
}

fmt.Fprint(out, finalWord)
}

func countDownFrom(from int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := from; i > 0; i-- {
if !yield(i) {
return
}
}
}
}

func main() {
sleeper := &ConfigurableSleeper{1 * time.Second, time.Sleep}
Countdown(os.Stdout, sleeper)
}

0 comments on commit dc172e1

Please sign in to comment.