You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: command-line.md
+374-1
Original file line number
Diff line number
Diff line change
@@ -203,5 +203,378 @@ That should make it pass.
203
203
204
204
Next we need to simulate reading from `Stdin` (the input from the user) so that we can record wins for specific players.
205
205
206
-
Let's add a new test to exercise this.
206
+
Let's extend our test to exercise this
207
+
208
+
## Write the test first
209
+
210
+
```go
211
+
funcTestCLI(t *testing.T) {
212
+
in:= strings.NewReader("Chris wins\n")
213
+
playerStore:= &StubPlayerStore{}
214
+
215
+
cli:= &PokerCLI{playerStore, in}
216
+
cli.PlayPoker()
217
+
218
+
iflen(playerStore.winCalls) < 1 {
219
+
t.Fatal("expected a win call but didnt get any")
220
+
}
221
+
222
+
got:= playerStore.winCalls[0]
223
+
want:="Chris"
224
+
225
+
if got != want {
226
+
t.Errorf("didnt record correct winner, got '%s', want '%s'", got, want)
227
+
}
228
+
}
229
+
```
230
+
231
+
`os.Stdin` is what we'll use in `main` to capture the user's input. It is a `*File` under the hood which means it implements `io.Reader` which as we know by now is a handy way of capturing text.
232
+
233
+
We create a `io.Reader` in our test using the handy `strings.NewReader`, filling it with what we expect the user to type.
234
+
235
+
## Try to run the test
236
+
237
+
`./PokerCLI_test.go:12:32: too many values in struct initializer`
238
+
239
+
## Write the minimal amount of code for the test to run and check the failing test output
240
+
241
+
We need to add our new dependency into `PokerCLI`.
242
+
243
+
```go
244
+
typePokerCLIstruct {
245
+
playerStore PlayerStore
246
+
in io.Reader
247
+
}
248
+
```
249
+
250
+
## Write enough code to make it pass
251
+
252
+
```
253
+
--- FAIL: TestCLI (0.00s)
254
+
PokerCLI_test.go:23: didnt record correct winner, got 'Cleo', want 'Chris'
255
+
FAIL
256
+
```
257
+
258
+
Remember to do the strictly easiest thing first
259
+
260
+
```go
261
+
func(cli *PokerCLI) PlayPoker() {
262
+
cli.playerStore.RecordWin("Chris")
263
+
}
264
+
```
265
+
266
+
The test passes. We'll add another test to force us to write some real code next, but first let's refactor
267
+
268
+
## Refactor
269
+
270
+
In `server_test` we earlier did checks to see if wins are recorded like we have here. Let's DRY that assertion up into a helper
271
+
272
+
```go
273
+
funcassertPlayerWin(t *testing.T, store *StubPlayerStore, winnerstring) {
274
+
t.Helper()
275
+
276
+
iflen(store.winCalls) != 1 {
277
+
t.Fatalf("got %d calls to RecordWin want %d", len(store.winCalls), 1)
278
+
}
279
+
280
+
if store.winCalls[0] != winner {
281
+
t.Errorf("did not store correct winner got '%s' want '%s'", store.winCalls[0], winner)
282
+
}
283
+
}
284
+
```
285
+
286
+
Now replace the assertions in both `server_test.go` and `PokerCLI_test.go`
287
+
288
+
The test should now read like so
289
+
290
+
```go
291
+
funcTestCLI(t *testing.T) {
292
+
in:= strings.NewReader("Chris wins\n")
293
+
playerStore:= &StubPlayerStore{}
294
+
295
+
cli:= &PokerCLI{playerStore, in}
296
+
cli.PlayPoker()
297
+
298
+
assertPlayerWin(t, playerStore, "Chris")
299
+
}
300
+
```
301
+
302
+
Now let's write _another_ test with different user input to force us into actually reading it.
303
+
304
+
## Write the test first
305
+
306
+
```go
307
+
funcTestCLI(t *testing.T) {
308
+
309
+
t.Run("record chris win from user input", func(t *testing.T) {
310
+
in:= strings.NewReader("Chris wins\n")
311
+
playerStore:= &StubPlayerStore{}
312
+
313
+
cli:= &PokerCLI{playerStore, in}
314
+
cli.PlayPoker()
315
+
316
+
assertPlayerWin(t, playerStore, "Chris")
317
+
})
318
+
319
+
t.Run("record cleo win from user input", func(t *testing.T) {
- Extract out the winner by using `strings.Replace` which takes the string to replace, what string to replace, its replacement and finally a flag to say how many instances to replace
359
+
360
+
## Refactor
361
+
362
+
We can extract getting the winner's name into a meaningful function
Now that we have some working(ish) software, we should wire this up into `main`. Remember we should always strive to have fully-integrated working software as quickly as we can.
log.Fatalf("problem creating file system player store, %v", err)
406
+
}
407
+
408
+
if err != nil{
409
+
log.Fatalf("problem creating ")
410
+
}
411
+
412
+
game:= poker.PokerCLI{store, os.Stdin}
413
+
game.PlayPoker()
414
+
}
415
+
```
416
+
417
+
You should get an error
418
+
419
+
```
420
+
command-line/v3/cmd/cli/main.go:32:25: implicit assignment of unexported field 'playerStore' in poker.PokerCLI literal
421
+
command-line/v3/cmd/cli/main.go:32:34: implicit assignment of unexported field 'in' in poker.PokerCLI literal
422
+
```
423
+
424
+
This highlights the importance of _integrating your work_. We rightfully made the dependencies of our `PokerCLI` private but haven't made a way for users to construct it.
425
+
426
+
Is there a way to have caught this problem earlier?
427
+
428
+
### `package mypackage_test`
429
+
430
+
In all other examples so far when we make a test file we declare it as being in the same package that we are testing.
431
+
432
+
This is fine and it means on the odd occasion where we want to test something internal to the package we have access to the unexported types.
433
+
434
+
But given we have advocated for _not_ testing internal things _generally_, can Go help enforce that? What if we could test our code where we only have access to the exported types (like our `main` does)?
435
+
436
+
When you're writing a project with different packages I would strongly recommend that your test package name has `_test` at the end. When you do this you will only be able to have access to the public types in your package. This would help with this specific case but also helps enforce the discipline of only testing public APIs.
437
+
438
+
An adage with TDD is that if you cannot test your code then it is probably poor code to work with. Using `package foo_test` will help with this.
439
+
440
+
Before fixing `main` let's change the package of our test inside `PokerCLI_test` to `poker_test`
441
+
442
+
If you have a well configured IDE you will suddenly see a lot of red! If you run the compiler you'll get the following errors
We have now stumbled into more questions on package design.
454
+
455
+
#### `Do we want to have our stubs and helpers 'public' ?`
456
+
457
+
This is a subjective discussion. One could definitely argue that you do not want to pollute your package's API with code to facilitate tests.
458
+
459
+
In the presentation ["Advanced Testing with Go"](https://speakerdeck.com/mitchellh/advanced-testing-with-go?slide=53) by Mitchell Hashimoto it is described how at HashiCorp they advocate doing this so that users of the package can write tests without having to re-invent the wheel writing stubs. In our case this would mean anyone using our poker package wont have to create their own stub `PlayerStore` if they wish to work with our code.
460
+
461
+
Anecdotally I have used this technique in other shared packages and it has proved extremely useful in terms of users saving time when integrating with our packages.
462
+
463
+
So let's create a file called `testing.go` and add our stub and our helpers.
464
+
465
+
```go
466
+
package poker
467
+
468
+
import"testing"
469
+
470
+
typeStubPlayerStorestruct {
471
+
scores map[string]int
472
+
winCalls []string
473
+
league []Player
474
+
}
475
+
476
+
func(s *StubPlayerStore) GetPlayerScore(namestring) int {
477
+
score:= s.scores[name]
478
+
return score
479
+
}
480
+
481
+
func(s *StubPlayerStore) RecordWin(namestring) {
482
+
s.winCalls = append(s.winCalls, name)
483
+
}
484
+
485
+
func(s *StubPlayerStore) GetLeague() League {
486
+
return s.league
487
+
}
488
+
489
+
funcAssertPlayerWin(t *testing.T, store *StubPlayerStore, winnerstring) {
490
+
t.Helper()
491
+
492
+
iflen(store.winCalls) != 1 {
493
+
t.Fatalf("got %d calls to RecordWin want %d", len(store.winCalls), 1)
494
+
}
495
+
496
+
if store.winCalls[0] != winner {
497
+
t.Errorf("did not store correct winner got '%s' want '%s'", store.winCalls[0], winner)
498
+
}
499
+
}
500
+
501
+
// todo for you - the rest of the helpers
502
+
```
503
+
504
+
You'll need to make the helpers public (remember exporting is done with a capital letter at the start) if you want them to be exposed to importers of our package.
505
+
506
+
In our CLI test you'll need to call the code as if you were using it within a different package.
507
+
508
+
```go
509
+
funcTestCLI(t *testing.T) {
510
+
511
+
t.Run("record chris win from user input", func(t *testing.T) {
512
+
in:= strings.NewReader("Chris wins\n")
513
+
playerStore:= &poker.StubPlayerStore{}
514
+
515
+
cli:= &poker.PokerCLI{playerStore, in}
516
+
cli.PlayPoker()
517
+
518
+
poker.AssertPlayerWin(t, playerStore, "Chris")
519
+
})
520
+
521
+
t.Run("record cleo win from user input", func(t *testing.T) {
522
+
in:= strings.NewReader("Cleo wins\n")
523
+
playerStore:= &poker.StubPlayerStore{}
524
+
525
+
cli:= &poker.PokerCLI{playerStore, in}
526
+
cli.PlayPoker()
527
+
528
+
poker.AssertPlayerWin(t, playerStore, "Cleo")
529
+
})
530
+
531
+
}
532
+
```
533
+
534
+
You'll now see we have the same problems as we had in `main`
535
+
536
+
```
537
+
./PokerCLI_test.go:15:26: implicit assignment of unexported field 'playerStore' in poker.PokerCLI literal
538
+
./PokerCLI_test.go:15:39: implicit assignment of unexported field 'in' in poker.PokerCLI literal
539
+
./PokerCLI_test.go:25:26: implicit assignment of unexported field 'playerStore' in poker.PokerCLI literal
540
+
./PokerCLI_test.go:25:39: implicit assignment of unexported field 'in' in poker.PokerCLI literal
541
+
```
542
+
543
+
The easiest way to get around this is to make a constructor as we have for other types
No matter what you type, you never see `2` logged. The reason is the `ReadAll`, we cant read "all" of `os.Stdin`, as you can just keep typing stuff in!
0 commit comments