sagichmal
Somewhat formalized: https://peter.bourgon.org/blog/2018/08/22/observability-sign...
They lost close to half of their engineering team, significant percentages of their operations teams, and many senior leaders.
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.
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
})
}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.
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)
...