Fix

"no test files" in Go

The "no test files" error occurs because Go cannot find any files matching the `_test.go` pattern in the current directory or its subdirectories.

When Go says "no test files"

You spend twenty minutes writing a test. You save the file. You run go test and the terminal stares back with no test files in .. You check the directory. The file is there. It has code. It has func Test. Go is lying to you. Or rather, Go is following a rule you haven't learned yet. The test runner isn't broken. It's just very picky about what counts as a test file.

This error stops development dead. You can't verify your code. You can't run the suite. The fix is almost always a filename or a function signature, but understanding why Go enforces these rules helps you avoid the trap forever.

The _test.go rule

Go separates production code from test code by filename. The compiler and the test runner look for a specific suffix: _test.go. If a file doesn't end with that exact string, Go treats it as regular source code. If you run go test in a directory with only main.go and utils.go, Go scans the files, finds no _test.go suffix, and reports no test files in ..

This design keeps test dependencies out of your binary. When you build your application, the test files are ignored. You don't accidentally ship test helpers or mock data to your users. The convention is strict because the tooling relies on it. The suffix is the contract. Without it, Go treats the file as production code.

Minimal working test

Save the following code as main_test.go. The filename ends with _test.go. The function starts with Test and takes a *testing.T parameter.

package main

import (
	"testing"
)

// TestAddition verifies the Add function returns the correct sum.
// The name must start with "Test" and take a *testing.T parameter.
func TestAddition(t *testing.T) {
	// t is the testing context. Use it to report failures.
	// If this assertion fails, t.Error marks the test as failed.
	if 1+1 != 2 {
		t.Error("Expected 2, got something else")
	}
}

Run go test. The output shows PASS. Rename the file to main_tests.go or test_main.go. Run go test again. You get no test files in .. The suffix is the key. Rename the file and the test vanishes. The filename is the trigger.

How go test finds your code

When you run go test, the tool performs a scan. It looks at the current package directory for files matching *_test.go. If it finds none, it stops and prints the error. It does not look at file contents yet.

If it finds matching files, it compiles them together with the package source code. The test runner injects a main function that discovers all functions starting with Test and calls them. The *testing.T parameter is passed automatically. This parameter tracks state, logs output, and handles parallel execution.

The function signature must be exact. func TestFoo(t *testing.T) works. func TestFoo() fails to compile with TestFoo has the wrong signature for a test function. func testFoo(t *testing.T) is ignored because the name isn't exported. Go requires test functions to be capitalized so the test runner can find them via reflection. If you lowercase the name, the file is found, but the test is skipped. You might see 0 tests in the output, which is harder to debug than an error.

The compiler builds a temporary binary just for testing. Test files never reach the final binary.

Realistic test patterns

Real projects use more structure. Go developers favor table-driven tests. This pattern lets you test many cases with minimal boilerplate. It also makes adding new cases trivial.

package user

import (
	"fmt"
	"testing"
)

// User represents a user in the system.
type User struct {
	Name string
	Role string
}

// Greeting returns a personalized message for the user.
// The receiver name is a short lowercase letter matching the type.
func (u *User) Greeting() string {
	return fmt.Sprintf("Hello, %s. You are a %s.", u.Name, u.Role)
}

// TestUserGreeting checks that Greeting formats the output correctly.
// Tests live in the same package to access unexported fields if needed.
func TestUserGreeting(t *testing.T) {
	u := &User{Name: "Alice", Role: "admin"}
	expected := "Hello, Alice. You are a admin."
	got := u.Greeting()

	// Use t.Errorf for formatted error messages on failure.
	if got != expected {
		t.Errorf("Greeting() = %q, want %q", got, expected)
	}
}

// TestGreetingTable uses a table-driven approach to test multiple cases.
// This pattern reduces boilerplate and makes adding cases easy.
func TestGreetingTable(t *testing.T) {
	tests := []struct {
		name     string
		user     User
		expected string
	}{
		{name: "Admin", user: User{Name: "Bob", Role: "admin"}, expected: "Hello, Bob. You are a admin."},
		{name: "Guest", user: User{Name: "Carol", Role: "guest"}, expected: "Hello, Carol. You are a guest."},
	}

	for _, tt := range tests {
		// t.Run creates a subtest. This gives each case a name in the output.
		t.Run(tt.name, func(t *testing.T) {
			got := tt.user.Greeting()
			if got != tt.expected {
				t.Errorf("Greeting() = %q, want %q", got, tt.expected)
			}
		})
	}
}

Table-driven tests scale. Add a case, not a function.

You might need a helper function to reduce repetition. If your helper calls t.Error, the error message points to the helper line, not the test line. Use t.Helper() to fix this.

// assertEqual checks if two values match and fails the test if they don't.
// Calling t.Helper() ensures error messages point to the caller, not this function.
func assertEqual(t *testing.T, got, want interface{}) {
	t.Helper()
	if got != want {
		t.Errorf("got %v, want %v", got, want)
	}
}

Common traps and errors

The no test files error is the most obvious trap. Other traps hide in plain sight.

If you name the file my_tests.go, Go ignores it. The suffix must be singular: _test.go. Plural suffixes are not recognized.

If you write func TestFoo(), the compiler rejects the program with TestFoo has the wrong signature for a test function. The parameter is mandatory.

If you write func testFoo(t *testing.T), the test is ignored. The function name must be exported. Capitalize the first letter.

If you place a test file in a subdirectory but run go test from the parent, you get no test files in .. The test runner operates on the current package. To run tests across a module, use go test ./... from the root. This expands to all packages recursively.

If you use an external test package, you declare package user_test instead of package user. This forces you to import the package you are testing. If you forget the import, the compiler complains with undefined: user. If you import the wrong package, you get imported and not used. External test packages are useful for black-box testing, but they require correct imports.

The test runner is a pattern matcher. Feed it the pattern it expects.

Choosing your test structure

Go offers flexibility in how you organize tests. Pick the structure that matches your needs.

Use package foo in your test file when you need to test unexported functions or access internal struct fields. This is the default approach for most unit tests.

Use package foo_test when you want to test the public API as an external consumer would, without access to private details. This enforces strict separation and catches accidental reliance on internal implementation.

Use *_test.go suffix for all test files to ensure the tooling discovers and compiles them correctly. Never use _tests.go or test_ prefixes.

Use go test ./... when you want to run tests across all packages in a module from the root directory. This is standard for CI/CD pipelines and pre-commit checks.

Use go test -v when you need detailed output to see which tests ran and passed. The verbose flag is essential for debugging failures in large suites.

Use t.Run for subtests when you have multiple cases or setup steps that should be isolated. Subtests allow you to run specific cases by name and improve output clarity.

Name the file right. Name the function right. The rest follows.

Where to go next