Original implementation:
type SingleItem struct { Field string `json:"field"` Hour int `json:"hour"` Minute int `json:"minute"` ItemCode string `json:"item_code"` Price float64 `json:"price"` Quantity int `json:"qty"` } type RawItems struct { Items []SingleItem `json:"items"` TotalRecordCount int `json:"total_record_count"` Start int `json:"start"` } type Calculator struct{} func (c *Calculator) SomeComplexAggregationFunction(startDate, endDate time.Time, field string) (float64, error) { convertedStartTime := startDate.Format("2006-02-01") convertedEndTime := startDate.Format("2006-02-01") rawResp, err := http.Get(fmt.Sprintf("http://example-data-server/api/data-archive/v1/retail?field=%v&start-date=%v&end-date=%v", field, convertedStartTime, convertedEndTime)) if err != nil { return 0.0, err } if rawResp.StatusCode != http.StatusOK { return 0.0, fmt.Errorf("unexpected status code") } raw, err := io.ReadAll(rawResp.Body) if err != nil { return 0.0, err } var items RawItems err = json.Unmarshal(raw, &items) if err != nil { return 0, err } // Pretend this is some complex calculation summer := 0.0 for k, v := range items.Items { fmt.Printf("processing current item: %v", k) summer = float64(v.Quantity)*v.Price + summer } return summer, nil }
Refactored implementation:
type V1InternalEndpoint struct{} func (e *V1InternalEndpoint) Retrieve(startDate, endDate time.Time, field string) ([]SingleItem, error) { convertedStartTime := startDate.Format("2006-02-01") convertedEndTime := startDate.Format("2006-02-01") rawResp, err := http.Get(fmt.Sprintf("http://example-data-server/api/data-archive/v1/retail?field=%v&start-date=%v&end-date=%v", field, convertedStartTime, convertedEndTime)) if err != nil { return []SingleItem{}, err } if rawResp.StatusCode != http.StatusOK { return []SingleItem{}, fmt.Errorf("unexpected status code") } raw, err := io.ReadAll(rawResp.Body) if err != nil { return []SingleItem{}, err } var items RawItems err = json.Unmarshal(raw, &items) if err != nil { return []SingleItem{}, err } return items.Items, nil } type DataRetriever interface { Retrieve(startDate, endDate time.Time, field string) ([]SingleItem, error) } type Calculator struct { d DataRetriever } func (c *Calculator) SomeComplexAggregationFunction(startDate, endDate time.Time, field string) (float64, error) { items, err := c.d.Retrieve(startDate, endDate, field) if err != nil { return 0, err } // Pretend this is some complex calculation summer := 0.0 for k, v := range items { fmt.Printf("processing current item: %v", k) summer = float64(v.Quantity)*v.Price + summer } return summer, nil }
Notice how much simpler the function becomes as we move the external call out of the function where we implement the logic which would contain our main logic. The call to retrieve data could be a real piece of code that actually does external calls and retrieve data via JSON or thrift, or GRPC protocols. It could also be a piece of code that provides fake data that we could test our application against.
Seeing that our code now relies on DataRetriever interface, we would need to build our mock against it and ensure that it follows that function signature.
type FakeDataRetriever struct{} func (f *FakeDataRetriever) Retrieve(startDate, endDate time.Time, field string) ([]SingleItem, error) { if field == "receipts" { return []SingleItem{SingleItem{Price: 1.1, Quantity: 2}}, nil } return []SingleItem{}, fmt.Errorf("no data available") }
We can specify what kind of data that might be returned from the external calls:
• Maybe an array of 1,000 items could be returned
• Maybe include invalid data
You can probably extend this concept further, such as database calls; application calls to queue systems such as Kafka and Nats; or application calls to caches such as Redis, and so on. All of this can be mocked and have unit tests be run against the logic that we write up—it is just that it takes a bit of effort to maintain such mocking code.