diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 4aa7d150e..55d6fcc16 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -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 diff --git a/go.mod b/go.mod index 7f237fecd..8cfdbcb51 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/intro-to-acceptance-tests.md b/intro-to-acceptance-tests.md index 19b5c1863..90b98c95e 100644 --- a/intro-to-acceptance-tests.md +++ b/intro-to-acceptance-tests.md @@ -298,7 +298,7 @@ import ( const ( port = "8080" - url = " [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. \ No newline at end of file diff --git a/mocking/v6/countdown_test.go b/mocking/v6/countdown_test.go new file mode 100644 index 000000000..8c1e74926 --- /dev/null +++ b/mocking/v6/countdown_test.go @@ -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 +} diff --git a/mocking/v6/main.go b/mocking/v6/main.go new file mode 100644 index 000000000..ab58444de --- /dev/null +++ b/mocking/v6/main.go @@ -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) +}