Pretty good advice, except the preference for interfaces to support mocking. You should avoid mocking when possible in favor of real objects or fakes. Interfaces are still cool, though.
Go best practices, six years in (2016)
41–50 of 66 posts
Re: Go best practices, six years in (2016)
#42Pretty good advice, except the preference for interfaces to support mocking. You should avoid mocking when possible in favor of real objects or fakes. Interfaces are still cool, though.
This is straight-up bad advice. You don't test the http.Client as part of your business logic, even (especially) if your business logic makes HTTP requests. Instead, you trust that package http is already well-tested, mock out http.Client as an abstract HTTP Request Do-er, and you downscope your tests to your business logic exclusively.
I'm not so worried about http.Client behaving incorrectly, but I am worried about the server I'm targeting behaving in a way that's different than my expectations. I'm also worried about getting cookie, post-data, url, etc. formats wrong. Even if they match what I put in my mock, they won't necessarily match what's out there in production. I'm worried that I'll handle download resumption wrong, for example.
If I have the capability to bring up the target server locally, that's what I will do. If I can't, but it's straightforward to create a work-alike, I'll bring that up in process. Only in the most dire of circumstances will I use mocks to test my code.
Think about it this way. What harm is done by using the real object, under the circumstances I described? Sure, if the dependency code breaks, my tests will too. Is that bad? Is there any point in my tests passing when my code won't actually work in production? OK, so maybe the tests will run slower? This matters in the limit, but often times you can use the production dependency without materially increasing test time. OK but what about the CI system using more resources? That will only be a problem of significant economic import if you're running at a scale like Google or Facebook.
My opinion is there are a few main valid objections to my approach. One is an argument about the size of the test code. In some cases, bringing up the production dependency will require more code than a mock. Sometimes a lot more code. My response is that we should make it easier to bring up the production dependency! But if we can't, I can see the case for a mock or a fake. The other counterargument is for when we really are interested in testing the sequence of interactions the code under test has with the production system. In that event, I guess I'm ok with using a mock, but I think most of the time we're more interested in testing state. We can also use things like testvalue injection to test behavior sequences, which is normally a lighter-weight approach, and in some cases can allow you to reproduce paths that you'd otherwise have trouble hitting, even with a mock.
Re: Go best practices, six years in (2016)
#43Pretty good advice, except the preference for interfaces to support mocking. You should avoid mocking when possible in favor of real objects or fakes. Interfaces are still cool, though.
After reading the Martin Fowler article you linked, your comment makes more sense, but I think the terminological distinctions Fowler makes are never going to be widely adopted. People are going to keep using terms like "mock" and "stub" to broadly refer to any kind of test double, and it's disingenuous to assume otherwise.
I'm sorry, I'm making an argument about how people should write tests, not what terms they'll use to describe it. :) I promise you that there is no disingenuity going on in my use of certain terminology to describe how I think it's best to write tests.
Re: Go best practices, six years in (2016)
#44Earlier quoted context omitted.
> the functional options pattern Functional options are controversial in the Go community. I don't like them personally. The idea of having a bunch of functions that exist only to mutate internal state on init is... an odd choice. Google code is riddled with this sort of style. It's annoying to write client code for another reason: It's hard to discover. For the "term" example in the article, you can't sit in your ed…
My biggest object to the functional options argument is the step where NewServer(..., Config{}) is considered bad for some reason. As far as I can tell, the entire purported utility of the thing is based on avoiding this obvious approach.
It means that sometimes you'll have to use dummy values (-1 for "no value", for example) or pointers (nil means optional). It means that any boolean has to be phrased in such a way that false is the default (so if it feels natural to call the option UseKeychain and it should default to true, you have to invert it and call it DontUseKeychain).
Config structs also means it's easier for a caller to construct invalid configs:
type Config struct {
// Either stream or channel can be set, but not both
OutputStream io.Writer
OutputChan chan Event
}
// ...
pipelines.New(pipelines.Config{
OutputStream: myFile,
OutputChan: myChan,
})
The semantics cannot be guaranteed by the type system, so won't be caught until runtime. Neither will this, of course: // ...
pipelines.New(
pipelines.OutputStream(myFile),
pipelines.OutputChan(myChan))
...but in this case we can quite sensibly mandate that the following option overrides the previous one, flipping the internal switch over to the setting you specify. With the Config struct, there's no way for pipelines.New() to know which one should have precedence.(In this particular case, you can work around it by having a single Output field that uses an interface, but there are other situations where mutually incompatible fields can occur.)
Re: Go best practices, six years in (2016)
#45Earlier quoted context omitted.
My biggest object to the functional options argument is the step where NewServer(..., Config{}) is considered bad for some reason. As far as I can tell, the entire purported utility of the thing is based on avoiding this obvious approach.
A big config struct is great, but it comes with a big downside: You have to rely on Go's zero value semantics to provide defaults. It's not always clear that 0 or an empty string means "no value". It means that sometimes you'll have to use dummy values (-1 for "no value", for example) or pointers (nil means optional). It means that any boolean has to be phrased in such a way that false is the default (so if it feels…
Re: Go best practices, six years in (2016)
#46Earlier quoted context omitted.
This is straight-up bad advice. You don't test the http.Client as part of your business logic, even (especially) if your business logic makes HTTP requests. Instead, you trust that package http is already well-tested, mock out http.Client as an abstract HTTP Request Do-er, and you downscope your tests to your business logic exclusively.
Lots of people have different opinions on this. You're entitled to yours, but I think it's hasty to call it bad advice. I'm not so worried about http.Client behaving incorrectly, but I am worried about the server I'm targeting behaving in a way that's different than my expectations. I'm also worried about getting cookie, post-data, url, etc. formats wrong. Even if they match what I put in my mock, they won't necessar…
Re: Go best practices, six years in (2016)
#47At [Kolide]( https://kolide.com/ ) we're heavy users of Go Kit, and as a result have also adopted a lot of the style Peter recommends here. We've been slowly expanding on it with a style guide and company specific set of common libraries [here]( https://github.com/kolide/kit#kolide-kit ). My coworker also wrote a [blog post]( https://blog.kolide.com/using-go-for-scalable-operating-syst... ) on how Go has been fantast…
I feel like go-kit is quite antithetical to the Go mindset...it presents a lossy abstraction as a means of future-proofing against eventualities that will almost certainly never be encountered to be honest it strikes me as the sort of library that excites intermediate developers who tend to over-architect
Re: Go best practices, six years in (2016)
#48Earlier quoted context omitted.
This is straight-up bad advice. You don't test the http.Client as part of your business logic, even (especially) if your business logic makes HTTP requests. Instead, you trust that package http is already well-tested, mock out http.Client as an abstract HTTP Request Do-er, and you downscope your tests to your business logic exclusively.
Lots of people have different opinions on this. You're entitled to yours, but I think it's hasty to call it bad advice. I'm not so worried about http.Client behaving incorrectly, but I am worried about the server I'm targeting behaving in a way that's different than my expectations. I'm also worried about getting cookie, post-data, url, etc. formats wrong. Even if they match what I put in my mock, they won't necessar…
This is the responsibility of system or integration tests, not unit tests. (Unit tests are what's under discussion in the article.)
> What harm is done by using the real object, under the circumstances I described?
Time, energy, and effort, all of which inevitably lead to longer (slower) dev/test cycles.
Re: Go best practices, six years in (2016)
#49At [Kolide]( https://kolide.com/ ) we're heavy users of Go Kit, and as a result have also adopted a lot of the style Peter recommends here. We've been slowly expanding on it with a style guide and company specific set of common libraries [here]( https://github.com/kolide/kit#kolide-kit ). My coworker also wrote a [blog post]( https://blog.kolide.com/using-go-for-scalable-operating-syst... ) on how Go has been fantast…
I feel like go-kit is quite antithetical to the Go mindset...it presents a lossy abstraction as a means of future-proofing against eventualities that will almost certainly never be encountered to be honest it strikes me as the sort of library that excites intermediate developers who tend to over-architect
Re: Go best practices, six years in (2016)
#50Earlier quoted context omitted.
A big config struct is great, but it comes with a big downside: You have to rely on Go's zero value semantics to provide defaults. It's not always clear that 0 or an empty string means "no value". It means that sometimes you'll have to use dummy values (-1 for "no value", for example) or pointers (nil means optional). It means that any boolean has to be phrased in such a way that false is the default (so if it feels…
Yep! It was probably overstepping on my part to say that the entire benefit depends on that argument. I should instead have said that the tradeoff does not seem good to me.
For stuff that needs finer-grained control, or long-lived API stability, functional options give you a lot more flexibility.