SlideShare a Scribd company logo
Writing Go(od) Tests
Nikki Attea
Software Engineer
Sensu Inc
Hi, I’m Nikki.
@nikkixdev
www.nikki.dev
github.com/sensu/sensu-go
- 🏐🏖
- 🍷🍺
- 🐶🐶
Why Golang?
Why Golang?
● Statically and structurally typed
● Compiled language with large standard library
● Self-contained
● Concurrent and asynchronous
Why Golang?
● Statically and structurally typed
● Compiled language with large standard library
● Self-contained
● Concurrent and asynchronous
Why Golang?
● Statically and structurally typed
● Compiled language with large standard library
● Self-contained
● Concurrent and asynchronous
Why Golang?
● Statically and structurally typed
● Compiled language with large standard library
● Self-contained
● Concurrent and asynchronous
Why Golang?
● Statically and structurally typed
● Compiled language with large standard library
● Self-contained
● Concurrent and asynchronous
Testing implications
● Statically and structurally typed
● Compiled language with large standard library
● Self-contained
● Concurrent and asynchronous
Testing implications
● Statically and structurally typed
● Compiled language with large standard library
● Self-contained
● Concurrent and asynchronous
Testing implications
● Statically and structurally typed
● Compiled language with large standard library
● Self-contained
● Concurrent and asynchronous
Testing implications
● Statically and structurally typed
● Compiled language with large standard library
● Self-contained
● Concurrent and asynchronous
Testing implications
● Statically and structurally typed
● Compiled language with large standard library
● Self-contained
● Concurrent and asynchronous
Writing (good) tests.
Writing Go(od) Tests (FOSDEM 2020)
Writing Go(od) Tests (FOSDEM 2020)
Writing Go(od) Tests (FOSDEM 2020)
Writing Go(od) Tests (FOSDEM 2020)
test-driven development
Running (go) tests.
run all tests in this directory:
go test
run a single test against a fully qualified package name:
go test -v -run TestSubtotal github.com/nikkixdev/beer/api
run all tests with tags, timeout, and race detector:
go test -tags=integration -race -timeout=60s -v -run ./...
run all tests with code coverage:
go test -cover
main.go -> main_test.go
func TestXxx(t *testing.T) {
}
Writing (go) tests.
// Cart represents a shopping cart.
type Cart struct {
Cases []*Case
}
// Case represents a case of beer.
type Case struct {
Count int
Beer *Beer
Price float64
}
// Beer represents a type of beer.
type Beer struct {
Brand string
Name string
Ounces float64
}
// AddCase adds a case of beer to the
shopping cart.
func (c *Cart) AddCase(beerCase *Case) {
c.Cases = append(c.Cases, beerCase)
}
// FixtureBeer creates a Beer fixture for
use in test.
func FixtureBeer(brand string, name string,
ounces float64) *Beer {
return &Beer{
Brand: brand,
Name: name,
Ounces: ounces,
}
}
func TestAddCaseTesting(t *testing.T) {
cart := NewCart()
if len(cart.Cases) != 0 {
t.Fatal("expected empty cart")
}
blueLight := FixtureBeer("Labatt", "Blue Light", 12.0)
cart.AddCase(FixtureCase(6, blueLight, 10.99))
if len(cart.Cases) != 1 {
t.Fatal("expected 1 case in cart")
}
}
testing vs. stretchr/testify
func TestAddCaseAssert(t *testing.T) {
cart := NewCart()
assert.Equal(t, 0, len(cart.Cases), "expected empty cart")
blueLight := FixtureBeer("Labatt", "Blue Light", 12.0)
cart.AddCase(FixtureCase(6, blueLight, 10.99))
assert.Equal(t, 1, len(cart.Cases))
}
test-driven development
func TestSubtotal(t *testing.T) {
cart := NewCart()
assert.Equal(t, 0, len(cart.Cases))
duvelHop := FixtureBeer("Duvel", "Tripel Hop", 11.0)
cart.AddCase(FixtureCase(4, duvelHop, 14.99))
blueLight := FixtureBeer("Labatt", "Blue Light", 12.0)
cart.AddCase(FixtureCase(30, blueLight, 24.99))
assert.Equal(t, 40.0, cart.Subtotal())
}
// Subtotal calculates the subtotal of the shopping cart.
func (c *Cart) Subtotal() float64 {
var subtotal float64
for _, beerCase := range c.Cases {
subtotal += beerCase.Price
}
return subtotal
}
func TestSubtotal(t *testing.T) {
cart := NewCart()
assert.Equal(t, 0, len(cart.Cases))
duvelHop := FixtureBeer("Duvel", "Tripel Hop", 11.0)
cart.AddCase(FixtureCase(4, duvelHop, 14.99))
blueLight := FixtureBeer("Labatt", "Blue Light", 12.0)
cart.AddCase(FixtureCase(30, blueLight, 24.99))
assert.Equal(t, 39.98, cart.Subtotal())
}
func TestSubtotalSuite(t *testing.T) {
testCases := []struct {
name string
cart *Cart
subtotal float64
}{
{
name: "Empty cart",
cart: &Cart{},
subtotal: 0,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.subtotal, tc.cart.Subtotal())
})
}
}
{
name: "Party time",
cart: &Cart{Cases: []*Case{
FixtureCase(4, FixtureBeer("Duvel", "Tripel Hop", 11.0), 14.99),
FixtureCase(30, FixtureBeer("Labatt", "Blue Light", 12.0), 24.99),
FixtureCase(30, FixtureBeer("Labatt", "Blue Light", 12.0), 24.99),
}},
subtotal: 64.97,
},
{
name: "Negative",
cart: &Cart{Cases: []*Case{
FixtureCase(4, FixtureBeer("Duvel", "Tripel Hop", 11.0), -14),
FixtureCase(30, FixtureBeer("Labatt", "Blue Light", 12.0), 24),
}},
subtotal: 10.00,
},
httptest.NewServer(http.Handler)
// ProcessPayment sends the total to an external payment api.
func ProcessPayment(total float64) ([]byte, error) {
b, _ := json.Marshal(total)
resp, err := http.Post("https://www.pay-me.com",
"application/json", bytes.NewBuffer(b))
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
// ProcessPayment sends the total to an external payment api.
func ProcessPayment(paymentServer string, total float64)
([]byte, error) {
b, _ := json.Marshal(total)
resp, err := http.Post(paymentServer, "application/json",
bytes.NewBuffer(b))
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
testCases := []struct {
name string
handler http.HandlerFunc
expectedError error
expectedBody []byte
}{
{
name: "OK",
handler: func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`OK`))
w.WriteHeader(http.StatusOK)
},
expectedError: nil,
expectedBody: []byte(`OK`),
},
{
name: "Internal server error",
handler: func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
},
expectedError: fmt.Errorf("payment server error: %d", http.StatusInternalServerError),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(tc.handler))
defer ts.Close()
body, err := ProcessPayment(ts.URL, 21.11)
assert.Equal(t, tc.expectedError, err)
assert.Equal(t, tc.expectedBody, body)
})
}
// ProcessPayment sends the total to an external payment api.
func ProcessPayment(paymentServer string, total float64)
([]byte, error) {
b, _ := json.Marshal(total)
resp, err := http.Post(paymentServer, "application/json",
bytes.NewBuffer(b))
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
// ProcessPayment sends the total to an external payment api.
func ProcessPayment(paymentServer string, total float64)
([]byte, error) {
b, _ := json.Marshal(total)
resp, err := http.Post(paymentServer, "application/json",
bytes.NewBuffer(b))
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("payment server error: %d",
resp.StatusCode)
}
return ioutil.ReadAll(resp.Body)
}
context.Background() vs. context.TODO()
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
return background
}
// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
return todo
}
// Subscription represents a shopping cart.
type Subscription struct {
Cart *Cart
Interval time.Duration
messageChan chan interface{}
}
// startSubscriptionTimer starts a timer and fires the cart to the
// order handler when the order is ready.
func (s *Subscription) startSubscriptionTimer(ctx context.Context) {
ticker := time.NewTicker(s.Interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
s.messageChan <- s.Cart
}
}
}
func TestStartSubscriptionTimer(t *testing.T) {
cart1 := &Cart{Cases: []*Case{FixtureCase(4, FixtureBeer("Duvel", "Tripel Hop", 11.0), 14)}}
cart2 := &Cart{Cases: []*Case{FixtureCase(30, FixtureBeer("Labatt", "Blue Light", 12.0), 24)}}
subscription := &Subscription{
Cart: cart1,
Interval: time.Duration(1) * time.Second,
messageChan: make(chan interface{}),
}
go subscription.startSubscriptionTimer(context.Background())
msg := <-subscription.messageChan
order, ok := msg.(*Cart)
if !ok {
t.Fatal("received invalid message on message channel")
}
assert.Equal(t, cart1, order)
subscription.Cart = cart2
msg = <-subscription.messageChan
order, ok = msg.(*Cart)
if !ok {
t.Fatal("received invalid message on message channel")
}
assert.Equal(t, cart2, order)
}
// startOrderHandler listens to the message channel and handles incoming orders.
func (o *OrderHandler) startOrderHandler(ctx context.Context) {
for {
msg, ok := <-o.messageChan
if !ok {
logger.Debug("message channel closed")
return
}
cart, ok := msg.(*Cart)
if ok {
if err := o.PlaceOrder(ctx, cart); err != nil {
logger.WithError(err).Error("error placing order")
continue
}
logger.Info("successfully placed order")
continue
}
logger.WithField("msg", msg).Errorf("received invalid message on message channel")
}
}
func TestStartOrderHandler(t *testing.T) {
handler := &OrderHandler{
messageChan: make(chan interface{}),
}
go handler.startOrderHandler(context.Background())
assert.Equal(t, 0, len(handler.ProcessedOrders))
handler.messageChan <- FixtureCart()
handler.messageChan <- FixtureCart()
handler.messageChan <- FixtureCase(30, FixtureBeer("Labatt", "Blue Light",
12.0), 24)
assert.Equal(t, 2, len(handler.ProcessedOrders))
}
Writing (good go) tests.
race conditions & race detection
Writing Go(od) Tests (FOSDEM 2020)
$ go test -run TestStartSubscriptionTimer -race
==================
WARNING: DATA RACE
Read at 0x00c00000e5e0 by goroutine 9:
github.com/nikkixdev/beer.(*Subscription).startSubscriptionTimer()
/Users/nikkixdev/go/src/github.com/nikkixdev/beer/main.go:123 +0xb7
Previous write at 0x00c00000e5e0 by goroutine 8:
github.com/nikkixdev/beer.TestStartSubscriptionTimer()
/Users/nikkixdev/go/src/github.com/nikkixdev/beer/main_test.go:138 +0x582
testing.tRunner()
/usr/local/go/src/testing/testing.go:909 +0x199
Goroutine 9 (running) created at:
github.com/nikkixdev/beer.TestStartSubscriptionTimer()
/Users/nikkixdev/go/src/github.com/nikkixdev/beer/main_test.go:130 +0x4cf
testing.tRunner()
/usr/local/go/src/testing/testing.go:909 +0x199
==================
--- FAIL: TestStartSubscriptionTimer (2.01s)
testing.go:853: race detected during execution of test
FAIL
exit status 1
FAIL github.com/nikkixdev/beer 2.387s
main.go L123
s.messageChan <- s.Cart
main_test.go L138
subscription.Cart = cart2
sync.Mutex
type Subscription struct {
cart *Cart
interval time.Duration
messageChan chan interface{}
mu sync.Mutex
}
// GetCart safely retrieves the subscriptions shopping cart.
func (s *Subscription) GetCart() *Cart {
s.mu.Lock()
defer s.mu.Unlock()
return s.cart
}
// SetCart safely sets the subscriptions shopping cart.
func (s *Subscription) SetCart(c *Cart) {
s.mu.Lock()
defer s.mu.Unlock()
s.cart = c
}
$ go test -run ./... -race
PASS
ok github.com/nikkixdev/beer 3.113s
Thanks for listening!
@nikkixdev
www.nikki.dev
github.com/nikkixdev/beer-testing
Resources
Go docs:
golang.org
Special thanks for the gopher illustrations:
● github.com/ashleymcnamara/gophers
● github.com/marcusolsson/gophers
● github.com/MariaLetta/free-gophers-pack

More Related Content

PDF
Design your client: go way
PDF
Greyhound - Powerful Pure Functional Kafka Library - Scala Love in the City
PDF
Job Queue in Golang
PDF
Writing Docker monitoring agent with Go
PDF
Better Open Source Enterprise C++ Web Services
PPT
Working with Bytecode
ODP
Anyevent
PDF
用 Go 語言打造多台機器 Scale 架構
Design your client: go way
Greyhound - Powerful Pure Functional Kafka Library - Scala Love in the City
Job Queue in Golang
Writing Docker monitoring agent with Go
Better Open Source Enterprise C++ Web Services
Working with Bytecode
Anyevent
用 Go 語言打造多台機器 Scale 架構

What's hot (17)

PDF
Critical errors in CryEngine V code
PDF
Build microservice with gRPC in golang
PDF
Event-sourced architectures with Akka - Sander Mak
PDF
Code as Risk
PDF
InterConnect: Server Side Swift for Java Developers
PPTX
Final project powerpoint template (fndprg) (1)
PDF
IBM Cloud University: Java, Node.js and Swift
PDF
GeeCON 2014 - Functional Programming without Lambdas
PPT
Friend this-new&delete
PDF
Event-sourced architectures with Akka
PDF
Flamingo Training - Hello World
PPTX
Chapter i c#(console application and programming)
PPTX
Avoiding Callback Hell with Async.js
PPTX
Workshop: Async and Parallel in C#
PPTX
Switch case and looping
PDF
Critical errors in CryEngine V code
Build microservice with gRPC in golang
Event-sourced architectures with Akka - Sander Mak
Code as Risk
InterConnect: Server Side Swift for Java Developers
Final project powerpoint template (fndprg) (1)
IBM Cloud University: Java, Node.js and Swift
GeeCON 2014 - Functional Programming without Lambdas
Friend this-new&delete
Event-sourced architectures with Akka
Flamingo Training - Hello World
Chapter i c#(console application and programming)
Avoiding Callback Hell with Async.js
Workshop: Async and Parallel in C#
Switch case and looping
Ad

Similar to Writing Go(od) Tests (FOSDEM 2020) (20)

PDF
Golang dot-testing
PDF
Golang dot-testing-lite
PDF
Geeks Anonymes - Le langage Go
PDF
GoCracow #5 Bartlomiej klimczak - GoBDD
KEY
Beauty and Power of Go
PDF
Go ahead, make my day
PDF
Fun with functions
PDF
Introduction to Go for Java Programmers
PDF
2011 july-nyc-gtug-go
PDF
Testing CLI tools with Go
PDF
Introduction to go
PDF
Using Go to build a REST API: yes, it’s a good match! - Vincent BEHAR & Mina ...
PDF
Something about Golang
PDF
entwickler.de Go Day: Go Web Development 101
PDF
DevOpsCon 2021: Go Web Development 101
PDF
Go Programming Patterns
PDF
Juju - Google Go in a scalable Environment
PDF
How to not write a boring test in Golang
PDF
Golang and Eco-System Introduction / Overview
PDF
Testing the waters of iOS
Golang dot-testing
Golang dot-testing-lite
Geeks Anonymes - Le langage Go
GoCracow #5 Bartlomiej klimczak - GoBDD
Beauty and Power of Go
Go ahead, make my day
Fun with functions
Introduction to Go for Java Programmers
2011 july-nyc-gtug-go
Testing CLI tools with Go
Introduction to go
Using Go to build a REST API: yes, it’s a good match! - Vincent BEHAR & Mina ...
Something about Golang
entwickler.de Go Day: Go Web Development 101
DevOpsCon 2021: Go Web Development 101
Go Programming Patterns
Juju - Google Go in a scalable Environment
How to not write a boring test in Golang
Golang and Eco-System Introduction / Overview
Testing the waters of iOS
Ad

Recently uploaded (20)

PPTX
VVF-Customer-Presentation2025-Ver1.9.pptx
PDF
How to Choose the Right IT Partner for Your Business in Malaysia
PDF
Addressing The Cult of Project Management Tools-Why Disconnected Work is Hold...
PPTX
Odoo POS Development Services by CandidRoot Solutions
PDF
Flood Susceptibility Mapping Using Image-Based 2D-CNN Deep Learnin. Overview ...
PPTX
Oracle E-Business Suite: A Comprehensive Guide for Modern Enterprises
PPTX
Agentic AI Use Case- Contract Lifecycle Management (CLM).pptx
PDF
Softaken Excel to vCard Converter Software.pdf
PPTX
Transform Your Business with a Software ERP System
PPTX
Lecture 3: Operating Systems Introduction to Computer Hardware Systems
PDF
T3DD25 TYPO3 Content Blocks - Deep Dive by André Kraus
PPTX
history of c programming in notes for students .pptx
PDF
How Creative Agencies Leverage Project Management Software.pdf
PDF
How to Migrate SBCGlobal Email to Yahoo Easily
PDF
Internet Downloader Manager (IDM) Crack 6.42 Build 42 Updates Latest 2025
PDF
medical staffing services at VALiNTRY
PDF
Claude Code: Everyone is a 10x Developer - A Comprehensive AI-Powered CLI Tool
PDF
Odoo Companies in India – Driving Business Transformation.pdf
PPTX
L1 - Introduction to python Backend.pptx
PPTX
ai tools demonstartion for schools and inter college
VVF-Customer-Presentation2025-Ver1.9.pptx
How to Choose the Right IT Partner for Your Business in Malaysia
Addressing The Cult of Project Management Tools-Why Disconnected Work is Hold...
Odoo POS Development Services by CandidRoot Solutions
Flood Susceptibility Mapping Using Image-Based 2D-CNN Deep Learnin. Overview ...
Oracle E-Business Suite: A Comprehensive Guide for Modern Enterprises
Agentic AI Use Case- Contract Lifecycle Management (CLM).pptx
Softaken Excel to vCard Converter Software.pdf
Transform Your Business with a Software ERP System
Lecture 3: Operating Systems Introduction to Computer Hardware Systems
T3DD25 TYPO3 Content Blocks - Deep Dive by André Kraus
history of c programming in notes for students .pptx
How Creative Agencies Leverage Project Management Software.pdf
How to Migrate SBCGlobal Email to Yahoo Easily
Internet Downloader Manager (IDM) Crack 6.42 Build 42 Updates Latest 2025
medical staffing services at VALiNTRY
Claude Code: Everyone is a 10x Developer - A Comprehensive AI-Powered CLI Tool
Odoo Companies in India – Driving Business Transformation.pdf
L1 - Introduction to python Backend.pptx
ai tools demonstartion for schools and inter college

Writing Go(od) Tests (FOSDEM 2020)

  • 1. Writing Go(od) Tests Nikki Attea Software Engineer Sensu Inc
  • 4. Why Golang? ● Statically and structurally typed ● Compiled language with large standard library ● Self-contained ● Concurrent and asynchronous
  • 5. Why Golang? ● Statically and structurally typed ● Compiled language with large standard library ● Self-contained ● Concurrent and asynchronous
  • 6. Why Golang? ● Statically and structurally typed ● Compiled language with large standard library ● Self-contained ● Concurrent and asynchronous
  • 7. Why Golang? ● Statically and structurally typed ● Compiled language with large standard library ● Self-contained ● Concurrent and asynchronous
  • 8. Why Golang? ● Statically and structurally typed ● Compiled language with large standard library ● Self-contained ● Concurrent and asynchronous
  • 9. Testing implications ● Statically and structurally typed ● Compiled language with large standard library ● Self-contained ● Concurrent and asynchronous
  • 10. Testing implications ● Statically and structurally typed ● Compiled language with large standard library ● Self-contained ● Concurrent and asynchronous
  • 11. Testing implications ● Statically and structurally typed ● Compiled language with large standard library ● Self-contained ● Concurrent and asynchronous
  • 12. Testing implications ● Statically and structurally typed ● Compiled language with large standard library ● Self-contained ● Concurrent and asynchronous
  • 13. Testing implications ● Statically and structurally typed ● Compiled language with large standard library ● Self-contained ● Concurrent and asynchronous
  • 21. run all tests in this directory: go test run a single test against a fully qualified package name: go test -v -run TestSubtotal github.com/nikkixdev/beer/api run all tests with tags, timeout, and race detector: go test -tags=integration -race -timeout=60s -v -run ./... run all tests with code coverage: go test -cover
  • 22. main.go -> main_test.go func TestXxx(t *testing.T) { }
  • 24. // Cart represents a shopping cart. type Cart struct { Cases []*Case } // Case represents a case of beer. type Case struct { Count int Beer *Beer Price float64 } // Beer represents a type of beer. type Beer struct { Brand string Name string Ounces float64 } // AddCase adds a case of beer to the shopping cart. func (c *Cart) AddCase(beerCase *Case) { c.Cases = append(c.Cases, beerCase) } // FixtureBeer creates a Beer fixture for use in test. func FixtureBeer(brand string, name string, ounces float64) *Beer { return &Beer{ Brand: brand, Name: name, Ounces: ounces, } }
  • 25. func TestAddCaseTesting(t *testing.T) { cart := NewCart() if len(cart.Cases) != 0 { t.Fatal("expected empty cart") } blueLight := FixtureBeer("Labatt", "Blue Light", 12.0) cart.AddCase(FixtureCase(6, blueLight, 10.99)) if len(cart.Cases) != 1 { t.Fatal("expected 1 case in cart") } }
  • 27. func TestAddCaseAssert(t *testing.T) { cart := NewCart() assert.Equal(t, 0, len(cart.Cases), "expected empty cart") blueLight := FixtureBeer("Labatt", "Blue Light", 12.0) cart.AddCase(FixtureCase(6, blueLight, 10.99)) assert.Equal(t, 1, len(cart.Cases)) }
  • 29. func TestSubtotal(t *testing.T) { cart := NewCart() assert.Equal(t, 0, len(cart.Cases)) duvelHop := FixtureBeer("Duvel", "Tripel Hop", 11.0) cart.AddCase(FixtureCase(4, duvelHop, 14.99)) blueLight := FixtureBeer("Labatt", "Blue Light", 12.0) cart.AddCase(FixtureCase(30, blueLight, 24.99)) assert.Equal(t, 40.0, cart.Subtotal()) }
  • 30. // Subtotal calculates the subtotal of the shopping cart. func (c *Cart) Subtotal() float64 { var subtotal float64 for _, beerCase := range c.Cases { subtotal += beerCase.Price } return subtotal }
  • 31. func TestSubtotal(t *testing.T) { cart := NewCart() assert.Equal(t, 0, len(cart.Cases)) duvelHop := FixtureBeer("Duvel", "Tripel Hop", 11.0) cart.AddCase(FixtureCase(4, duvelHop, 14.99)) blueLight := FixtureBeer("Labatt", "Blue Light", 12.0) cart.AddCase(FixtureCase(30, blueLight, 24.99)) assert.Equal(t, 39.98, cart.Subtotal()) }
  • 32. func TestSubtotalSuite(t *testing.T) { testCases := []struct { name string cart *Cart subtotal float64 }{ { name: "Empty cart", cart: &Cart{}, subtotal: 0, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { assert.Equal(t, tc.subtotal, tc.cart.Subtotal()) }) } }
  • 33. { name: "Party time", cart: &Cart{Cases: []*Case{ FixtureCase(4, FixtureBeer("Duvel", "Tripel Hop", 11.0), 14.99), FixtureCase(30, FixtureBeer("Labatt", "Blue Light", 12.0), 24.99), FixtureCase(30, FixtureBeer("Labatt", "Blue Light", 12.0), 24.99), }}, subtotal: 64.97, }, { name: "Negative", cart: &Cart{Cases: []*Case{ FixtureCase(4, FixtureBeer("Duvel", "Tripel Hop", 11.0), -14), FixtureCase(30, FixtureBeer("Labatt", "Blue Light", 12.0), 24), }}, subtotal: 10.00, },
  • 35. // ProcessPayment sends the total to an external payment api. func ProcessPayment(total float64) ([]byte, error) { b, _ := json.Marshal(total) resp, err := http.Post("https://www.pay-me.com", "application/json", bytes.NewBuffer(b)) if err != nil { return nil, err } defer resp.Body.Close() return ioutil.ReadAll(resp.Body) }
  • 36. // ProcessPayment sends the total to an external payment api. func ProcessPayment(paymentServer string, total float64) ([]byte, error) { b, _ := json.Marshal(total) resp, err := http.Post(paymentServer, "application/json", bytes.NewBuffer(b)) if err != nil { return nil, err } defer resp.Body.Close() return ioutil.ReadAll(resp.Body) }
  • 37. testCases := []struct { name string handler http.HandlerFunc expectedError error expectedBody []byte }{ { name: "OK", handler: func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`OK`)) w.WriteHeader(http.StatusOK) }, expectedError: nil, expectedBody: []byte(`OK`), }, { name: "Internal server error", handler: func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) }, expectedError: fmt.Errorf("payment server error: %d", http.StatusInternalServerError), }, }
  • 38. for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(tc.handler)) defer ts.Close() body, err := ProcessPayment(ts.URL, 21.11) assert.Equal(t, tc.expectedError, err) assert.Equal(t, tc.expectedBody, body) }) }
  • 39. // ProcessPayment sends the total to an external payment api. func ProcessPayment(paymentServer string, total float64) ([]byte, error) { b, _ := json.Marshal(total) resp, err := http.Post(paymentServer, "application/json", bytes.NewBuffer(b)) if err != nil { return nil, err } defer resp.Body.Close() return ioutil.ReadAll(resp.Body) }
  • 40. // ProcessPayment sends the total to an external payment api. func ProcessPayment(paymentServer string, total float64) ([]byte, error) { b, _ := json.Marshal(total) resp, err := http.Post(paymentServer, "application/json", bytes.NewBuffer(b)) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode >= 400 { return nil, fmt.Errorf("payment server error: %d", resp.StatusCode) } return ioutil.ReadAll(resp.Body) }
  • 42. var ( background = new(emptyCtx) todo = new(emptyCtx) ) // Background returns a non-nil, empty Context. It is never canceled, has no // values, and has no deadline. It is typically used by the main function, // initialization, and tests, and as the top-level Context for incoming // requests. func Background() Context { return background } // TODO returns a non-nil, empty Context. Code should use context.TODO when // it's unclear which Context to use or it is not yet available (because the // surrounding function has not yet been extended to accept a Context // parameter). func TODO() Context { return todo }
  • 43. // Subscription represents a shopping cart. type Subscription struct { Cart *Cart Interval time.Duration messageChan chan interface{} } // startSubscriptionTimer starts a timer and fires the cart to the // order handler when the order is ready. func (s *Subscription) startSubscriptionTimer(ctx context.Context) { ticker := time.NewTicker(s.Interval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: s.messageChan <- s.Cart } } }
  • 44. func TestStartSubscriptionTimer(t *testing.T) { cart1 := &Cart{Cases: []*Case{FixtureCase(4, FixtureBeer("Duvel", "Tripel Hop", 11.0), 14)}} cart2 := &Cart{Cases: []*Case{FixtureCase(30, FixtureBeer("Labatt", "Blue Light", 12.0), 24)}} subscription := &Subscription{ Cart: cart1, Interval: time.Duration(1) * time.Second, messageChan: make(chan interface{}), } go subscription.startSubscriptionTimer(context.Background()) msg := <-subscription.messageChan order, ok := msg.(*Cart) if !ok { t.Fatal("received invalid message on message channel") } assert.Equal(t, cart1, order) subscription.Cart = cart2 msg = <-subscription.messageChan order, ok = msg.(*Cart) if !ok { t.Fatal("received invalid message on message channel") } assert.Equal(t, cart2, order) }
  • 45. // startOrderHandler listens to the message channel and handles incoming orders. func (o *OrderHandler) startOrderHandler(ctx context.Context) { for { msg, ok := <-o.messageChan if !ok { logger.Debug("message channel closed") return } cart, ok := msg.(*Cart) if ok { if err := o.PlaceOrder(ctx, cart); err != nil { logger.WithError(err).Error("error placing order") continue } logger.Info("successfully placed order") continue } logger.WithField("msg", msg).Errorf("received invalid message on message channel") } }
  • 46. func TestStartOrderHandler(t *testing.T) { handler := &OrderHandler{ messageChan: make(chan interface{}), } go handler.startOrderHandler(context.Background()) assert.Equal(t, 0, len(handler.ProcessedOrders)) handler.messageChan <- FixtureCart() handler.messageChan <- FixtureCart() handler.messageChan <- FixtureCase(30, FixtureBeer("Labatt", "Blue Light", 12.0), 24) assert.Equal(t, 2, len(handler.ProcessedOrders)) }
  • 48. race conditions & race detection
  • 50. $ go test -run TestStartSubscriptionTimer -race ================== WARNING: DATA RACE Read at 0x00c00000e5e0 by goroutine 9: github.com/nikkixdev/beer.(*Subscription).startSubscriptionTimer() /Users/nikkixdev/go/src/github.com/nikkixdev/beer/main.go:123 +0xb7 Previous write at 0x00c00000e5e0 by goroutine 8: github.com/nikkixdev/beer.TestStartSubscriptionTimer() /Users/nikkixdev/go/src/github.com/nikkixdev/beer/main_test.go:138 +0x582 testing.tRunner() /usr/local/go/src/testing/testing.go:909 +0x199 Goroutine 9 (running) created at: github.com/nikkixdev/beer.TestStartSubscriptionTimer() /Users/nikkixdev/go/src/github.com/nikkixdev/beer/main_test.go:130 +0x4cf testing.tRunner() /usr/local/go/src/testing/testing.go:909 +0x199 ================== --- FAIL: TestStartSubscriptionTimer (2.01s) testing.go:853: race detected during execution of test FAIL exit status 1 FAIL github.com/nikkixdev/beer 2.387s
  • 51. main.go L123 s.messageChan <- s.Cart main_test.go L138 subscription.Cart = cart2
  • 53. type Subscription struct { cart *Cart interval time.Duration messageChan chan interface{} mu sync.Mutex } // GetCart safely retrieves the subscriptions shopping cart. func (s *Subscription) GetCart() *Cart { s.mu.Lock() defer s.mu.Unlock() return s.cart } // SetCart safely sets the subscriptions shopping cart. func (s *Subscription) SetCart(c *Cart) { s.mu.Lock() defer s.mu.Unlock() s.cart = c }
  • 54. $ go test -run ./... -race PASS ok github.com/nikkixdev/beer 3.113s
  • 55. Thanks for listening! @nikkixdev www.nikki.dev github.com/nikkixdev/beer-testing Resources Go docs: golang.org Special thanks for the gopher illustrations: ● github.com/ashleymcnamara/gophers ● github.com/marcusolsson/gophers ● github.com/MariaLetta/free-gophers-pack