{"id":19634922,"url":"https://github.com/rikonor/stubby-mcmockerface","last_synced_at":"2025-04-28T07:31:53.223Z","repository":{"id":72775114,"uuid":"94943249","full_name":"rikonor/stubby-mcmockerface","owner":"rikonor","description":"A useful pattern for testing and other things","archived":false,"fork":false,"pushed_at":"2017-06-20T23:29:21.000Z","size":8,"stargazers_count":10,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-05T07:41:38.250Z","etag":null,"topics":["dependency-injection","golang","mocking","testing"],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rikonor.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2017-06-20T23:23:44.000Z","updated_at":"2017-07-06T20:55:41.000Z","dependencies_parsed_at":"2023-02-28T00:31:31.539Z","dependency_job_id":null,"html_url":"https://github.com/rikonor/stubby-mcmockerface","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rikonor%2Fstubby-mcmockerface","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rikonor%2Fstubby-mcmockerface/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rikonor%2Fstubby-mcmockerface/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rikonor%2Fstubby-mcmockerface/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rikonor","download_url":"https://codeload.github.com/rikonor/stubby-mcmockerface/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251271284,"owners_count":21562519,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dependency-injection","golang","mocking","testing"],"created_at":"2024-11-11T12:23:13.527Z","updated_at":"2025-04-28T07:31:52.156Z","avatar_url":"https://github.com/rikonor.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"Stubby McMockerface\n---\n\nThe goal of this document is to make a case for using Go interfaces for:\n* dependency injection (DI)\n* Easily mocking interfaces\n* Augmenting an existing instance of an interface\n\n### 1. Original Motivation - Dependency Injection (DI)\n\nDependency Injection is a Software Development technique whereby one object supplies the dependencies of another object. This is in contrast to the object building or finding it's dependencies from some global scope.\n\nLets look at a simple example.\nWe have a person that would like to introduce himself.\n\n```go\ntype Person struct {\n  Name string\n}\n\nfunc (p *Person) IntroduceYourself() {\n  fmt.Println(\"Hi, my name is \" + p.Name + \".\")\n}\n```\n\nWe actually want to support different kinds of people, e.g a loud person, a normal person and perhaps a mute person.\n\n```go\n// say is how a normal person would speak\nfunc say(msg string) {\n  fmt.Println(msg)\n}\n\n// sayLoud is how a loud person would speak\nfunc sayLoud(msg string) {\n  fmt.Println(strings.ToUpper(msg))\n}\n\n// sayMute is how a mute person would speak\nfunc sayMute(msg string) {\n  // Do nothing because you're mute\n}\n```\n\nIn this case it might be a good idea us to allow Person to have a Say function injected into it, rather then having one tightly coupled implementation.\n\n```go\n// Define a type for Say functions\ntype SayFunc func(msg string)\n\n// Make sure Person allows us to specify a Say function for it to use\ntype Person struct {\n  ...\n  SayFn SayFunc\n}\n\n// Make the person use the injected SayFn to introduce itself\nfunc (p *Person) IntroduceYourself() {\n  p.SayFn(\"Hi, my name is \" + p.Name + \".\")\n}\n```\n\n```go\np := \u0026Person{\n  Name: \"Kip\",\n  SayFn: SayFunc(sayLoud), // Inject a Say function of our choice\n}\n\np.IntroduceYourself()\n\n// Output: HI, MY NAME IS KIP.\n```\n\n### 2. Mocking via DI\n\nA common issue when testing software is dealing with external services that our code depends on.\nHaving to depend on the availability of these services can complicate tests significantly.\nA few examples are disk resources, network resources, databases, API libraries and more.\n\nLet's examine how to solve this issue by mocking a service and providing the mocked version via dependency injection.\nIn this way, the code receiving the injected dependency doesn't even know that it did not receive the real service.\n\n##### Example - `http.Client`\n\nFor our use-case we will examine `http.Client` which is a struct from the `net/http` standard Go lib which allows us to make network requests. The way we make a network request is by building an `http.Request` first and then providing it to the client to perform.\n\n```go\n// Make a GET request to `url`\nreq, err := http.NewRequest(\"GET\", url, nil)\n\n// Perform the request\nres, err := http.DefaultClient.Do(req)\n```\n\n`http.Client` does not satisfy any specific interface out of the box, but that doesn't mean we can't create an interface to match it.\n\n```go\n// HTTPClient is a general interface for http clients\n// Coincidentally, it is implemented by http.Client\n// If we want to be more idiomatic it can also be named Doer\ntype HTTPClient interface {\n\tDo(req *http.Request) (*http.Response, error)\n}\n```\n\nNow we'll see a pattern that can be used to very generically mock an interface.\n\n```go\n// MockHTTPClient is a mockable HTTPClient\ntype MockHTTPClient struct {\n\tDoFn func(req *http.Request) (*http.Response, error)\n}\n\n// Do calls the underlying Do method\nfunc (c *MockHTTPClient) Do(req *http.Request) (*http.Response, error) {\n\treturn c.DoFn(req)\n}\n```\n\n`MockHTTPClient` takes in a generic `DoFn`, so we can create many different mock clients.\n\n```go\n// FromString returns an HTTPClient which always returns a response with the given string\nfunc FromString(s string) HTTPClient {\n\treturn \u0026MockHTTPClient{\n\t\tDoFn: func(req *http.Request) (*http.Response, error) {\n\t\t\t// convert the given string to a ReadCloser (same as Response.Body)\n\t\t\tbody := ioutil.NopCloser(strings.NewReader(s))\n\n\t\t\t// Just return a response with the given string\n\t\t\treturn \u0026http.Response{\n\t\t\t\tBody: body,\n\n\t\t\t\t// Can mock other fields as well: StatusCode, etc\n\t\t\t}, nil\n\t\t},\n\t}\n}\n```\n\nNotice there are lots of options for mocking `HTTPClient`: `FromStatusCode`, `FromCookies`, `FromHeaders`, etc. We can also just create a `MockHTTPClient` on the fly with some special custom logic.\n\nLet's say we have a function `FetchSomeNetworkResources`, which accepts an `HTTPClient`.\nAlthough, under normal circumstances we would give it an `http.Client`, during tests we can just give it one of our mock clients.\n\n```go\nc := FromString(\"data: boop\")\n\ndata, err := FetchSomeNetworkResources(c)\nif data != \"boop\" {\n  t.Fatal(...) // fail the test\n}\n```\n\nWe can follow the same pattern to mock any interface.\n\n### 3. Enhancing existing interfaces\n\nAnother cool aspect of this pattern is that it can be used for much more then just mocking.\nNote: It's been debated that in the context of \"enhancing/augmenting\" `StubXXX` is more appropriate then `MockXXX`. That said, we will keep using `MockXXX` for the sake of demonstration.\n\n##### Example - `RetryHTTPClient`\n\n```go\n// RetryHTTPClient wraps an HTTPClient with retry functionality\nfunc RetryHTTPClient(c HTTPClient, retries int) HTTPClient {\n\treturn \u0026MockHTTPClient{\n\t\tDoFn: func(req *http.Request) (*http.Response, error) {\n\t\t\tvar res *http.Response\n\t\t\tvar err error\n\n\t\t\t// try `retries` times\n\t\t\tfor i := 0; i \u003c retries; i++ {\n\t\t\t\t// attempt the request\n\t\t\t\tres, err = c.Do(req)\n\t\t\t\tif err != nil {\n\t\t\t\t\t// retry on failure\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\treturn res, nil\n\t\t\t}\n\n\t\t\t// we made `retries` attempts and never succeeded\n\t\t\treturn nil, err\n\t\t},\n\t}\n}\n```\n\n##### Example - `RewriteHostHTTPClient`\n\n```go\n// RewriteHostHTTPClient will rewrite the host of any request passing through it\nfunc RewriteHostHTTPClient(c HTTPClient, host string) HTTPClient {\n\treturn \u0026MockHTTPClient{\n\t\tDoFn: func(req *http.Request) (*http.Response, error) {\n\t\t\t// Rewrite the Host portion of the request\n\t\t\treq.Host = host\n\t\t\treq.URL.Host = host\n\n\t\t\t// Send the request\n\t\t\treturn c.Do(req)\n\t\t},\n\t}\n}\n```\n\n##### Example - `Publisher` and friends\n\nLet's define a `Publisher` interface.\n\n```go\ntype Publisher interface {\n  Publish(msg Message) error\n}\n```\n\nAnd a generic mock publisher.\n\n```go\ntype MockPublisher struct {\n  PublishFn func(msg Message) error\n}\n\nfunc (p *MockPublisher) Publish(msg Message) error {\n  return p.PublishFn(msg)\n}\n```\n\n##### `TransformPublisher`\n\n_Definition_\n\n```go\n// TransformFunc is a function that changes a message and returns the changed version\ntype TransformFunc func(msg string) string\n\n// TransformPublisher wraps a given Publisher with a message TransformFunc\nfunc TransformPublisher(p Publisher, tfn TransformFunc) Publisher {\n\treturn \u0026MockPublisher{\n\t\tPublishFn: func(msg string) error {\n\t\t\t// transform the message using the given transform function, then send it along\n\t\t\treturn p.Publish(tfn(msg))\n\t\t},\n\t}\n}\n```\n\n_Usage_\n\n```go\n// Lets try and create a Publisher that will transform our messages before sending them out\ntp := TransformPublisher(p, func(msg string) string {\n  // as an example, lets capitalize the message\n  return strings.Title(msg)\n})\n\n// Should publish: \"Hello\"\ntp.Publish(\"hello\")\n```\n\n##### `MultiPublisher`\n\n_Definition_\n\n```go\n// MultiPublisher wraps all given Publishers into one Publisher\nfunc MultiPublisher(ps ...Publisher) Publisher {\n\treturn \u0026MockPublisher{\n\t\tPublishFn: func(msg string) error {\n\t\t\t// iterate over all publishers and send to each in turn\n\t\t\tfor _, p := range ps {\n\t\t\t\t// there's multiple possible error handling strategies here\n\t\t\t\t// in this case we'll just return the first encountered error\n\t\t\t\tif err := p.Publish(msg); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}\n}\n```\n\n_Usage_\n\n```go\nmp := MultiPublisher(p1, p2, p3)\n\n// Should publish to `p1`, `p2` and `p3`\nmp.Publish(\"hello\")\n```\n\n##### `BatchPublisher`\n\n_Definition_\n\n```go\n// BatchPublisher batches messages together before sending them out\nfunc BatchPublisher(p Publisher, batchSize int) Publisher {\n\t// hold our batched msgs somewhere\n\tmsgs := []string{}\n\n\treturn \u0026MockPublisher{\n\t\tPublishFn: func(msg string) error {\n\t\t\tmsgs = append(msgs, msg)\n\n\t\t\t// if enough messages have been batched, we can send them out\n\t\t\tif len(msgs) == batchSize {\n\t\t\t\t// there's multiple ways to batch the messages\n\t\t\t\t// in this case we'll just concatenate them\n\t\t\t\tbatchMsg := strings.Join(msgs, \",\")\n\t\t\t\treturn p.Publish(batchMsg)\n\t\t\t}\n\n\t\t\t// Note: It's also possible to flush the batch publisher after some pre-defined time duration\n\t\t\t// but to keep the example simple we will not do so\n\n\t\t\t// still waiting for batch buffer to fill up\n\t\t\treturn nil\n\t\t},\n\t}\n}\n```\n\n_Usage_\n\n```go\nbp := BatchPublisher(p, 3)\n\nbp.Publish(msg) // Won't publish yet\nbp.Publish(msg) // Won't publish yet\nbp.Publish(msg) // Will publish all three now\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frikonor%2Fstubby-mcmockerface","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frikonor%2Fstubby-mcmockerface","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frikonor%2Fstubby-mcmockerface/lists"}