From b7d9ed303b1fcccac0e1be7303429a0630713fe4 Mon Sep 17 00:00:00 2001 From: Christian Haas Date: Sun, 6 Oct 2019 19:57:02 +0200 Subject: [PATCH] added Go variant - using homegrown approval test function --- go/README.md | 22 ++++++ go/go.mod | 5 ++ go/go.sum | 10 +++ go/theatre/StatementPrinter.go | 53 +++++++++++++ go/theatre/StatementPrinter_test.go | 74 ++++++++++++++++++ go/theatre/approval_test.go | 78 +++++++++++++++++++ go/theatre/testdata/.gitignore | 1 + .../TestPrinterPrintByApproval/1.approved.txt | 6 ++ .../TestPrinterPrintByApproval/1.in.json | 15 ++++ go/theatre/types.go | 16 ++++ 10 files changed, 280 insertions(+) create mode 100644 go/README.md create mode 100644 go/go.mod create mode 100644 go/go.sum create mode 100644 go/theatre/StatementPrinter.go create mode 100644 go/theatre/StatementPrinter_test.go create mode 100644 go/theatre/approval_test.go create mode 100644 go/theatre/testdata/.gitignore create mode 100644 go/theatre/testdata/TestPrinterPrintByApproval/1.approved.txt create mode 100644 go/theatre/testdata/TestPrinterPrintByApproval/1.in.json create mode 100644 go/theatre/types.go diff --git a/go/README.md b/go/README.md new file mode 100644 index 0000000..5d7acaa --- /dev/null +++ b/go/README.md @@ -0,0 +1,22 @@ +## Theatrical-Players-Refactoring-Kata (Go) + +This variant provides the kata in [Go](www.golang.org). + +### Installation + +* Install Go 1.12 (or later) +* In the directory of this file, run `go mod download` +* You can then run tests with `go test ./...` + +### Approval Tests + +Approval tests are run with a basic home-grown approval function (see `approval_test.go`). + +In short, for a new test case of `TestPrinterPrintByApproval`: +* Add a new `.in.json` file under `testdata/TestPrinterPrintByApproval`. You can copy an existing one for starters. +* Run `go test ./...` - it should fail and complain about a missing file. +* However, a `.out.txt` file will have been created - use this one as the new `.approved.txt` file. + +#### Approval-testing errors + +Right now, the error is tested in-line. You can convert this test to an approval-test as well. diff --git a/go/go.mod b/go/go.mod new file mode 100644 index 0000000..da7f685 --- /dev/null +++ b/go/go.mod @@ -0,0 +1,5 @@ +module github.com/emilybache/Theatrical-Players-Refactoring-Kata/go + +go 1.13 + +require github.com/leekchan/accounting v0.0.0-20190702062627-a09595581342 diff --git a/go/go.sum b/go/go.sum new file mode 100644 index 0000000..cac802c --- /dev/null +++ b/go/go.sum @@ -0,0 +1,10 @@ +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/leekchan/accounting v0.0.0-20190702062627-a09595581342 h1:vzuUv5azbeM31DiEL386i5eQESs5FNN2gDfkPowzNkk= +github.com/leekchan/accounting v0.0.0-20190702062627-a09595581342/go.mod h1:VfQkU+lPM8fjkGqiTn6R+4MX4A/pVwE1XGdDBqp0JFk= +github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= diff --git a/go/theatre/StatementPrinter.go b/go/theatre/StatementPrinter.go new file mode 100644 index 0000000..716ac58 --- /dev/null +++ b/go/theatre/StatementPrinter.go @@ -0,0 +1,53 @@ +package theatre + +import ( + "fmt" + "math" + + "github.com/leekchan/accounting" +) + +type StatementPrinter struct{} + +func (StatementPrinter) Print(invoice Invoice, plays map[string]Play) (string, error) { + totalAmount := 0 + volumeCredits := 0 + result := fmt.Sprintf("Statement for %s\n", invoice.Customer) + + ac := accounting.Accounting{Symbol: "$", Precision: 2} + + for _, perf := range invoice.Performances { + play := plays[perf.PlayID] + thisAmount := 0 + + switch play.Type { + case "tragedy": + thisAmount = 40000 + if perf.Audience > 30 { + thisAmount += 1000 * (perf.Audience - 30) + } + case "comedy": + thisAmount = 30000 + if perf.Audience > 20 { + thisAmount += 10000 + 500*(perf.Audience-20) + } + thisAmount += 300 * perf.Audience + default: + return "", fmt.Errorf("unknown type: %s", play.Type) + } + + // add volume credits + volumeCredits += int(math.Max(float64(perf.Audience)-30, 0)) + // add extra credit for every ten comedy attendees + if play.Type == "comedy" { + volumeCredits += int(math.Floor(float64(perf.Audience) / 5)) + } + + // print line for this order + result += fmt.Sprintf(" %s: %s (%d seats)\n", play.Name, ac.FormatMoney(float64(thisAmount)/100), perf.Audience) + totalAmount += thisAmount + } + result += fmt.Sprintf("Amount owed is %s\n", ac.FormatMoney(float64(totalAmount)/100)) + result += fmt.Sprintf("You earned %d credits\n", volumeCredits) + return result, nil +} diff --git a/go/theatre/StatementPrinter_test.go b/go/theatre/StatementPrinter_test.go new file mode 100644 index 0000000..151a771 --- /dev/null +++ b/go/theatre/StatementPrinter_test.go @@ -0,0 +1,74 @@ +package theatre_test + +import ( + "encoding/json" + "testing" + + "github.com/emilybache/Theatrical-Players-Refactoring-Kata/go/theatre" +) + +func TestPrinterPrintByApproval(t *testing.T) { + verify(t, "json", "txt", func(t testing.TB, data []byte) []byte { + var in struct { + Plays []struct { + ID string + Play struct { + Name string + Type string + } + } + Invoice struct { + Customer string + Performances []struct { + PlayID string + Audience int + } + } + } + + if err := json.Unmarshal(data, &in); err != nil { + t.Fatalf("failed to unmarshal input data: %v", err) + return nil + } + + // copy test-structure to production structure. Making use of matching types. + plays := make(map[string]theatre.Play) + invoice := theatre.Invoice{ + Customer: in.Invoice.Customer, + Performances: make([]theatre.Performance, 0, len(in.Invoice.Performances)), + } + for _, perf := range in.Invoice.Performances { + invoice.Performances = append(invoice.Performances, perf) + } + for _, identifiedPlay := range in.Plays { + plays[identifiedPlay.ID] = identifiedPlay.Play + } + + var printer theatre.StatementPrinter + statement, err := printer.Print(invoice, plays) + if err != nil { + t.Fatalf("failed to create statement, unexpected error: %v", err) + } + return []byte(statement) + }) +} + +func TestStatementWithNewPlayTypes(t *testing.T) { + plays := map[string]theatre.Play{ + "henry-v": {Name: "Henry V", Type: "history"}, + "as-like": {Name: "As You Like It", Type: "pastoral"}, + } + invoice := theatre.Invoice{ + Customer: "BigCo", + Performances: []theatre.Performance{ + {PlayID: "henry-v", Audience: 53}, + {PlayID: "as-like", Audience: 55}, + }, + } + + var printer theatre.StatementPrinter + _, err := printer.Print(invoice, plays) + if err == nil { + t.Errorf("Expected an error, got none") + } +} diff --git a/go/theatre/approval_test.go b/go/theatre/approval_test.go new file mode 100644 index 0000000..9ae832f --- /dev/null +++ b/go/theatre/approval_test.go @@ -0,0 +1,78 @@ +package theatre_test + +import ( + "bytes" + "io/ioutil" + "path" + "strings" + "testing" +) + +// verify runs a list of sub-tests using input data from the "testdata" subdirectory named after the current test. +// All files with the suffix ".in." are used as input for the test case function. +// The name before the suffix is used as the base name for the files this function uses. +// +// What the function returns will be compared to the file with suffix ".approved.". +// If its content does not match the result, or the file does not exist, the test fails and a file with suffix ".out." is created. +// +// In case the function encounters an error, it can call Fatalf() on the passed test instance. +// To verify errors as approved data, be sure to include the error in your structured data. +// +// This function reports problems of file access, as well as a failure if no input files were found. +func verify(t *testing.T, inType string, outType string, testCase func(testing.TB, []byte) []byte) { + t.Helper() + datadir := path.Join("testdata", t.Name()) + files, err := ioutil.ReadDir(datadir) + if err != nil { + t.Fatalf("could not read test directory: %v", err) + } + fullInSuffix := ".in." + inType + fullApprovedSuffix := ".approved." + outType + inputData := make(map[string][]byte) + approvedData := make(map[string][]byte) + for _, file := range files { + switch { + case file.IsDir(): + case strings.HasSuffix(file.Name(), fullInSuffix): + data, err := ioutil.ReadFile(path.Join(datadir, file.Name())) + if err != nil { + t.Errorf("failed to read input file %s: %v", file.Name(), err) + } + inputData[file.Name()] = data + case strings.HasSuffix(file.Name(), fullApprovedSuffix): + data, err := ioutil.ReadFile(path.Join(datadir, file.Name())) + if err != nil { + t.Errorf("failed to read approved file %s: %v", file.Name(), err) + } + approvedData[file.Name()] = data + } + } + if len(inputData) == 0 { + t.Fatalf("no input files in %s - is this the intent?", datadir) + } + for rangedInFilename, rangedInData := range inputData { + inFilename := rangedInFilename + inData := rangedInData + t.Run(t.Name()+"/"+inFilename, func(t *testing.T) { + result := testCase(t, inData) + + baseFilename := inFilename[:len(inFilename)-len(fullInSuffix)] + expected, hasApproved := approvedData[baseFilename+fullApprovedSuffix] + writeOutFile := false + switch { + case !hasApproved: + t.Errorf("no approved data available (file %s not available)", baseFilename+fullApprovedSuffix) + writeOutFile = true + case !bytes.Equal(expected, result): + t.Errorf("result does not match expectation") + writeOutFile = true + } + if writeOutFile { + err = ioutil.WriteFile(path.Join(datadir, baseFilename+".out."+outType), result, 0644) + if err != nil { + t.Errorf("failed to write output data: %v", err) + } + } + }) + } +} diff --git a/go/theatre/testdata/.gitignore b/go/theatre/testdata/.gitignore new file mode 100644 index 0000000..f6ce4f7 --- /dev/null +++ b/go/theatre/testdata/.gitignore @@ -0,0 +1 @@ +*.out.* diff --git a/go/theatre/testdata/TestPrinterPrintByApproval/1.approved.txt b/go/theatre/testdata/TestPrinterPrintByApproval/1.approved.txt new file mode 100644 index 0000000..fa59f94 --- /dev/null +++ b/go/theatre/testdata/TestPrinterPrintByApproval/1.approved.txt @@ -0,0 +1,6 @@ +Statement for BigCo + Hamlet: $650.00 (55 seats) + As You Like It: $580.00 (35 seats) + Othello: $500.00 (40 seats) +Amount owed is $1,730.00 +You earned 47 credits diff --git a/go/theatre/testdata/TestPrinterPrintByApproval/1.in.json b/go/theatre/testdata/TestPrinterPrintByApproval/1.in.json new file mode 100644 index 0000000..4d42993 --- /dev/null +++ b/go/theatre/testdata/TestPrinterPrintByApproval/1.in.json @@ -0,0 +1,15 @@ +{ + "plays": [ + { "id": "hamlet", "play": { "name": "Hamlet", "type": "tragedy" } }, + { "id": "as-like", "play": { "name": "As You Like It", "type": "comedy" } }, + { "id": "othello", "play": { "name": "Othello", "type": "tragedy" } } + ], + "invoice": { + "customer": "BigCo", + "performances": [ + {"playId": "hamlet", "audience": 55 }, + {"playId": "as-like", "audience": 35 }, + {"playId": "othello", "audience": 40 } + ] + } +} diff --git a/go/theatre/types.go b/go/theatre/types.go new file mode 100644 index 0000000..f0131b1 --- /dev/null +++ b/go/theatre/types.go @@ -0,0 +1,16 @@ +package theatre + +type Performance struct { + PlayID string + Audience int +} + +type Play struct { + Name string + Type string +} + +type Invoice struct { + Customer string + Performances []Performance +}