Hacker News new | past | comments | ask | show | jobs | submit login

Maintainable software projects are modeled as a dependency graph of components that encapsulate implementation details and depend on other components.

    func main
        foo, err := NewFoo()
        handle err
        bar, err := NewBar(foo)
        handle err
Given a single component, each external dependency should be injected as an interface.

    type Fooer interface{ Foo() int }
    
    func NewBar(f Fooer) (*Bar, error) { 
        ...
    }
Test components in isolation by providing mock (stub, fake, whatever, it's all meaningless) implementations of its dependencies.

    func TestBar(t *testing.T) {
        f := &mockFoo{...}
        b, err := NewBar(f)
        ...



You're giving a baby-level introduction to mocking in a thread about how this approach leads to low-quality, meaningless tests in some cases, and right beneath concrete suggestions about how to make tests better by deviating from this pattern.


I'm sorry if you've had bad experiences with this approach in the past, but it emphatically does not lead to low quality and/or meaningless tests. It's the essential foundation of well-abstracted and maintainable software.


Here's a test I wrote recently, in the style expected at my company. Tell me what exactly you think this is contributing to maintainability, or what you think could be done better. I spent an hour on this and found it pure drudgery. I half suspect I could have written a code generator for it in that hour instead. I had no idea whether the code really worked until I ran it against the real upstream.

The unit under test is a gateway to a configuration service.

https://dpaste.com/FZGC8R66K


It's hard to give solid advice based on a view of this single layer, but at a glance unless this gateway client is itself something to be extended by other projects, this is probably not something I would write test cases for per se. If "apipb" stands for protobuf, I definitely wouldn't inject a mock here but would make a real pb server listening with expectations and canned responses. (Our protobuf services have something like this available in the same package as the client, i.e. anyone using the client also has the tools to write the application-specific tests they need.)

The resulting code probably wouldn't be shorter, but it would exercise a lot more of the real code paths. The availability of a test server with expectation methods could also (IMO) improve readability. Instead of trying to model multiple variants of behavior via a single test case table, using a suite setup + methods (e.g. `s.ExpectStartTransaction(...); s.ExpectUpsert(...)`) would make clearer test bodies. Check sqlmock for something I think is a good example of a fluent expectation API in Go.


Wow! No wonder you find this tedious.

Your gateway struct hopefully looks something like

    type Fooer interface{ ... }
    type Barer interface{ ... }
    type gateway struct{ f Fooer; b Barer; ... }
    newGateway(f Fooer, b Barer, ...) (*gateway, error) { ... }
    func (g *gateway) StartTransaction(...)
That is, a gateway is something that depends on other things, modeled as interfaces, and provides capabilities as methods.

                 +--------------------+
                 | gateway            |
                 | - f Fooer          |
                 | - b Barer          |
                 | - ...              |
                 |                    |
    input -------> StartTransaction -----> output
                 | ...                |
                 +--------------------+
When you want to exercise this code, you want to construct an instance with mock/deterministic dependencies, so that you have predictable results when you apply input and receive output. That's the model: give input, assert output.

But your linked code is kind of different! Each subtest varies not the input but the behavior of the mocked dependencies. I understand the point: you want to run through all the codepaths in the gateway method. But is that worth testing? Do the tests meaningfully reduce risk? I dunno. It's not obvious to me that they do.

The use of gomock is also a big smell. Generating mocks kind of defeats the purpose of using them. I would definitely write a bespoke client:

    type mockClient struct {
     StartTransaction func(...) (xxx.Transaction, error)
     Upsert           func(...) (xxx.Result, error)
     AbortTransavtion func(...) (xxx.Xxx, error)
    }
Then each test case is simpler to express as

    for _, tc := range []struct{
     name        string
     client      *mockClient
     input       UpdateConfigRequest
     startRes    xxx.Transaction
     startErr    error
     upsertRes   xxx.Result
     upsertErr   error
     res         xxx.UpdateConfigResponse
     err         error
    } {
     {
      name:      "success",
      client:    &mockClient{StartTransaction: good, ...},
      startRes:  ...,
      upsertRes: ...,
      res:       ...,
     },
     {
      name:      "bad start",
      client:    &mockClient{StartTransaction: bad, ...},
      startErr:  ...,
     },
     {
      name:      "bad upsert",
      client:    &mockClient{StartTransaction: good, Upsert: bad, ...},
      startRes:  ...,
      upsertErr: ...,
     },
     ...
    } {
     t.Run(tc.name, func(t *testing.T) {
      g := newGateway{tc.client}
      output := g.StartTransaction(tc.input)
      // asserts
     })
    }




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: