From d29d93a12dfb23bf6c528fe1bc39782748876c52 Mon Sep 17 00:00:00 2001 From: Nguyen Gia Date: Sun, 16 Sep 2018 20:15:38 +0900 Subject: [PATCH 1/2] Finished implementing --- kadai4/nguyengiabk/.gitignore | 1 + kadai4/nguyengiabk/README.md | 25 ++++++ kadai4/nguyengiabk/main.go | 61 +++++++++++++ kadai4/nguyengiabk/main_test.go | 71 +++++++++++++++ kadai4/nguyengiabk/omikuji/omikuji.go | 92 +++++++++++++++++++ kadai4/nguyengiabk/omikuji/omikuji_test.go | 100 +++++++++++++++++++++ 6 files changed, 350 insertions(+) create mode 100644 kadai4/nguyengiabk/.gitignore create mode 100644 kadai4/nguyengiabk/README.md create mode 100644 kadai4/nguyengiabk/main.go create mode 100644 kadai4/nguyengiabk/main_test.go create mode 100644 kadai4/nguyengiabk/omikuji/omikuji.go create mode 100644 kadai4/nguyengiabk/omikuji/omikuji_test.go diff --git a/kadai4/nguyengiabk/.gitignore b/kadai4/nguyengiabk/.gitignore new file mode 100644 index 0000000..b1174a0 --- /dev/null +++ b/kadai4/nguyengiabk/.gitignore @@ -0,0 +1 @@ +kadai4 diff --git a/kadai4/nguyengiabk/README.md b/kadai4/nguyengiabk/README.md new file mode 100644 index 0000000..16fcc2d --- /dev/null +++ b/kadai4/nguyengiabk/README.md @@ -0,0 +1,25 @@ +# Gopher Dojo 3 - Kadai4 +## Problem + +おみくじAPIを作ってみよう +* [x] JSON形式でおみくじの結果を返す +* [x] 正月(1/1-1/3)だけ大吉にする +* [x] ハンドラのテストを書いてみる + +## Build +``` +$ go build -o kadai4 . +``` + +## Usage + +Start server +``` +$ ./kadai4 +``` + +Client send request +``` +$ curl localhost:8080 +{"result":"凶"} +``` diff --git a/kadai4/nguyengiabk/main.go b/kadai4/nguyengiabk/main.go new file mode 100644 index 0000000..7d4bd0e --- /dev/null +++ b/kadai4/nguyengiabk/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "encoding/json" + "io" + "net/http" + + "github.com/gopherdojo/dojo3/kadai4/nguyengiabk/omikuji" +) + +// Result represents api response structure +type Result struct { + Result omikuji.Fortune `json:"result"` +} + +// JSONEncoder is used to encode JSON +type JSONEncoder interface { + Encode(interface{}, io.Writer) error +} + +// JSONEncodeFunc is function that used to encode JSON +type JSONEncodeFunc func(interface{}, io.Writer) error + +// Encode encode JSON and write result to writer +func (f JSONEncodeFunc) Encode(data interface{}, w io.Writer) error { + return f(data, w) +} + +// OmikujiServer handles request and response as JSON +type OmikujiServer struct { + jsonEncoder JSONEncoder + omikuji omikuji.Omikuji +} + +func (os *OmikujiServer) encode(data interface{}, w io.Writer) error { + if os.jsonEncoder != nil { + return os.jsonEncoder.Encode(data, w) + } + encoder := json.NewEncoder(w) + if err := encoder.Encode(data); err != nil { + return err + } + return nil +} + +// ServerErrorMessage is message that will be returned to user in case of error +const serverErrorMessage = "Server error" + +func (os *OmikujiServer) handler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + res := Result{os.omikuji.GetResult()} + if err := os.encode(res, w); err != nil { + http.Error(w, serverErrorMessage, http.StatusInternalServerError) + } +} + +func main() { + os := OmikujiServer{omikuji: omikuji.Omikuji{}} + http.HandleFunc("/", os.handler) + http.ListenAndServe(":8080", nil) +} diff --git a/kadai4/nguyengiabk/main_test.go b/kadai4/nguyengiabk/main_test.go new file mode 100644 index 0000000..51fc6e3 --- /dev/null +++ b/kadai4/nguyengiabk/main_test.go @@ -0,0 +1,71 @@ +package main + +import ( + "errors" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gopherdojo/dojo3/kadai4/nguyengiabk/omikuji" +) + +type testCase struct { + os OmikujiServer + statusCode int + response string +} + +var TestHandlerFixtures = map[string]testCase{ + "Test json encode error": { + OmikujiServer{ + JSONEncodeFunc(func(data interface{}, w io.Writer) error { + return errors.New("Encode error") + }), + omikuji.Omikuji{}, + }, + http.StatusInternalServerError, + "Server error\n", + }, + "Test normal case": { + OmikujiServer{ + // want to fixed response + omikuji: omikuji.Omikuji{Randomize: omikuji.RandomizeFunc(func(max int) int { + return 0 + })}, + }, + http.StatusOK, + "{\"result\":\"大吉\"}\n", + }, +} + +func TestHandler(t *testing.T) { + for name, tc := range TestHandlerFixtures { + tc := tc + t.Run(name, func(t *testing.T) { + RunTestCase(t, tc) + }) + } +} + +// split to use defer +func RunTestCase(t *testing.T, tc testCase) { + t.Helper() + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + tc.os.handler(w, r) + rw := w.Result() + defer rw.Body.Close() + + if rw.StatusCode != tc.statusCode { + t.Errorf("unexpected status code, actual = %v, expected = %v", rw.StatusCode, tc.statusCode) + } + b, err := ioutil.ReadAll(rw.Body) + if err != nil { + t.Errorf("unexpected error when read response body") + } + if s := string(b); s != tc.response { + t.Fatalf("unexpected response: actual = %s, expected = %s", s, tc.response) + } +} diff --git a/kadai4/nguyengiabk/omikuji/omikuji.go b/kadai4/nguyengiabk/omikuji/omikuji.go new file mode 100644 index 0000000..ee04e45 --- /dev/null +++ b/kadai4/nguyengiabk/omikuji/omikuji.go @@ -0,0 +1,92 @@ +package omikuji + +import ( + "math/rand" + "time" +) + +// Fortune represents omikuji result +type Fortune string + +const ( + // Daikichi represents 大吉 result + Daikichi Fortune = "大吉" + + // Chukichi represents 中吉 result + Chukichi Fortune = "中吉" + + // Shokichi represents 小吉 result + Shokichi Fortune = "小吉" + + // Kichi represents 吉 result + Kichi Fortune = "吉" + + // Kyou represents 凶 result + Kyou Fortune = "凶" + + // Shokyou represents 小凶 result + Shokyou Fortune = "小凶" + + // Daikyou represents 大凶 result + Daikyou Fortune = "大凶" +) + +var omikujiValues = []Fortune{Daikichi, Chukichi, Shokichi, Kichi, Kyou, Shokyou, Daikyou} + +// Clock defines types that can return current time +type Clock interface { + Now() time.Time +} + +// ClockFunc returns current time, we use this type for testing +type ClockFunc func() time.Time + +// Now returns current time by calling CockFunc itself +func (f ClockFunc) Now() time.Time { + return f() +} + +// Randomize defines types that can return ramdom integer +type Randomize interface { + Rand(max int) int +} + +// RandomizeFunc returns randome integer, we use this type for testing +type RandomizeFunc func(max int) int + +// Rand returns random integer by calling RandomizeFunc itself +func (f RandomizeFunc) Rand(max int) int { + return f(max) +} + +// Omikuji is used to get omikuji result based on current time +type Omikuji struct { + Clock Clock + Randomize Randomize +} + +func (o *Omikuji) now() time.Time { + if o.Clock == nil { + return time.Now() + } + return o.Clock.Now() +} + +func (o *Omikuji) rand(max int) int { + if o.Randomize == nil { + rand.Seed(time.Now().UnixNano()) + return rand.Intn(max) + } + return o.Randomize.Rand(max) +} + +// GetResult return randomly selected omikuji value +func (o *Omikuji) GetResult() Fortune { + _, m, d := o.now().Date() + switch { + case m == time.January && d <= 3: + return Daikichi + default: + return omikujiValues[o.rand(len(omikujiValues))] + } +} diff --git a/kadai4/nguyengiabk/omikuji/omikuji_test.go b/kadai4/nguyengiabk/omikuji/omikuji_test.go new file mode 100644 index 0000000..032e04f --- /dev/null +++ b/kadai4/nguyengiabk/omikuji/omikuji_test.go @@ -0,0 +1,100 @@ +package omikuji_test + +import ( + "fmt" + "testing" + "time" + + "github.com/gopherdojo/dojo3/kadai4/nguyengiabk/omikuji" +) + +func Example() { + o := omikuji.Omikuji{} + fmt.Println(o.GetResult()) +} + +var testGetResultFixtures = map[string]struct { + clock omikuji.Clock + randomize omikuji.Randomize + result omikuji.Fortune +}{ + "Test 1/1": { + omikuji.ClockFunc(func() time.Time { + return time.Date(2018, 1, 1, 0, 0, 0, 0, time.Local) + }), + nil, + omikuji.Daikichi, + }, + "Test 1/2": { + omikuji.ClockFunc(func() time.Time { + return time.Date(2018, 1, 2, 0, 0, 0, 0, time.Local) + }), + nil, + omikuji.Daikichi, + }, + "Test 1/3": { + omikuji.ClockFunc(func() time.Time { + return time.Date(2018, 1, 3, 0, 0, 0, 0, time.Local) + }), + nil, + omikuji.Daikichi, + }, + "Test 1/4 and Kyou": { + omikuji.ClockFunc(func() time.Time { + return time.Date(2018, 1, 4, 0, 0, 0, 0, time.Local) + }), + omikuji.RandomizeFunc(func(max int) int { + return 4 + }), + omikuji.Kyou, + }, + "Test Daikichi not new year": { + omikuji.ClockFunc(func() time.Time { + return time.Date(2018, 7, 1, 0, 0, 0, 0, time.Local) + }), + omikuji.RandomizeFunc(func(max int) int { + return 0 + }), + omikuji.Daikichi, + }, + "Test Chukichi": { + nil, + omikuji.RandomizeFunc(func(max int) int { + return 1 + }), + omikuji.Chukichi, + }, + "Test Shokichi": { + nil, + omikuji.RandomizeFunc(func(max int) int { + return 2 + }), + omikuji.Shokichi, + }, + "Test Shokyou": { + nil, + omikuji.RandomizeFunc(func(max int) int { + return 5 + }), + omikuji.Shokyou, + }, + "Test Daikyou": { + nil, + omikuji.RandomizeFunc(func(max int) int { + return 6 + }), + omikuji.Daikyou, + }, +} + +func TestGetResult(t *testing.T) { + for name, tc := range testGetResultFixtures { + t.Run(name, func(t *testing.T) { + o := omikuji.Omikuji{Clock: tc.clock, Randomize: tc.randomize} + result := o.GetResult() + if result != tc.result { + t.Errorf("GetResult() return wrong result, actual = %v, expected = %v", result, tc.result) + } + }) + } +} From a8656fe62d1f1e4d12a97d939ecfa5780bfdd6ab Mon Sep 17 00:00:00 2001 From: Nguyen Gia Date: Mon, 17 Sep 2018 01:15:54 +0900 Subject: [PATCH 2/2] Refactored test --- kadai4/nguyengiabk/omikuji/omikuji_test.go | 112 ++++++++------------- 1 file changed, 42 insertions(+), 70 deletions(-) diff --git a/kadai4/nguyengiabk/omikuji/omikuji_test.go b/kadai4/nguyengiabk/omikuji/omikuji_test.go index 032e04f..b4c1f88 100644 --- a/kadai4/nguyengiabk/omikuji/omikuji_test.go +++ b/kadai4/nguyengiabk/omikuji/omikuji_test.go @@ -13,84 +13,37 @@ func Example() { fmt.Println(o.GetResult()) } +type randomize struct { + useMock bool + randResult int +} + var testGetResultFixtures = map[string]struct { - clock omikuji.Clock - randomize omikuji.Randomize + date string + randomize randomize result omikuji.Fortune }{ - "Test 1/1": { - omikuji.ClockFunc(func() time.Time { - return time.Date(2018, 1, 1, 0, 0, 0, 0, time.Local) - }), - nil, - omikuji.Daikichi, - }, - "Test 1/2": { - omikuji.ClockFunc(func() time.Time { - return time.Date(2018, 1, 2, 0, 0, 0, 0, time.Local) - }), - nil, - omikuji.Daikichi, - }, - "Test 1/3": { - omikuji.ClockFunc(func() time.Time { - return time.Date(2018, 1, 3, 0, 0, 0, 0, time.Local) - }), - nil, - omikuji.Daikichi, - }, - "Test 1/4 and Kyou": { - omikuji.ClockFunc(func() time.Time { - return time.Date(2018, 1, 4, 0, 0, 0, 0, time.Local) - }), - omikuji.RandomizeFunc(func(max int) int { - return 4 - }), - omikuji.Kyou, - }, - "Test Daikichi not new year": { - omikuji.ClockFunc(func() time.Time { - return time.Date(2018, 7, 1, 0, 0, 0, 0, time.Local) - }), - omikuji.RandomizeFunc(func(max int) int { - return 0 - }), - omikuji.Daikichi, - }, - "Test Chukichi": { - nil, - omikuji.RandomizeFunc(func(max int) int { - return 1 - }), - omikuji.Chukichi, - }, - "Test Shokichi": { - nil, - omikuji.RandomizeFunc(func(max int) int { - return 2 - }), - omikuji.Shokichi, - }, - "Test Shokyou": { - nil, - omikuji.RandomizeFunc(func(max int) int { - return 5 - }), - omikuji.Shokyou, - }, - "Test Daikyou": { - nil, - omikuji.RandomizeFunc(func(max int) int { - return 6 - }), - omikuji.Daikyou, - }, + "Test 1/1": {"2018/01/01", randomize{false, 0}, omikuji.Daikichi}, + "Test 1/2": {"2018/01/02", randomize{false, 0}, omikuji.Daikichi}, + "Test 1/3": {"2018/01/03", randomize{false, 0}, omikuji.Daikichi}, + "Test 1/4 and Kyou": {"2018/01/04", randomize{true, 4}, omikuji.Kyou}, + "Test Daikichi not new year": {"2018/07/01", randomize{true, 0}, omikuji.Daikichi}, + "Test Chukichi": {"2018/08/02", randomize{true, 1}, omikuji.Chukichi}, + "Test Shokichi": {"2018/09/10", randomize{true, 2}, omikuji.Shokichi}, + "Test Shokyou": {"2018/10/11", randomize{true, 5}, omikuji.Shokyou}, + "Test Daikyou": {"2018/03/21", randomize{true, 6}, omikuji.Daikyou}, } func TestGetResult(t *testing.T) { for name, tc := range testGetResultFixtures { + tc := tc t.Run(name, func(t *testing.T) { - o := omikuji.Omikuji{Clock: tc.clock, Randomize: tc.randomize} + clock := mockClock(t, tc.date) + var randomize omikuji.Randomize + if tc.randomize.useMock { + randomize = mockRandomize(t, tc.randomize.randResult) + } + o := omikuji.Omikuji{Clock: clock, Randomize: randomize} result := o.GetResult() if result != tc.result { t.Errorf("GetResult() return wrong result, actual = %v, expected = %v", result, tc.result) @@ -98,3 +51,22 @@ func TestGetResult(t *testing.T) { }) } } + +func mockClock(t *testing.T, v string) omikuji.Clock { + t.Helper() + now, err := time.Parse("2006/01/02", v) + if err != nil { + t.Fatal("unexpected error:", err) + } + + return omikuji.ClockFunc(func() time.Time { + return now + }) +} + +func mockRandomize(t *testing.T, v int) omikuji.Randomize { + t.Helper() + return omikuji.RandomizeFunc(func(max int) int { + return v + }) +}