This sounds like dependency injection of test behavior as a substitute for the “real” IO implementations injected in whatever glue code accesses the unit under test. If that’s a correct understanding, it sounds like mocking by another name?
Not that I think there’s anything necessarily wrong with that. And I think it that kind of inversion of control can often produce more robust designs/systems/tests. But I think it’s a good idea to recognize that’s what it is, and that it has similar limitations to other mocking techniques.
type example struct {
backend db.DB // Never actually used the database library, so this MAY not be the right type name
...
}
Then your code uses that to call the methods on var.backend, and you can replace it with a test instance. This feels approximately like dependency injection, maybe? Or maybe just encapsulation?And the other is that you pass your "IO" as a parameter. A pretty typical example would be fmt.Fprintf.
func Fprintf(out io.Writer, format string, args ...interface{}) (n int, e error)
It is of course limited, but all testing is limited. But, most "mocking" I have seen replaces the original type while the test is ongoing, and that specifically is pretty hard in Go. It is trivial to pass in a parameter, or set a struct slot, to something that fulfils a specific interface. And it is often quite useful. And, again, of course something that you may have to write tests for, to ensure that it does what you intend it to (especially if it is complex and needs to hold some state, which unfortunately sometimes happens).The boilerplate propagation is the point in Go.
Go isn't DRY. Go would rather copy and paste a bunch of code than introduce the "complexity" of inheritance.
Do you not do this? What do you do instead? I guess I can see how testing would be less painful with fewer layers, but at the cost of the production code becoming more entangled.
What's different is that the "model" is often e.g. a bare sql.DB and not an object repository, and the "view" is a template called directly by the controller, but "MVC" - even this kind, which isn't the original kind - doesn't have to be an auto-wired DI interface-laden mess. PHP and Java just made it that way.
But if you'd like I'd be happy to take a look at some of the problems you're encountering. I'm @alecthomas on the Gophers Slack or Twitter, feel free to DM me. No guarantees, but it sounds very unusual for an additional test to consume 15 minutes of setup.
I'm not sure that Go pushes you to duplicate code... Do you mean copy a slightly tweak code for different use cases and types ?
This doesn't seem like a go specific issue but an engineering one.
Java and Python mocking is also a lot more lightweight; you don’t have to generate mocks ahead of using them, regenerate them when the interface changes or work generation into your build process, etc. Richer reflection APIs make it a pretty casual handful of characters to mock something out.
If reliability is critical, having explicit error handling and coverage tools which expose error conditions which haven't been tested is very helpful.
Having to check this in the particular case of every call at every later underlying every handler, does not make my software more reliable in than when it is simply guaranteed in general.
This isn't theoretical. I review a lot of Python code and I would say in over 20% of cases I see a try block with a `finally` longer than two lines, it mistakenly uses a variable that might not be set when an exception is thrown earlier than the writer expected.
You don't need to do this in Go either. Embed the interface type in your mock type. You only need to implement functions that will be called in the function under test.
If you're writing tests to improve coverage numbers then maybe you're motivation is wrong.
In most languages with automatic exception propagation you never know what exceptions can be thrown. Is it any different from not testing?
- Don't unit test so much (or expand your definition of "unit", or whatever semantic difference you prefer). Test functionality. (Unit tests can still be appropriate for large classes of complicated pure functions, e.g. parsers, but these don't require mocks.)
- Don't mock so much; rather, stub (or mock if absolutely necessary) at lower levels. Use the httptest server; use the sqlmock or sqlite drivers; use a net.Conn with canned data; etc.
(I should add that this is explicitly contra to e.g sethammons's suggestion above, which seems to be relatively common in the part of the Go community that come from PHP. I inherited a couple large projects that did this. Today they use sqlite instead, and both the program and test code is ~50% the size it used to be.)
For us, stub injection points come naturally out of 12-factor-style application design; the program can already configure the address of the 2-3 other things it needs to talk to or files it needs to output, etc, just out of our need for manual testing or staging vs. production environments. If you have technical leadership encouraging Spring-but-in-Go, you'll probably hit a wall here too though.
It's also possible you're simply writing too many functions that can return errors. Over-complex code makes over-complex tests; always think about whether you're handling an error or a programming mistake - if the latter, panic instead of returning.
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)
...The unit under test is a gateway to a configuration service.
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
})
}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.