Skip to content

Commit 2707b5c

Browse files
Merge pull request #12 from chrisreddington/increase-test-coverage
Increase test coverage
2 parents 2d0e451 + 30f9948 commit 2707b5c

4 files changed

Lines changed: 432 additions & 92 deletions

File tree

cmd/cointoss.go

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ package cmd
22

33
import (
44
"fmt"
5-
"strings"
6-
7-
"github.com/chrisreddington/gh-game/internal/cointoss" // adjust import path as needed
5+
"os"
86

7+
"github.com/chrisreddington/gh-game/internal/cointoss"
8+
userPrompt "github.com/cli/go-gh/v2/pkg/prompter"
99
"github.com/spf13/cobra"
1010
)
1111

@@ -20,24 +20,11 @@ var cointossCmd = &cobra.Command{
2020
return cointoss.ValidateGuess(args[0])
2121
},
2222
Run: func(cmd *cobra.Command, args []string) {
23-
guess := strings.ToLower(strings.TrimSpace(args[0]))
24-
streak := 0
25-
keepPlaying := true
26-
27-
for keepPlaying {
28-
result := cointoss.TossCoin()
29-
fmt.Printf("The coin shows: %s!\n", strings.Title(result))
30-
31-
if guess == result {
32-
streak++
33-
fmt.Printf("Correct! Streak: %d\n", streak)
34-
var continuePlay bool
35-
guess, continuePlay = cointoss.GetNextGuess()
36-
keepPlaying = continuePlay
37-
} else {
38-
fmt.Printf("Game Over! Final streak: %d\n", streak)
39-
keepPlaying = false
40-
}
41-
}
23+
input := userPrompt.New(os.Stdin, os.Stdout, os.Stderr)
24+
cointoss.PlayGame(input, args[0])
4225
},
4326
}
27+
28+
func init() {
29+
rootCmd.AddCommand(cointossCmd)
30+
}

internal/cointoss/cointoss.go

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,30 @@ package cointoss
33
import (
44
"fmt"
55
"math/rand"
6-
"os"
76
"strings"
87
"time"
9-
10-
userPrompt "github.com/cli/go-gh/v2/pkg/prompter"
118
)
129

10+
// Game represents the state of a coin toss game
11+
type Game struct {
12+
PlayerGuess string
13+
Result string
14+
IsOver bool
15+
}
16+
1317
// prompter interface allows us to mock the prompt functionality in tests
1418
type prompter interface {
1519
Select(prompt string, defaultValue string, options []string) (int, error)
1620
}
1721

18-
func TossCoin() string {
22+
func NewGame() *Game {
23+
return &Game{
24+
IsOver: false,
25+
}
26+
}
27+
28+
// TossCoin is a variable so it can be replaced in tests
29+
var TossCoin = func() string {
1930
rand.Seed(time.Now().UnixNano())
2031
if rand.Float32() < 0.5 {
2132
return "heads"
@@ -31,11 +42,8 @@ func ValidateGuess(guess string) error {
3142
return nil
3243
}
3344

34-
func GetNextGuess() (string, bool) {
35-
return GetNextGuessWithPrompter(userPrompt.New(os.Stdin, os.Stdout, os.Stderr))
36-
}
37-
38-
func GetNextGuessWithPrompter(p prompter) (string, bool) {
45+
// GetPlayerGuess gets the player's next guess using the provided prompter
46+
func GetPlayerGuess(p prompter) (string, bool) {
3947
options := []string{"Heads", "Tails", "Quit"}
4048

4149
answer, err := p.Select("What's your next guess? Heads, Tails or Quit?", "Heads", options)
@@ -51,3 +59,40 @@ func GetNextGuessWithPrompter(p prompter) (string, bool) {
5159

5260
return answerLower, true
5361
}
62+
63+
// Play executes a round of the coin toss game
64+
func (g *Game) Play(guess string) {
65+
g.PlayerGuess = guess
66+
g.Result = TossCoin()
67+
g.IsOver = true
68+
}
69+
70+
// GetResult returns the game result message
71+
func (g *Game) GetResult() string {
72+
if g.PlayerGuess == g.Result {
73+
return fmt.Sprintf("You guessed %s and the coin landed on %s. You win!", g.PlayerGuess, g.Result)
74+
}
75+
return fmt.Sprintf("You guessed %s but the coin landed on %s. You lose!", g.PlayerGuess, g.Result)
76+
}
77+
78+
// PlayGame handles the main game loop
79+
func PlayGame(p prompter, initialGuess string) {
80+
game := NewGame()
81+
streak := 0
82+
keepPlaying := true
83+
guess := strings.ToLower(strings.TrimSpace(initialGuess))
84+
85+
for keepPlaying {
86+
game.Play(guess)
87+
fmt.Println(game.GetResult())
88+
89+
if game.PlayerGuess == game.Result {
90+
streak++
91+
fmt.Printf("Streak: %d\n", streak)
92+
guess, keepPlaying = GetPlayerGuess(p)
93+
} else {
94+
fmt.Printf("Game Over! Final streak: %d\n", streak)
95+
keepPlaying = false
96+
}
97+
}
98+
}

internal/cointoss/cointoss_test.go

Lines changed: 135 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cointoss
22

33
import (
44
"errors"
5+
"strings"
56
"testing"
67
)
78

@@ -41,7 +42,7 @@ func TestTossCoin(t *testing.T) {
4142
}
4243
}
4344

44-
func TestGetNextGuessWithPrompter(t *testing.T) {
45+
func TestGetPlayerGuess(t *testing.T) {
4546
tests := []struct {
4647
name string
4748
selectAnswer int
@@ -77,13 +78,6 @@ func TestGetNextGuessWithPrompter(t *testing.T) {
7778
expectedGuess: "",
7879
expectedCont: false,
7980
},
80-
{
81-
name: "error during selection prints error message",
82-
selectAnswer: 0,
83-
selectError: errors.New("test error"),
84-
expectedGuess: "",
85-
expectedCont: false,
86-
},
8781
}
8882

8983
for _, tt := range tests {
@@ -92,15 +86,143 @@ func TestGetNextGuessWithPrompter(t *testing.T) {
9286
selectAnswer: tt.selectAnswer,
9387
selectError: tt.selectError,
9488
}
95-
96-
guess, cont := GetNextGuessWithPrompter(mockP)
97-
89+
guess, cont := GetPlayerGuess(mockP)
9890
if guess != tt.expectedGuess {
99-
t.Errorf("GetNextGuessWithPrompter() guess = %v, want %v", guess, tt.expectedGuess)
91+
t.Errorf("GetPlayerGuess() guess = %v, want %v", guess, tt.expectedGuess)
10092
}
10193
if cont != tt.expectedCont {
102-
t.Errorf("GetNextGuessWithPrompter() cont = %v, want %v", cont, tt.expectedCont)
94+
t.Errorf("GetPlayerGuess() cont = %v, want %v", cont, tt.expectedCont)
95+
}
96+
})
97+
}
98+
}
99+
100+
func TestGame_Play(t *testing.T) {
101+
game := NewGame()
102+
103+
// Test initial state
104+
if game.IsOver {
105+
t.Error("New game should not be over")
106+
}
107+
108+
// Play a round
109+
game.Play("heads")
110+
111+
// Test that game state is updated
112+
if !game.IsOver {
113+
t.Error("Game should be over after playing")
114+
}
115+
if game.PlayerGuess != "heads" {
116+
t.Errorf("PlayerGuess = %v, want heads", game.PlayerGuess)
117+
}
118+
if game.Result != "heads" && game.Result != "tails" {
119+
t.Errorf("Result = %v, want either heads or tails", game.Result)
120+
}
121+
}
122+
123+
func TestGame_GetResult(t *testing.T) {
124+
tests := []struct {
125+
name string
126+
playerGuess string
127+
result string
128+
wantWin bool
129+
}{
130+
{
131+
name: "player wins with heads",
132+
playerGuess: "heads",
133+
result: "heads",
134+
wantWin: true,
135+
},
136+
{
137+
name: "player wins with tails",
138+
playerGuess: "tails",
139+
result: "tails",
140+
wantWin: true,
141+
},
142+
{
143+
name: "player loses with heads",
144+
playerGuess: "heads",
145+
result: "tails",
146+
wantWin: false,
147+
},
148+
{
149+
name: "player loses with tails",
150+
playerGuess: "tails",
151+
result: "heads",
152+
wantWin: false,
153+
},
154+
}
155+
156+
for _, tt := range tests {
157+
t.Run(tt.name, func(t *testing.T) {
158+
game := &Game{
159+
PlayerGuess: tt.playerGuess,
160+
Result: tt.result,
161+
IsOver: true,
103162
}
163+
got := game.GetResult()
164+
if tt.wantWin && !contains(got, "You win!") {
165+
t.Errorf("GetResult() = %v, want win message", got)
166+
}
167+
if !tt.wantWin && !contains(got, "You lose!") {
168+
t.Errorf("GetResult() = %v, want lose message", got)
169+
}
170+
})
171+
}
172+
}
173+
174+
// Helper function to check if a string contains a substring
175+
func contains(s, substr string) bool {
176+
return strings.Contains(s, substr)
177+
}
178+
179+
func TestPlayGame(t *testing.T) {
180+
tests := []struct {
181+
name string
182+
selectAnswer int
183+
selectError error
184+
initialGuess string
185+
results []string // sequence of coin flip results to test
186+
}{
187+
{
188+
name: "win first round then quit",
189+
selectAnswer: 2, // quit
190+
initialGuess: "heads",
191+
results: []string{"heads"},
192+
},
193+
{
194+
name: "lose first round",
195+
initialGuess: "heads",
196+
results: []string{"tails"},
197+
},
198+
{
199+
name: "win twice then lose",
200+
selectAnswer: 0, // heads
201+
initialGuess: "heads",
202+
results: []string{"heads", "heads", "tails"},
203+
},
204+
}
205+
206+
for _, tt := range tests {
207+
t.Run(tt.name, func(t *testing.T) {
208+
mockP := &mockPrompter{
209+
selectAnswer: tt.selectAnswer,
210+
selectError: tt.selectError,
211+
}
212+
213+
// Override TossCoin for deterministic testing
214+
resultIndex := 0
215+
oldTossCoin := TossCoin
216+
TossCoin = func() string {
217+
result := tt.results[resultIndex]
218+
if resultIndex < len(tt.results)-1 {
219+
resultIndex++
220+
}
221+
return result
222+
}
223+
defer func() { TossCoin = oldTossCoin }()
224+
225+
PlayGame(mockP, tt.initialGuess)
104226
})
105227
}
106228
}

0 commit comments

Comments
 (0)