Day 027 — Project: Tests + README¶
Month 1 · Week 4 · ⬅ Day 026 · Day 028 ➡ · Journal index
🎯 Learning Objective¶
Make taskcli trustworthy: table-driven tests for the store and commands (using t.TempDir() for real file round-trips), plus a clear project README.
📚 Topics¶
testing— table-driven tests, subtestst.Run, helpers +t.Helper()t.TempDir()for isolated, auto-cleaned filesystem tests- Testing errors with
errors.Is(sentinelErrNotFound) go test -run/-v/-cover,go test ./...· writing a useful README
📖 Reading / Sources¶
-
pkg.go.dev/testing—T,TempDir,Helper - Go blog — Using Subtests and Sub-benchmarks
- Effective Go — Testing names
📝 Notes¶
- Table-driven tests are the Go idiom: a slice of
{name, input, want}cases looped witht.Run(tc.name, ...)so each row is an independent, named subtest you can target with-run. t.TempDir()returns a fresh directory auto-removed after the test — perfect forLoad/Saveround-trips without touching the real home dir or leaking files. No manual cleanup needed.- Test the round-trip: build a store, add tasks,
Save,Loadfrom the same path, assert equality withreflect.DeepEqual(or compare fields; usetime.Time.Equalfor timestamps). Connects to [[Day 025]]. - Test error paths explicitly:
MarkDone(999)should return an error matching a sentinelErrNotFound— assert witherrors.Is(err, ErrNotFound), never string-compare. Connects to the [[errors]] toolkit. - Mark reusable assertion code with
t.Helper()so failures report the caller's line, not the helper's. - Use
t.Fatalfwhen continuing is pointless (setup failed) andt.Errorfto record a failure but keep checking other assertions in the row. - Don't depend on map iteration order in assertions (it's randomized) — sort first or compare sets. Connects to [[maps-sets]].
- Coverage:
go test -cover ./...; target the store + command logic, not trivial getters. Run the whole repo withgo test ./...before committing. - README should answer: what it does, install/build, usage examples for each subcommand, where data is stored, and how to run tests. A good README is part of the deliverable.
💻 Code Examples¶
func TestStoreRoundTrip(t *testing.T) {
dir := t.TempDir() // isolated, auto-cleaned
path := filepath.Join(dir, "tasks.json")
s, err := Load(path) // missing file → empty store, no error
if err != nil {
t.Fatalf("Load empty: %v", err)
}
s.Add("write tests")
s.Add("ship it")
if err := s.Save(); err != nil {
t.Fatalf("Save: %v", err)
}
reloaded, err := Load(path)
if err != nil {
t.Fatalf("reload: %v", err)
}
if got := len(reloaded.All()); got != 2 {
t.Fatalf("len after reload = %d; want 2", got)
}
}
func TestMarkDoneNotFound(t *testing.T) {
s := &Store{} // empty
err := s.MarkDone(999)
if !errors.Is(err, ErrNotFound) { // sentinel match, not string compare
t.Errorf("MarkDone(999) err = %v; want ErrNotFound", err)
}
}
Project tests live under
projects/taskcli/store_test.go. The same table-driven +errors.Ispatterns are exercised stdlib-only in the Week-4 exercises.
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
| Table-driven tests across all Week-4 exercises green | ✅ | exercises/month-01/week-4 |
t.TempDir store round-trip + ErrNotFound path |
✅ | (project store_test.go) |
🐛 Mistakes Made¶
- Wrote a temp file in the test and forgot to clean it up; replaced manual
os.MkdirTemp+defer os.RemoveAllwitht.TempDir()(auto-cleanup, less boilerplate). - Asserted
err.Error() == "task not found"— brittle. Switched toerrors.Is(err, ErrNotFound). - A list test compared slices built from map iteration and flaked; sorted by ID before comparing.
❓ Open Questions¶
- Fuzz testing (
go test -fuzz) for the JSON decode path — worth a look in Month 2.
🧠 Active Recall (answer without looking)¶
-
Q: What does
t.TempDir()give you and why prefer it overos.MkdirTemp?
A
A unique temp directory scoped to the test that's **automatically removed** when the test finishes — no manual `defer os.RemoveAll`, and isolation between parallel tests. -
Q: Why assert error cases with
errors.Is(err, ErrNotFound)instead of comparing the message string?
A
The message can change and may be wrapped; `errors.Is` walks the wrap chain and matches the sentinel by identity, so the test stays correct under `%w` wrapping and message edits.
🪶 Feynman Reflection¶
Tests pin down behavior so I can refactor fearlessly. The two power tools today: table-driven subtests (one loop, many named cases) and t.TempDir() (real file I/O with zero cleanup). Testing the error paths with errors.Is proves the contract, not the wording.
🕳️ Knowledge Gaps¶
- Benchmarks (
testing.B) and-racefor the project — light here, will lean on them in concurrency month.
✅ Summary¶
taskcli is tested end-to-end with table-driven subtests and t.TempDir() round-trips, error paths asserted via errors.Is, and shipped with a README — it's a portfolio-ready mini-project.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 028: Month 1 review, write the monthly retro, and tag the
v0.1.0release.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 95 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦🟦⬜ |
Suggested commit: test(taskcli): table-driven store tests and project README (day 027)