{
  "$type": "site.standard.document",
  "canonicalUrl": "https://rednafi.com/go/organizing-tests/",
  "description": "Organize Go tests with in-package, external _test packages, and integration tests. Learn white-box vs black-box testing conventions.",
  "path": "/go/organizing-tests/",
  "publishedAt": "2025-10-08T00:00:00.000Z",
  "site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
  "tags": [
    "Go",
    "Testing"
  ],
  "textContent": "When it comes to test organization, Go's standard testing library only gives you a few\noptions. I think that's a great thing because there are fewer details to remember and fewer\nthings to onboard people to. However, during code reviews, I often see people contravene a\nfew common conventions around test organization, especially those who are new to the\nlanguage.\n\nIf we distill the most common questions that come up when organizing tests, they are:\n\n- Where to put the unit tests for a package\n- How to enable [white-box] and [black-box] testing\n- Where the [executable examples], [benchmarks], and [fuzz tests] should live\n- Where the integration and end-to-end tests for a service should live\n\nTo answer these, let's consider a simple test subject.\n\nSystem under test (SUT)\n\nLet's define a small app called myapp that contains a single package mypkg. It has a\nGreet function that returns a greeting message as a string. We'll use this throughout the\ndiscussion and evolve the directory structure as needed.\n\nHere's how greet.go looks:\n\nIn-package tests\n\nMost Go tests live next to the code they verify. These are called _in-package tests_, and\nthey share the same package name as the code under test. This setup gives them access to\nunexported functions and variables, making them ideal for unit tests that target specific\ninternal logic.\n\nThe structure stays the same:\n\nThese are your bread-and-butter unit tests. You can run them with go test ./..., and\nthey'll have full access to unexported details in the package.\n\nThe [Go documentation] explains it as:\n\n> The test file can be in the same package as the one being tested. If the test file is in\n> the same package, it may refer to unexported identifiers within the package.\n\nThis approach is called _white-box testing_. Your test code has full access to the package\ninternals, allowing you to test them directly when needed. For example, if there's an\nunexported function in greet.go, the test in greet_test.go can call it directly.\nFollowing the [test pyramid], most tests in your system should be written this way.\n\nCo-located external tests\n\nSometimes you want to verify that your package behaves correctly from the outside. At this\npoint, you're not concerned with its internals and just want to confirm that the public API\nworks as intended.\n\nGo makes this possible by letting you write tests under a package name that ends with\n_test. This creates a separate test package that lives alongside the package under test.\nFor example:\n\nYour directory now includes both internal and external tests:\n\nIn this setup, the mypkg directory can only contain the mypkg and mypkg_test packages.\nThe compiler recognizes the _test suffix and disallows any other package names in the same\ndirectory.\n\nA key detail is that the Go test harness doesn't build the tests of mypkg_test together\nwith those of mypkg. It compiles two separate test binaries: one containing the package\ncode and its in-package tests, and another containing the external tests. Each binary runs\nindependently, and the external one links against the compiled mypkg archive just like any\nother importing package. You can find more about this process in the [Go documentation on\nhow tests are run].\n\nThis structure is particularly useful for validating public contracts and ensuring that\nrefactors don't break exported APIs.\n\nAs noted in the official testing package docs:\n\n> If the file is in a separate _test package, the package being tested must be imported\n> explicitly, and only its exported identifiers may be used. This is known as “black-box\"\n> testing.\n\nIt's a neat way to test your package from the outside without moving your tests into a\nseparate directory tree. You can find examples of this style in [net/http], [context], and\n[errors].\n\nExamples, benchmarks, and fuzz tests\n\nGo's testing tool treats examples, benchmarks, and fuzz tests as first-class test functions.\nThey use the same go test command as your regular unit tests and usually live in the same\npackage. This makes them part of the same discovery and execution process but with different\nentry points.\n\nHere's how all three can coexist in the same package:\n\nThis setup doesn't change your layout:\n\nIf you prefer to separate these test types, you can move them into their own file while\nkeeping them in the same package:\n\nIn this layout, greet_bench_fuzz_example.go houses the benchmarks, fuzz tests, and\nexamples, but all files still declare the same package mypkg. These are regular unit tests\nwith specialized entry points. See how packages like [encoding/json] or [html] organize\ntheir fuzz tests.\n\nIt's not a strict rule to keep them in the same package. You can also put them in a _test\npackage. The [sort] package, for example, keeps its examples in sort_test.\n\nAs mentioned in the testing docs, benchmarks are discovered and executed with the -bench\nflag, and fuzz tests with the -fuzz flag.\n\nIntegration and end-to-end tests\n\nWhen your project grows into multiple packages, you'll want to verify that everything works\ntogether, not just in isolation. That's where integration and end-to-end tests come in. They\ntypically live outside the package tree because they often span multiple packages or\nprocesses.\n\nHere's what one might look like:\n\nIntegration tests import real packages and test their interactions. They can spin up\nservers, connect to databases, or coordinate subsystems. The integration test packages are\njust like any other package: to communicate with any other package, it needs to be imported\nexplicitly.\n\nYou'll see this pattern in [kubernetes], which has a test directory with subpackages like\nintegration and e2e.\n\nHaving a top-level package for testing only makes sense if you're testing multiple packages.\nOtherwise, if you're writing integration or functional tests for a single package, you can\nstill nest the tests under the SUT package. In this case, integration tests for mypkg can\nbe tucked away under mypkg/test.\n\nClosing\n\nThe general rule of thumb is:\n\n- Unit tests stay in the same package as the code.\n- Black-box tests use a _test package in the same directory.\n- Examples, benchmarks, and fuzz tests live with the unit tests, though you may put them in\n  _test if needed.\n- Integration and end-to-end tests live outside the SUT package tree.\n\nThe following tree attempts to capture the full picture:\n\n\n\n\n\n[white-box]:\n    https://en.wikipedia.org/wiki/White-box_testing\n\n[black-box]:\n    https://en.wikipedia.org/wiki/Black-box_testing\n\n[executable examples]:\n    https://go.dev/blog/examples\n\n[benchmarks]:\n    https://dave.cheney.net/2013/06/30/how-to-write-benchmarks-in-go\n\n[fuzz tests]:\n    https://go.dev/doc/tutorial/fuzz\n\n[go documentation]:\n    https://pkg.go.dev/testing\n\n[test pyramid]:\n    https://martinfowler.com/articles/practical-test-pyramid.html\n\n[go documentation on how tests are run]:\n    https://pkg.go.dev/cmd/go#hdr-Test_packages\n\n[net/http]:\n    https://github.com/golang/go/tree/master/src/net/http\n\n[context]:\n    https://github.com/golang/go/tree/master/src/context\n\n[errors]:\n    https://github.com/golang/go/tree/master/src/errors\n\n[encoding/json]:\n    https://github.com/golang/go/tree/master/src/encoding/json\n\n[html]:\n    https://github.com/golang/go/tree/master/src/html\n\n[sort]:\n    https://github.com/golang/go/tree/master/src/sort\n\n[kubernetes]:\n    https://github.com/kubernetes/kubernetes/tree/master/test",
  "title": "Organizing Go tests"
}