{
  "$type": "site.standard.document",
  "canonicalUrl": "https://rednafi.com/go/subtest-grouping/",
  "description": "Organize Go subtests with t.Run nesting and parallel execution. Learn patterns for setup, teardown, and readable test hierarchies.",
  "path": "/go/subtest-grouping/",
  "publishedAt": "2025-10-01T00:00:00.000Z",
  "site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
  "tags": [
    "Go",
    "Testing"
  ],
  "textContent": "Go has [support for subtests] starting from version 1.7. With t.Run, you can nest tests,\nassign names to cases, and let the runner execute work in parallel by calling t.Parallel\nfrom subtests if needed.\n\nFor small suites, a flat set of t.Run calls is usually enough. That's where I tend to\nbegin. As the suite grows, your setup and teardown requirements may demand subtest grouping.\nThere are multiple ways to handle that.\n\nOne option is to group subtests using nested t.Run. However, since t.Run supports\narbitrary nesting, it's easy to create tests that are hard to read and reason about,\nespecially when each group has its own setup and teardown. When you add calls to\nt.Parallel, it can also become unclear which groups of tests run sequentially and which\nrun in parallel.\n\nThis is all a bit hand wavy without examples. We'll start with the simplest possible subtest\ngrouping and work our way up. Coming up with examples that make the point while still\nfitting in a blog is tricky, so you'll have to bear with my toy examples and use a bit of\nimagination.\n\nSystem under test (SUT)\n\nLet's say we're writing tests for a calculator that, for the sake of argument, can only do\naddition and multiplication. Instead of going for table-driven tests, we'll split the tests\nfor addition and multiplication into two groups using subtests. The reason being, let's say\naddition and multiplication need different kinds of setup and teardown for some reason.\n\nI know I'm reaching, but bear with me. I'd rather make the point without dragging in mocks,\ndatabases, or [testcontainers] and getting lost in details. But you can find similar setup\nin a real codebase everywhere where you might be talking to a database and your read and\nwrite path have separate [test lifecycles].\n\nKeep it flat until you can't\n\nIf we didn't need different setup and teardown for the two groups, the simplest way to test\na system would be through a set of table-driven tests:\n\nRunning the tests returns:\n\nUnrolling the tests would give you this. The following is equivalent to the above test\nsuite:\n\nObserve that all the subtests live at the same level. The names of the tests are the\nindicator of which function of the calculator they're testing. But this obviously doesn't\nallow us to have separate lifecycles for the addition and multiplication groups. There's no\ngrouping as of now.\n\nGroup subtests with nested t.Run when lifecycle diverges\n\nTo allow different setup and teardown for addition and multiplication, we can introduce\ngrouping by nesting the subtests via t.Run. Notice:\n\nIn this case, you can run the common setup and teardown in the top-level test function and\nthe groups can have their own lifecycle operations alongside. Introducing the group also\nallows us to name them properly and they show up when we run the tests:\n\nFrom the output it's clear which subtests belong to which group. This setup also allows you\nto run the groups in parallel by calling t.Parallel in each group.\n\nStarting with flat subtests and nesting them one extra level with t.Run should suffice in\nthe majority of cases. Readability of your tests usually starts hurting when you need to\nintroduce any additional nesting.\n\nI almost always frown when I encounter more than two degrees of nesting in a test suite. On\ntop of that, if your overly nested subtests start calling t.Parallel then it's quite\ndifficult to reason about the test execution flow. Plus, maintaining the lifecycles of the\nnested subgroups can get out of hand pretty quickly.\n\nBut even when you're grouping subtests with two degrees of nesting, if the individual test\nlogic starts getting longer, that might start hurting readability. Named functions for the\nsubtests can help here in most cases.\n\nExtract subtest groups into functions\n\nWe can rewrite the subtest grouping example of the previous section by extracting subtests\ninto two group-specific functions like this:\n\nAll we did here is extract the groups into their own functions. Other than that this test is\nidentical to the previous two-degree subtest grouping. You can call t.Parallel from the\nsubgroup functions:\n\nOr you can bring the t.Parallel at the top-level test function:\n\nThat's all there is to it. But some people don't like the manual wiring that we needed to do\nin the top-level TestCalc function. Also, in a larger codebase, you'll need some\ndiscipline to make sure the pattern is followed by others extending the code.\n\nSo often people want the subtest groups to be automatically discovered without them having\nto manually wire them in the main test function. While I'm not a big fan of automagical\ngroup discovery, I got curious about it nonetheless. The gRPC-go has a [group discovery\nfunction] that does this.\n\ngRPC-go uses reflection to discover groups\n\nIf we were writing tests inside the grpc-go repository, we could lean on its small helper\npackage, internal/grpctest, which reflects over a value you pass in, discovers methods\nwhose names start with Test, and runs each of those as a subtest. Crucially, the helper\nalso runs setup before and teardown after each discovered test method, which gives you a\nclear spot for per-group lifecycle work. The public surface is tiny: RunSubTests(t, x)\nplus a default hook carrier Tester that you embed to get Setup and Teardown.\n\nHere is our same calculator suite in that style, as if we were adding tests inside grpc-go:\n\nOutside grpc-go you can't import google.golang.org/grpc/internal/grpctest because it lives\nunder an internal/ path. Go's visibility rule only allows packages within that module tree\nto use it. If you want the subtest discoverer, there's nothing stopping you from [blatantly\ncopying the code]. It's only a few dozen lines and devoid of any dependencies other than the\nleak checker. You can drop the file in your tests, remove the leak checker code if you don't\nneed that, adjust the import paths, and start using RunSubTests. To avoid repetition, I'll\nleave that as an exercise to the reader.\n\nAnother thing to point out is that grpctest.RunSubTests doesn't change the standard\nscheduler; you still opt into concurrency with t.Parallel() where it is safe.\n\nSubgroup with third party libraries\n\nIf you like automatic subgroup discovery but want something you can use outside grpc-go, two\ncommon options are [testify's suite] and [Bloomberg's go-testgroup]. Both let you organize\ntests into named groups and keep per-group setup/teardown close to the cases.\n\nTestify's suite\n\nTestify models a suite as a struct with Test methods and gives you s.Run for subtests\nand assertion helpers.\n\nOne limitation is that the [suite runner doesn't support using t.Parallel] to run the\nsuite methods (TestAddition, TestMultiplication) in parallel. Bloomberg's test group\nallows you to do that.\n\nBloomberg's go-testgroup\n\nBloomberg's library also groups by methods, but passes a testgroup.T and provides two\nrunners so you can choose serial or parallel execution at the group level.\n\nRunInParallel handles group-level parallelism for you and documents not to mix in your own\nt.Parallel inside those methods.\n\nClosing\n\nWhile there are multiple ways to organize subtest groups, I try to keep them flat for as\nlong as possible. When grouping becomes necessary, I gradually add a single extra level of\nnesting with t.Run.\n\nIn larger tests, [extracting groups into their own named functions] improves readability and\nmaintainability quite a bit. I almost never use reflection-based wiring because that's one\nextra bit of code to carry around.\n\nI also tend to eschew pulling in third-party test suites unless I am already working in a\ncodebase that uses them. Tools like testify or go-testgroup require you to define a struct\nand attach tests to it. I prefer to keep tests as standalone functions. In addition,\n[testing frameworks often develop into mini-languages of their own], which makes onboarding\nharder. Notice how different the APIs of testify suite and go-testgroup are despite doing\npretty much the same thing.\n\nIn my experience, even in large codebases, a bit of discipline is usually enough to get by\nwith manual subtest grouping.\n\n\n\n[support for subtests]:\n    https://go.dev/blog/subtests\n\n[testcontainers]:\n    https://golang.testcontainers.org/\n\n[test lifecycles]:\n    /go/lifecycle-management-in-tests/\n\n[group discovery function]:\n    https://github.com/grpc/grpc-go/blob/d0ebcdffc75dc76f18966ab9cccafe6a949d6fb5/internal/grpctest/grpctest.go#L109\n\n[blatantly copying the code]:\n    https://github.com/grpc/grpc-go/blob/d0ebcdffc75dc76f18966ab9cccafe6a949d6fb5/internal/grpctest/grpctest.go\n\n[testify's suite]:\n    https://github.com/stretchr/testify\n\n[suite runner doesn't support using t.Parallel]:\n    https://github.com/stretchr/testify?tab=readme-ov-file#suite-package\n\n[bloomberg's go-testgroup]:\n    https://github.com/bloomberg/go-testgroup\n\n[extracting groups into their own named functions]:\n    /go/subtest-grouping/#extract-subtest-groups-into-functions\n\n[testing frameworks often develop into mini-languages of their own]:\n    https://go.dev/doc/faq#testing_framework",
  "title": "Subtest grouping in Go"
}