writing-go-tests
Writing Go Tests
Project-specific test conventions for this codebase.
Critical Rules
- Always use mockery-generated
Moq*mocks when one exists for the interface. Never hand-roll a mock struct for an interface that has a*_mock.gofile. Runmockery(no args) from the module root to regenerate mocks after interface changes. - Use
logger.NoopLogger{}when tests don't need to assert on log output. It already implements the fullLoggerinterface. Never create a custom mock logger just to satisfy the interface — that duplicatesNoopLoggerfor no reason. - Use
logger.MoqLogger{}only when the test needs to verify specific log calls (e.g., asserting thatWarningwas called with a specific message).
Mock Selection Guide
| Situation | Use | Import |
|---|---|---|
Interface has a *_mock.go file |
Moq* struct from that file |
Same package (in-package tests) |
| Logger needed but output doesn't matter | logger.NoopLogger{} |
utils/logger |
| Logger needed and must assert on calls | logger.MoqLogger{} |
utils/logger |
Interface has NO mock file (e.g., MultiSelectSelector[T]) |
Inline mock struct in test file | N/A |
Before creating an inline mock, check if a *_mock.go file exists in the interface's package:
ls installer/<package-path>/*_mock.go
Test Naming
Format: Test_<DescriptiveStatement>
Test names describe behavior, not implementation:
// Good: describes behavior
func Test_CompatibilityConfigCanBeLoadedFromFile(t *testing.T)
func Test_CreatingClientShouldLoadCompatibilityMapFromFile(t *testing.T)
// Bad: describes implementation
func Test_LoadConfig(t *testing.T)
func Test_ConfigLoader_Success(t *testing.T)
Assertions
Use testify/require for all assertions. When expecting errors, match by keyword, not full message:
// Good: checks for key error indicator
require.Error(t, err)
require.Contains(t, err.Error(), "not found")
// Bad: matches entire error message
require.EqualError(t, err, "file could not be found in the path /config/nonexistent.yaml")
Unit Tests
Unit tests verify a single function or method in isolation.
- Use mocks to isolate the function being tested.
- Place unit tests in the same package as the code being tested.
- Each test verifies a single behavior.
package brew
func Test_BrewPackageManagerInstallsPackageSuccessfully(t *testing.T) {
// Arrange
commander := &MoqCommander{
RunCommandFunc: func(ctx context.Context, name string, args []string, opts ...Option) (Result, error) {
return Result{ExitCode: 0}, nil
},
}
pm := NewBrewPackageManager(&logger.NoopLogger{}, commander, osManager, "/opt/homebrew/bin/brew")
// Act
err := pm.InstallPackage(ctx, RequestedPackageInfo{Name: "git"})
// Assert
require.NoError(t, err)
}
Table-Driven Tests
Use when testing multiple scenarios of the same function:
func Test_VerbosityLevelDetermination(t *testing.T) {
tests := []struct {
name string
verbose bool
extra bool
expected VerbosityLevel
}{
{"default returns normal", false, false, VerbosityNormal},
{"verbose flag returns verbose", true, false, VerbosityVerbose},
{"both flags returns extra verbose", true, true, VerbosityExtraVerbose},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := determineVerbosity(tt.verbose, tt.extra)
require.Equal(t, tt.expected, result)
})
}
}
Integration Tests
Integration tests verify interaction between components, including OS-dependent interactions.
- Allow opting out with
testing.Short(). - Place in the test package (e.g.,
brew_testforbrewpackage). - Use BDD-style naming:
Test_<gerund>_Should_<behavior>_When_<condition>.
package brew_test
func Test_InstallingPackage_Should_SucceedWithoutError_When_BrewIsAvailable(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test in short mode")
}
pm := NewBrewPackageManager(/* real dependencies */)
err := pm.InstallPackage(ctx, RequestedPackageInfo{Name: "tree"})
require.NoError(t, err)
}
Tech Stack
testify/require— assertions (neverassertfor error checks)mockerywith moq template — mock generation (see.mockery.yml)logger.NoopLogger{}— silent logger for tests
More from mrpointer/dotfiles
configuring-zsh
Configure and troubleshoot Zsh shell. Use when editing .zshenv, .zprofile, .zshrc, .zlogin, or .zlogout, setting up powerlevel10k prompt, configuring oh-my-zsh or sheldon plugin manager, fixing PATH or environment variables, debugging slow shell startup, setting up completions/compinit/fpath, or working with zsh-autocomplete, zsh-autosuggestions, or zsh-syntax-highlighting plugins.
20configuring-github-actions
Create and troubleshoot GitHub Actions workflows. Use when editing .github/workflows files, setting up CI/CD pipelines, configuring matrix builds for multi-platform testing, debugging failing workflows, adding caching or artifacts, running E2E tests in containers, or asking "why is my workflow failing" or "how do I test on multiple OSes".
11managing-chezmoi
Manage dotfiles with chezmoi. Use when adding files to chezmoi, running chezmoi add/apply/diff/status, debugging why changes aren't appearing, working with chezmoi templates or .chezmoiignore, understanding source vs target files, resolving merge conflicts, or asking "how do I manage this file with chezmoi". For chezmoi command uncertainties, use Context7 to fetch latest docs.
2writing-go-code
Apply Go coding standards when writing or modifying Go code. Use when implementing functions, using dependency injection, handling errors idiomatically, or working with interfaces. For test conventions, use the `writing-go-tests` skill instead.
1testing-go-code
Run Go unit tests, coverage reports, and benchmarks. Use when you need to run tests, check coverage, run benchmarks, or regenerate mocks after interface changes.
1linting-go-code
Lint and format Go code. Use when you need to run linters, fix lint errors, format code, or understand why a linter is complaining.
1