Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kadai4 shimastripe #72

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions kadai4/shimastripe/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# おみくじAPIを作ってみよう

## 仕様

- JSON形式でおみくじ􏰁結果を返す
- 正月(1/1-1/3)だけ大吉にする
- ハンドラ􏰁テストを書いてみる

## 実行方法

```bash
$ go run cmd/kadai4/main.go
$ curl 'http://localhost:8080'

# => {"fortune":"大吉"}
```

## Test

```bash
$ go test -v

=== RUN TestHandlerRandomly
--- PASS: TestHandlerRandomly (0.00s)
=== RUN TestHandlerWhenNewYear
=== RUN TestHandlerWhenNewYear/NewYearCase
=== RUN TestHandlerWhenNewYear/NewYearCase#01
=== RUN TestHandlerWhenNewYear/NewYearCase#02
--- PASS: TestHandlerWhenNewYear (0.00s)
--- PASS: TestHandlerWhenNewYear/NewYearCase (0.00s)
--- PASS: TestHandlerWhenNewYear/NewYearCase#01 (0.00s)
--- PASS: TestHandlerWhenNewYear/NewYearCase#02 (0.00s)
PASS
ok github.com/gopherdojo/dojo3/kadai4/shimastripe 0.018s
```

## Detail

- 時間は講義で受けたClock interfaceを用意して抽象化
- ハンドラ側のテストを用意
- ランダムで決定する要素と決定しない要素を分けて書いた。ランダム要素側は不安定なので実質フレークテスト
53 changes: 53 additions & 0 deletions kadai4/shimastripe/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package shimastripe

import (
"encoding/json"
"log"
"math/rand"
"net/http"
)

const success = iota

type CLI struct {
Clock Clock
}

// Generate a seed only once
func (c *CLI) generateSeed() {
rand.Seed(c.Clock.Now().Unix())
}

func (c *CLI) IsNewYear() bool {
_, m, d := c.Clock.Now().Date()
if m == 1 && d <= 3 {
return true
}
return false
}

// handler
func (c *CLI) handler(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
var respBody *FortuneRepository

if c.IsNewYear() {
respBody = &FortuneRepository{Fortune: daikichi}
} else {
respBody = &FortuneRepository{Fortune: DrawRandomly()}
}

if err := json.NewEncoder(w).Encode(respBody); err != nil {
log.Printf("Encode error: %v\n", err)
http.Error(w, "{\"result\":\"Internal server error\"}\n", http.StatusInternalServerError)
}
}

// Run a server
func (c *CLI) Run(args []string) int {
c.generateSeed()
http.HandleFunc("/", c.handler)
http.ListenAndServe(":8080", nil)
return success
}
112 changes: 112 additions & 0 deletions kadai4/shimastripe/cli_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package shimastripe

import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
)

var fortuneList []string

func TestMain(m *testing.M) {
setup()
ret := m.Run()
os.Exit(ret)
}

func setup() {
for index := 0; index < int(threshold); index++ {
fortuneList = append(fortuneList, FortuneElement(index).String())
}
}

// Test handler. This test is flaky. (choose fortune element randomly)
func TestHandlerRandomly(t *testing.T) {
cli := &CLI{Clock: ClockFunc(func() time.Time {
return time.Now()
})}
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", nil)
cli.handler(w, r)
rw := w.Result()
defer rw.Body.Close()

if rw.StatusCode != http.StatusOK {
t.Error("unexpected status code\n")
}

m := struct {
Fortune string `json:"fortune"`
}{}

if err := json.NewDecoder(rw.Body).Decode(&m); err != nil {
t.Error("Decode error\n")
}

if !contain(fortuneList, m.Fortune) {
t.Errorf("unexpected fortune element: %v", m.Fortune)
}
}

func TestHandlerWhenNewYear(t *testing.T) {
cases := []struct {
clock Clock
answer string
}{
{clock: mockClock(t, "2018/01/01"), answer: "大吉"},
{clock: mockClock(t, "2018/01/02"), answer: "大吉"},
{clock: mockClock(t, "2018/01/03"), answer: "大吉"},
}

for _, c := range cases {
t.Run("NewYearCase", func(t *testing.T) {
t.Helper()
cli := &CLI{Clock: c.clock}
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", nil)
cli.handler(w, r)
rw := w.Result()
defer rw.Body.Close()

if rw.StatusCode != http.StatusOK {
t.Error("unexpected status code\n")
}

m := struct {
Fortune string `json:"fortune"`
}{}

if err := json.NewDecoder(rw.Body).Decode(&m); err != nil {
t.Error("Decode error\n")
}

if m.Fortune != c.answer {
t.Errorf("unexpected fortune element: %v, expected: %v", m.Fortune, c.answer)
}
})
}
}

func contain(list []string, elm string) bool {
for _, l := range list {
if l == elm {
return true
}
}
return false
}

func mockClock(t *testing.T, v string) Clock {
t.Helper()
now, err := time.Parse("2006/01/02", v)
if err != nil {
t.Fatal("unexpected error:", err)
}

return ClockFunc(func() time.Time {
return now
})
}
15 changes: 15 additions & 0 deletions kadai4/shimastripe/clock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package shimastripe

import (
"time"
)

type Clock interface {
Now() time.Time
}

type ClockFunc func() time.Time

func (f ClockFunc) Now() time.Time {
return f()
}
15 changes: 15 additions & 0 deletions kadai4/shimastripe/cmd/kadai4/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package main

import (
"os"
"time"

"github.com/gopherdojo/dojo3/kadai4/shimastripe"
)

func main() {
cli := &shimastripe.CLI{Clock: shimastripe.ClockFunc(func() time.Time {
return time.Now()
})}
os.Exit(cli.Run(os.Args))
}
50 changes: 50 additions & 0 deletions kadai4/shimastripe/fortune.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package shimastripe

import "math/rand"

type FortuneElement int
type FortuneRepository struct {
Fortune FortuneElement `json:"fortune"`
}

const (
daikichi FortuneElement = iota
chukichi
shokichi
suekichi
kichi
kyo
daikyo
threshold // use FortuneElement length
)

// Behave Stringer interface
func (oe FortuneElement) String() string {
switch oe {
case daikichi:
return "大吉"
case chukichi:
return "中吉"
case shokichi:
return "小吉"
case suekichi:
return "末吉"
case kichi:
return "吉"
case kyo:
return "凶"
case daikyo:
return "大凶"
default:
return "強運"
}
}

func (oe FortuneElement) MarshalJSON() ([]byte, error) {
return []byte(`"` + oe.String() + `"`), nil
}

// Draw a fortune randomly
func DrawRandomly() FortuneElement {
return FortuneElement(rand.Intn(int(threshold)))
}