Skip to content

proposal: table test guidance is too prescriptive #173

Closed
@tyler-french

Description

@tyler-french

Table tests are great for reducing repetition, but as the SUT becomes more complex, they become harder to read, maintain, and debug.

Making tests harder to read and maintain at scale negatively impacts velocity, productivity, and overall sentiment on testing.

One common pattern I notice is with logical branches based on variables like shouldError, shouldCallExecutor, shouldDoXYZ. This creates an extremely difficult-to-read set of tests. As an exmple, consider the following test:

tests := []struct{
  give     string
  wantHost string
  wantPort string
  wantErr error
  shouldCallXYZ bool
  giveXYZResponse string
}{
  // ...
}

for _, tt := range tests {
  t.Run( ... ) {
    err := DoThing(tt.stuff)
    if tt.wantErr != nil {
      require.EqualError(t, err, tt.wantErr)
    }
    require.NoError(t, err)
    if tt.shouldCallXYZ{
      xyzMock.EXPECT().Call(...).Return(tt.giveXYZResponse, nil)
    }
  })
}

These tests are confusing. It's hard to tell what's going to be checked in which conditions, and unclear when certain fields of the tests struct are even used. The reader has to reference logic within the tests structs, as well as the complex behavioral logic within the test loop to understand what's happening.

Consider again that I need to add another subsystem to the service that has a mock which is called only some of the time. This adds another layer of complexity to the table test, and potentially obstructs all previously written parts of the table.

Having the guidance push to use Table Tests for "repeated patterns" actually can negatively impact code quality. Rather than recommending Table Tests for such patterns, we should recommend using Table tests for situations when ONLY the inputs and outputs of the system differ. It would also be good to recommend to AVOID Table Tests with logic existing inside the test loop.

Aside:
Somewhat unrelated to style, but it's also harder to maintain tests at scale with the dynamic assignment of subtest names. Splitting up table tests more granularly into separate TestXXX functions makes it easier to keep track of the speed and reliability of tests.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions