| ▲ | et1337 2 days ago |
| At $WORK we have taken interface segregation to the extreme. For example, say we have a data access object that gets consumed by many different packages. Rather than defining a single interface and mock on the producer side that can be reused by all these packages, each package defines its own minimal interface containing only the methods it needs, and a corresponding mock. This makes it extremely difficult to trace the execution flow, and turns a simple function signature change into an hour-long ordeal of regenerating mocks. |
|
| ▲ | leetrout 2 days ago | parent | next [-] |
| > a single interface and mock on the producer side I still believe in Go it is better to _start_ with interfaces on the consumer and focus on "what you need" with interfaces instead of "what you provide" since there's no "implements" concept. I get the mock argument all the time for having producer interfaces and I don't deny at a certain scale it makes sense but I don't understand why so many people reach for it out of the gate. I'm genuinely curious if you have felt the pain from interfaces on the producer that would go away if there were just (multiple?) concrete types in use or if you happen to have a notion of OO in Go that is hard to let go of? |
| |
| ▲ | mekoka 2 days ago | parent [-] | | > or if you happen to have a notion of OO in Go that is hard to let go of? So much this. I think Go's interfaces are widely misunderstood. Often times when they're complained about, it boils down to "<old OO language> did interface this way. Why Go won't abide?" There's insistence in turning them into cherished pets. Vastly more treasured than they ought to be in Go, a meaningless thin paper wrapper that says "I require these behaviors". |
|
|
| ▲ | eximius 2 days ago | parent | prev | next [-] |
| > Rather than defining a single interface and mock on the producer side that can be reused by all these packages This is the answer. The domain that exports the API should also provide a high fidelity test double that is a fake/in memory implementation (not a mock!) that all internal downstream consumers can use. New method on the interface (or behavioral change to existing methods)? Update the fake in the same change (you have to, otherwise the fake won't meet the interface and uses won't compile!), and your build system can run all tests that use it. |
| |
| ▲ | 9rx 2 days ago | parent [-] | | > The domain that exports the API should also provide a high fidelity test double that is a fake/in memory implementation (not a mock!) Not a mock? But that's exactly what a mock is: An implementation that isn't authentic, but that doesn't try to deceive. In other words, something that behaves just like the "real thing" (to the extent that matters), but is not authentically the "real thing". Hence the name. | | |
| ▲ | B-Con 2 days ago | parent [-] | | There are different definitions of the term "mock". You described the generic usage where "mock" is a catch-all for "not the real thing", but there are several terms in this space to refer to more precise concepts. What I've seen: * "test double" - a catch-all term for "not the real thing". What you called a "mock". But this phrasing is more general so the term "mock" can be used elsewhere. * "fake" - a simplified implementation, complex enough to mimic real behavior. It probably uses a lot of the real thing under the hood, but with unnecessary testing-related features removed. ie: a real database that only runs in memory. * "stub" - a very thin shim that only provides look-up style responses. Basically a map of which inputs produce which outputs. * "mock" - an object that has expectations about how it is to be used. It encodes some test logic itself. The Go ecosystem seems to prefer avoiding test objects that encode expectations about how they are used and the community uses the term "mock" specifically to refer to that. This is why you hear "don't use mocks in Go". It refers to a specific type of test double. By these definitions, OP was referring to a "fake". And I agree with OP that there is much benefit to providing canonical test fakes, so long as you don't lock users into only using your test fake because it will fall short of someone's needs at some point. Unfortunately there's no authoritative source for these terms (that I'm aware of), so there's always arguing about what exactly words mean. Martin Fowler's definitions are closely aligned with the Go community I'm familiar with: https://martinfowler.com/articles/mocksArentStubs.html Wikipedia has chosen to cite him as well: https://en.wikipedia.org/wiki/Test_double#General . My best guess is that software development co-opted the term "mock" from the vocabulary of other fields, and the folks who were into formalities used the term for a more specific definition, but the software dev discipline doesn't follow much formal vocabulary and a healthy portion of devs intuitively use the term "mock" generically. (I myself was in the field for years before I encountered any formal vocabulary on the topic.) | | |
| ▲ | 9rx 2 days ago | parent [-] | | > "mock" - an object that has expectations about how it is to be used. It encodes some test logic itself.* Something doesn't add up. Your link claims that mock originated from XP/TDD, but mock as you describe here violates the core principles of TDD. It also doesn't fit the general definition of mock, whereas what you described originally does. Beck seemed to describe a mock as something that: 1. Imitates the real object. 2. Records how it is used. 3. Allows you to assert expectations on it. #2 and #3 sound much like what is sometimes referred to as a "spy". This does not speak to the test logic being in the object itself. But spies do not satisfy #1. So it is seems clear that what Beck was thinking of is more like, say, an in-memory database implementation where it: 1. Behaves like a storage-backed database. 2. Records changes in state. (e.g. update record) 3. Allows you to make assertions on that change in state. (e.g. fetch record and assert it has changed) I'm quite sure Fowler's got it wrong here. He admits to being wrong about it before, so the odds are that he still is. The compounding evidence is not in his favour. Certainly if anyone used what you call a mock in their code you'd mock (as in make fun of) them for doing so. It is not a good idea. But I'm not sure that equates to the pattern itself also being called a mock. |
|
|
|
|
| ▲ | the_gipsy 2 days ago | parent | prev | next [-] |
| Yes, this is exactly the problem with go's recipe. Either you copypaste the same interface over and over and over, with the maintenance nightmare that is, or you always have these struct-and-interface pairs, where it's unclear why there is an interface to begin with. If the answer is testing, maybe that's the wrong question ti begin with. So, I would rather have duck typing (the structural kind, not just interfaces) for easy testing. I wonder if it would technically be possible to only compile with duck typing in test, in a hypothetical language. |
| |
| ▲ | 9rx 2 days ago | parent [-] | | > I wonder if it would technically be possible to only compile with duck typing in test Not exactly the same thing, but you can use build tags to compile with a different implementation for a concrete type while under test. Sounds like a serious case of overthinking it, though. The places where you will justifiably swap implementations during testing are also places where you will justifiably want to be able to swap implementations in general. That's what interfaces are there for. If you cannot find any reason why you'd benefit from a second implementation outside of the testing scenario, you won't need it while under test either. In that case, learn how to test properly and use the single implementation you already have under all scenarios. | | |
| ▲ | the_gipsy 2 days ago | parent [-] | | > The places where you will justifiably swap implementations during testing are also places where you will justifiably want to be able to swap implementations in general. I don't get this. Just because I want to mock something doesn't mean I really need different implementations. That was my point: if I could just duck-type-swap it in a test, it would be so much easier than 1. create an interface that just repeats all methods, and then 2. need to use some mock generation tool. If I don't mock it, then my tests become integration test behemoths. Which have their use too, but it's bad if you can't write simple unit tests anymore. | | |
| ▲ | 9rx 2 days ago | parent [-] | | > then my tests become integration test behemoths. There are no consistent definitions found in the world of testing, but I assume integration here means entry into some kind of third-party system that you don't have immediate control over? That seems to be how it is most commonly used. And that's exactly one of the places you'd benefit from enabling multiple implementations, even if testing wasn't in the picture. There are many reasons why you don't want to couple your application to these integrations. The benefits found under test are a manifestation of the very same, not some unique situation. | | |
| ▲ | the_gipsy 2 days ago | parent [-] | | Not really. Sometimes you just want to mock some bigger system that is still internal/local. And sometimes it is an external system, but it makes no sense to wrap some sdk in yet another layer, if you won't ever swap it out. | | |
| ▲ | 9rx 2 days ago | parent [-] | | > Sometimes you just want to mock some bigger system that is still internal/local. What for? |
|
|
|
|
|
|
| ▲ | Groxx 2 days ago | parent | prev | next [-] |
| I 100% agree with what you've written, but if you haven't checked it out, I'll highly suggest trying mockery v3 for mocks: https://vektra.github.io/mockery It's generally faster than a build (no linking steps), regardless of the number of things to generate, because it loads types just once and generates everything needed from that. Wildly better than the go:generate based ones. |
| |
|
| ▲ | Xeoncross 2 days ago | parent | prev | next [-] |
| What is the alternative though? In strongly typed languages like Go, Rust, etc.. you must define the contract. So you either focus on what you need, or you just make a kitchen-sink interface. I don't even want to think about the global or runtime rewriting that is possible (common) in Java and JavaScript as a reasonable solution to this DI problem. |
| |
| ▲ | jerf 2 days ago | parent [-] | | I'm still fiddling with this so I haven't seen it at scale yet, but in some code I'm writing now, I have a centralized repository for services that register themselves. There is a struct that will provide the union of all possible subservices that they may require (logging, caching, db, etc.). The service registers a function with the central repository that can take that object, but can also take an interface that it defines with just a subset of the values. This uses reflect and is nominally checked at run time, but over time more and more I am distinguishing between a runtime check that runs arbitrarily often over the execution of a program, and one that runs in an init phase. I have a command-line option on the main executable that runs the initialization without actually starting any services up, so even though it's a run-time panic if a service misregisters itself, it's caught at commit time in my pre-commit hook. (I am also moving towards worrying less about what is necessarily caught at "compile time" and what is caught at commit time, which opens up some possibilities in any language.) The central service module also defines some convenient one-method interfaces that the services can use, so one service may look like: type myDependencies interface {
services.UsesDB
services.UsesLogging
}
func init() {
services.Register(func(in myDependencies) error {
// init here
}
}
and another may have type myDependencies interface {
services.UsesLogging
services.UsesCaching
services.UsesWebCrawler
}
// func init() { etc. }
and in this way, each services declaring its own dependencies means each service's test cases only need to worry about what it actually uses, and the interfaces don't pollute anything else. This fully decouples "the set of services I'm providing from my modules" from "the services each module requires", and while I don't get compile-time checking that a module's service requirements are satisfied, I can easily get commit-time checking.I also have some default fakes that things can use, but they're not necessary. They're just one convenient implementation for testing if you need them. | | |
| ▲ | Groxx 2 days ago | parent [-] | | tbh this sounds pretty similar to go.uber.org/fx (or dig). or really almost any dependency injection framework, though e.g. wire is compile-time validated rather than run-time (and thus much harder for some kinds of runtime flexibility - I make no claim to one being better than the other). DI frameworks, when they're not gigantic monstrosities like in Java, are pretty great. | | |
| ▲ | jerf 2 days ago | parent [-] | | Yes. The nice thing about this is that it's one function, about 20-30 lines, rather than a "framework". I've been operating up to this point without this structure in a fairly similar manner, and it has worked fine in the tens-of-thousands-of-lines range. I can see maybe another order or two up I'd need more structure, but people really badly underestimate the costs of these massive frameworks, IMHO, and also often fail to understand that the value proposition of these frameworks often just boils down to something that could fit comfortably in the aforementioned 20-30 lines. | | |
| ▲ | Groxx 2 days ago | parent [-] | | yeah, if it's only 20-30 lines then it's likely overkill to do any way except by hand. most of the stuff I've done has involved at least 20-30 libraries, many of which have other dependencies and config, so it's on the order of hundreds or thousands of lines if written by hand. it's totally worth a (simple) DI tool at that point. |
|
|
|
|
|
| ▲ | wizhi 2 days ago | parent | prev [-] |
| Maybe your actual issue is needing to mock stuff for tests to begin with. Break them down further so they can actually be tested in isolation instead. |