Skip to content

Commit

Permalink
Initial commit with examples and tests
Browse files Browse the repository at this point in the history
Signed-off-by: Nikki Attea <[email protected]>
  • Loading branch information
Nikki Attea committed Feb 1, 2020
0 parents commit 651a0f6
Show file tree
Hide file tree
Showing 4 changed files with 380 additions and 0 deletions.
10 changes: 10 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module github.com/nikkixdev/beer-testing

go 1.13

require (
github.com/kr/pretty v0.1.0 // indirect
github.com/sirupsen/logrus v1.4.2
github.com/stretchr/testify v1.4.0
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
)
29 changes: 29 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
183 changes: 183 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package main

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"sync"
"time"

"github.com/sirupsen/logrus"
)

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

// NewCart initializes a new shopping cart.
func NewCart() *Cart {
return &Cart{}
}

// AddCase adds a case of beer to the shopping cart.
func (c *Cart) AddCase(beerCase *Case) {
c.Cases = append(c.Cases, beerCase)
}

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

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

// PlaceOrder places the order in the warehouse.
func (o *OrderHandler) PlaceOrder(ctx context.Context, cart *Cart) error {
o.ProcessedOrders = append(o.ProcessedOrders, cart)
return nil
}

// OrderHandler represents a concurrent order handler.
type OrderHandler struct {
ProcessedOrders []*Cart
messageChan chan interface{}
}

var logger = logrus.WithFields(logrus.Fields{
"component": "beer",
})

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

// Subscription represents a shopping cart.
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
}

// GetInterval safely retrieves the subscriptions interval.
func (s *Subscription) GetInterval() time.Duration {
s.mu.Lock()
defer s.mu.Unlock()
return s.interval
}

// SetInterval safely sets the subscriptions interval.
func (s *Subscription) SetInterval(t time.Duration) {
s.mu.Lock()
defer s.mu.Unlock()
s.interval = t
}

// 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.GetInterval())
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
s.messageChan <- s.GetCart()
}
}
}

// 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,
}
}

// FixtureCase creates a Case fixture for use in test.
func FixtureCase(count int, beer *Beer, price float64) *Case {
return &Case{
Count: count,
Beer: beer,
Price: price,
}
}

// FixtureCart creates a Cart fixture for use in test.
func FixtureCart() *Cart {
return &Cart{
Cases: []*Case{FixtureCase(4, FixtureBeer("Duvel", "Tripel Hop", 11.0), 14)},
}
}

func main() {}
158 changes: 158 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package main

import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

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

func TestAddCaseAssert(t *testing.T) {
cart := NewCart()
assert.Equal(t, 0, len(cart.Cases))

blueLight := FixtureBeer("Labatt", "Blue Light", 12.0)
cart.AddCase(FixtureCase(6, blueLight, 10.99))
assert.Equal(t, 1, len(cart.Cases))
}

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,
},
{
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,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.subtotal, tc.cart.Subtotal())
})
}
}

func TestProcessPayment(t *testing.T) {
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)
})
}
}

func TestStartSubscriptionTimer(t *testing.T) {
ctx := context.Background()
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(ctx)
msg := <-subscription.messageChan
order, ok := msg.(*Cart)
if !ok {
t.Fatal("received invalid message on message channel")
}
assert.Equal(t, cart1, order)

subscription.SetCart(cart2)
msg = <-subscription.messageChan
order, ok = msg.(*Cart)
if !ok {
t.Fatal("received invalid message on message channel")
}
assert.Equal(t, cart2, order)
}

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

0 comments on commit 651a0f6

Please sign in to comment.