HN user

sagichmal

2,816 karma

e1b1ce94085da63902d7c51d8f1e0697

Posts5
Comments1,038
View on HN

I’m not sure why you think people who care about justice draw that energy from the same pool that motivates their work, as if it’s a zero-sum calculus. It isn’t. People who think about things systemically tend to apply that perspective in all of their practices. And that’s the value.

My point is that DEBUG level logging is (hopefully!) not on by default, and that this is what it makes the production log volume manageable.

My experience has been that 1 customer-facing byte tends to generate something like ~10 DEBUG-level telemetry bytes. This level of request amplification can't be feasibly sustained at nontrivial request volumes, your logging infrastructure would dwarf your production infrastructure.

Effective Go 5 years ago

When I read MVC I guess I take it pretty literally, in that you would have packages named model/s, view/s, and controller/s. That's what I'm speaking to. Of course, abstractly, many programs tend to be structured in some kind of layering scheme that's MVC-ish :)

You continue to assert that error handling adds noise to a block of code. This is an opinion, that's fine, but it's not an objective truth. In many many domains -- in fact the domains which Go targets -- error handling is equally important to business logic.

Effective Go 5 years ago

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
     })
    }

Is there any theoretical groundwork happening on how CRDTs can preserve domain semantics?

Effectively using CRDTs requires a fundamentally different model of state. Typical CRUD stuff simply doesn't work. If you're unwilling or unable to refactor your state layer, then CRDTs are gonna seem totally infeasible.

Effective Go 5 years ago

I don't understand why you would reply with this comment. If the question is "how do I do X?" and X isn't something that you should be doing, why is it not constructive to point that out?

Effective Go 5 years ago

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.

Effective Go 5 years ago

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)
        ...

Because when the author writes or speaks about the project, they consistently demonstrate a fundamental lack of understanding of the domain. There is a reason that GunDB is never mentioned in literature or papers.