Testing a HTTP endpoint that handles a form submission of multipart/form-data in Go - unit-testing

I am creating an API endpoint to handle form submissions.
The form takes the following:
Name
Email
Phone
Photo files (up to 5)
Then basically, sends an email to some email address with the photos as attachments.
I want to write tests for my handler to make sure everything is working well, however, I am struggling.
CODE:
Below is my HTTP handler (will run in AWS lambda, not that it matters).
package aj
import (
"fmt"
"mime"
"net/http"
"go.uber.org/zap"
)
const expectedContentType string = "multipart/form-data"
func FormSubmissionHandler(logger *zap.Logger, emailSender EmailSender) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// enforce a multipart/form-data content-type
contentType := r.Header.Get("content-type")
mediatype, _, err := mime.ParseMediaType(contentType)
if err != nil {
logger.Error("error when parsing the mime type", zap.Error(err))
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if mediatype != expectedContentType {
logger.Error("unsupported content-type", zap.Error(err))
http.Error(w, fmt.Sprintf("api expects %v content-type", expectedContentType), http.StatusUnsupportedMediaType)
return
}
err = r.ParseMultipartForm(32 << 20)
if err != nil {
logger.Error("error parsing form data", zap.Error(err))
http.Error(w, "error parsing form data", http.StatusBadRequest)
return
}
name := r.PostForm.Get("name")
if name == "" {
fmt.Println("inside if statement for name")
logger.Error("name not set", zap.Error(err))
http.Error(w, "api expects name to be set", http.StatusBadRequest)
return
}
email := r.PostForm.Get("email")
if email == "" {
logger.Error("email not set", zap.Error(err))
http.Error(w, "api expects email to be set", http.StatusBadRequest)
return
}
phone := r.PostForm.Get("phone")
if phone == "" {
logger.Error("phone not set", zap.Error(err))
http.Error(w, "api expects phone to be set", http.StatusBadRequest)
return
}
emailService := NewEmailService()
m := NewMessage("Test", "Body message.")
err = emailService.SendEmail(logger, r.Context(), m)
if err != nil {
logger.Error("an error occurred sending the email", zap.Error(err))
http.Error(w, "error sending email", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
})
}
The now updated test giving me trouble is:
package aj
import (
"bytes"
"context"
"encoding/json"
"fmt"
"image/jpeg"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"go.uber.org/zap"
)
type StubEmailService struct {
sendEmail func(logger *zap.Logger, ctx context.Context, email *Message) error
}
func (s *StubEmailService) SendEmail(logger *zap.Logger, ctx context.Context, email *Message) error {
return s.sendEmail(logger, ctx, email)
}
func TestFormSubmissionHandler(t *testing.T) {
// create the logger
logger, _ := zap.NewProduction()
t.Run("returns 400 (bad request) when name is not set in the body", func(t *testing.T) {
// set up a pipe avoid buffering
pipeReader, pipeWriter := io.Pipe()
// this writer is going to transform what we pass to it to multipart form data
// and write it to our io.Pipe
multipartWriter := multipart.NewWriter(pipeWriter)
go func() {
// close it when it has done its job
defer multipartWriter.Close()
// create a form field writer for name
nameField, err := multipartWriter.CreateFormField("name")
if err != nil {
t.Error(err)
}
// write string to the form field writer for name
nameField.Write([]byte("John Doe"))
// we create the form data field 'photo' which returns another writer to write the actual file
fileField, err := multipartWriter.CreateFormFile("photo", "test.png")
if err != nil {
t.Error(err)
}
// read image file as array of bytes
fileBytes, err := os.ReadFile("../../00-test-image.jpg")
// create an io.Reader
reader := bytes.NewReader(fileBytes)
// convert the bytes to a jpeg image
image, err := jpeg.Decode(reader)
if err != nil {
t.Error(err)
}
// Encode() takes an io.Writer. We pass the multipart field 'photo' that we defined
// earlier which, in turn, writes to our io.Pipe
err = jpeg.Encode(fileField, image, &jpeg.Options{Quality: 75})
if err != nil {
t.Error(err)
}
}()
formData := HandleFormRequest{Name: "John Doe", Email: "john.doe#example.com", Phone: "07542147833"}
// create the stub patient store
emailService := StubEmailService{
sendEmail: func(_ *zap.Logger, _ context.Context, email *Message) error {
if !strings.Contains(email.Body, formData.Name) {
t.Errorf("expected email.Body to contain %s", formData.Name)
}
return nil
},
}
// create a request to pass to our handler
req := httptest.NewRequest(http.MethodPost, "/handler", pipeReader)
// set the content type
req.Header.Set("content-type", "multipart/form-data")
// create a response recorder
res := httptest.NewRecorder()
// get the handler
handler := FormSubmissionHandler(logger, &emailService)
// our handler satisfies http.handler, so we can call its serve http method
// directly and pass in our request and response recorder
handler.ServeHTTP(res, req)
// assert status code is what we expect
assertStatusCode(t, res.Code, http.StatusBadRequest)
})
}
func assertStatusCode(t testing.TB, got, want int) {
t.Helper()
if got != want {
t.Errorf("handler returned wrong status code: got %v want %v", got, want)
}
}
As mentioned in the test name, I want to make sure a Name property is coming through with the request.
When I run go test ./... -v I get:
=== RUN TestFormSubmissionHandler/returns_400_(bad_request)_when_name_is_not_set_in_the_body
{"level":"error","ts":1675459283.4969518,"caller":"aj/handler.go:33","msg":"error parsing form data","error":"no multipart boundary param in Content-Type","stacktrace":"github.com/jwankhalaf-dh/ajgenerators.co.uk__form-handler/api/aj.FormSubmissionHandler.func1\n\t/home/j/code/go/src/github.com/jwankhalaf-dh/ajgenerators.co.uk__form-handler/api/aj/handler.go:33\nnet/http.HandlerFunc.ServeHTTP\n\t/usr/local/go/src/net/http/server.go:2109\ngithub.com/jwankhalaf-dh/ajgenerators.co.uk__form-handler/api/aj.TestFormSubmissionHandler.func3\n\t/home/j/code/go/src/github.com/jwankhalaf-dh/ajgenerators.co.uk__form-handler/api/aj/handler_test.go:132\ntesting.tRunner\n\t/usr/local/go/src/testing/testing.go:1446"}
I understand the error, but I am not sure how to overcome it.
My next test would be to test the same thing but for email, and then phone, then finally, I'd like to test file data, but I'm not sure how.

Thanks to Adrian and Cerise I was able to correctly construct the multipart/form-data in the request (updated code is in the question).
However, it was still not working, and the reason was, I was doing:
// set the content type
req.Header.Set("content-type", "multipart/form-data")
instead of:
// set the content type
req.Header.Add("content-type", multipartWriter.FormDataContentType())

Related

Sending a POST Request to external API from GCP cloud function returns 500 but not when sent locally

I'm currently trying to send a POST request to an external API from a GCP Cloud Function. I've tested the function extensively locally and it fulfills the request every time and also works from Postman, but when I run the exact same code from within a cloud function, it returns a 500 from the external API every single time.
I'm genuinely at a loss as to why when sending the POST request from within the cloud function it fails every single time.
Does GCP add any headers that might interfere with an external API call or is there a configuration option within the cloud function settings that needs to be configured to allow an external POST request?
I've attempted to implement an http retry mechanism, but that did not work either.
Again, locally and from Postman, the exact same code is successful every time I run it.
Here is the code I use to generate and send the request:
package email
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"github.com/hashicorp/go-retryablehttp"
)
var FailedRequestErr = errors.New("failed request to moosend")
const (
successCode = 0
moosendHost = "api.moosend.com/v3"
dailyNewsletterMailingListID = "2e461f4c-99d1-4a8e-80ea-168b20bdaf5f"
mainEmail = "jason#functionalbits.io"
campaignNameBase = "Functional Bits Newsletter - Issue"
campaignSubjectBase = "Functional Bits Issue"
)
type CreatingADraftCampaignRequest struct {
Name string `json:"Name"`
Subject string `json:"Subject"`
SenderEmail string `json:"SenderEmail"`
ReplyToEmail string `json:"ReplyToEmail"`
IsAB string `json:"IsAB"`
ConfirmationToEmail string `json:"ConfirmationToEmail,omitempty"`
WebLocation string `json:"WebLocation,omitempty"`
MailingLists []MailingLists `json:"MailingLists,omitempty"`
SegmentID string `json:"SegmentID,omitempty"`
ABCampaignType string `json:"ABCampaignType,omitempty"`
TrackInGoogleAnalytics string `json:"TrackInGoogleAnalytics,omitempty"`
DontTrackLinkClicks string `json:"DontTrackLinkClicks,omitempty"`
SubjectB string `json:"SubjectB,omitempty"`
WebLocationB string `json:"WebLocationB,omitempty"`
SenderEmailB string `json:"SenderEmailB,omitempty"`
HoursToTest string `json:"HoursToTest,omitempty"`
ListPercentage string `json:"ListPercentage,omitempty"`
ABWinnerSelectionType string `json:"ABWinnerSelectionType,omitempty"`
}
type MailingLists struct {
MailingListID string `json:"MailingListId"`
SegmentID float64 `json:"SegmentId,omitempty"`
}
type CampaignResponse struct {
Code int32 `json:"Code"`
Err interface{} `json:"Error"`
Context interface{} `json:"Context"`
}
type MoosendAPI struct {
apiKey string
client *http.Client
}
func NewMoosendAPI(apiKey string) *MoosendAPI {
retryClient := retryablehttp.NewClient()
retryClient.RetryMax = 5
standardClient := retryClient.StandardClient()
return &MoosendAPI{
apiKey: apiKey,
client: standardClient,
}
}
func (m *MoosendAPI) CreateDraftCampaign(issueNumber string, webLocation string) (*CampaignResponse, error) {
campaign := CreatingADraftCampaignRequest{
Name: fmt.Sprintf("%s %s", campaignNameBase, issueNumber),
Subject: fmt.Sprintf("%s %s", campaignSubjectBase, issueNumber),
IsAB: "false",
WebLocation: webLocation,
MailingLists: []MailingLists{{MailingListID: dailyNewsletterMailingListID}},
SenderEmail: mainEmail,
ReplyToEmail: mainEmail,
ConfirmationToEmail: mainEmail,
TrackInGoogleAnalytics: "true",
}
body, err := json.Marshal(&campaign)
if err != nil {
log.Println("error marshalling campaign request")
return nil, err
}
fullURL := fmt.Sprintf("https://%s/campaigns/create.json?apikey=%s", moosendHost, m.apiKey)
req, err := http.NewRequest(http.MethodPost, fullURL, bytes.NewBuffer(body))
if err != nil {
log.Println("request error")
return nil, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
log.Printf("request: %+v", req)
resp, err := m.client.Do(req)
if resp.StatusCode != http.StatusOK {
return nil, FailedRequestErr
}
if err != nil {
log.Println("error sending request")
return nil, err
}
log.Printf("response: %+v", resp)
defer resp.Body.Close()
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println("error reading response body")
return nil, err
}
var draftResponse CampaignResponse
if err := json.Unmarshal(respBody, &draftResponse); err != nil {
log.Println("error unmarshalling response")
log.Printf("%+v", draftResponse)
return nil, err
}
return &draftResponse, nil
}
func (m *MoosendAPI) SendCampaign(campaignID string) error {
fullURL := fmt.Sprintf("https://%s/campaigns/%s/send.json?apikey=%s", moosendHost, campaignID, m.apiKey)
req, err := http.NewRequest(http.MethodPost, fullURL, nil)
if err != nil {
log.Println("error creating request")
return err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
resp, err := m.client.Do(req)
if err != nil {
log.Println("error sending request")
return err
}
defer resp.Body.Close()
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println("error reading response body")
return err
}
var sendResponse CampaignResponse
if err := json.Unmarshal(respBody, &sendResponse); err != nil {
log.Println("error unmarshalling response")
log.Printf("%+v", sendResponse)
return err
}
return nil
}
Then how it's run in the main function code:
package function
import (
"context"
"encoding/json"
"errors"
"log"
"os"
"github.com/Functional-Bits/emailer-service/internal/email"
"github.com/Functional-Bits/emailer-service/internal/publish"
)
func CampaignGenerator(ctx context.Context, m publish.PubSubMessage) error {
moosendAPIKey, ok := os.LookupEnv("MOOSEND_API_KEY")
if !ok {
log.Println("missing moosendAPIKey")
}
mAPI := email.NewMoosendAPI(moosendAPIKey)
var msg publish.IncomingMessage
if err := json.Unmarshal(m.Data, &msg); err != nil {
log.Println(err)
return err
}
log.Printf("received message: %+v", msg)
log.Printf("generating draft campaign for issue %s", msg.IssueNumber)
draftResponse, err := mAPI.CreateDraftCampaign(msg.IssueNumber, msg.FileURL)
if err != nil {
log.Println(err)
return err
}
log.Printf("draft response: %+v", draftResponse)
campaignID, ok := draftResponse.Context.(string)
if !ok {
log.Printf("response didn't contain an ID: %+v", draftResponse)
return errors.New("no campaign generated")
}
log.Printf("sending campgain %s", campaignID)
if err := mAPI.SendCampaign(campaignID); err != nil {
log.Println(err)
return err
}
log.Printf("campaign successfully sent for issue number %s", msg.IssueNumber)
return nil
}
When this code is run locally, It correctly makes the 2 calls and sends an email campaign. When run from the cloud function I get a 500 internal server error with no additional information as to why. Link to API docs.
I get the following response from the external API (from my cloud function logs)
response: &{
Status:500 Internal Server Error
StatusCode:500
Proto:HTTP/1.1
ProtoMajor:1
ProtoMinor:1
Header:map[Access-Control-Allow-Headers:[Content-Type, Accept, Cache-Control, X-Requested-With]
Access-Control-Allow-Methods:[GET, POST, OPTIONS, DELETE, PUT]
Access-Control-Allow-Origin:[*]
Cache-Control:[private]
Content-Length:[12750]
Content-Type:[text/html; charset=utf-8]
Date:[Sun, 12 Dec 2021 07:00:09 GMT]
Server:[Microsoft-IIS/10.0]
X-Aspnet-Version:[4.0.30319]
X-Powered-By:[ASP.NET]
X-Robots-Tag:[noindex, nofollow]
X-Server-Id:[1]]
Body:0xc0003f04c0
ContentLength:12750
TransferEncoding:[]
Close:false
Uncompressed:false
Trailer:map[]
Request:0xc000160b00
TLS:0xc000500630
}
The response causes an unmarshal error because no campaign ID is returned.

How to mock a nested client in test

I am building a simple function that calls an API that returns a Post using GraphQL (https://github.com/machinebox/graphql). I wrapped the logic in a service that looks like this:
type Client struct {
gcl graphqlClient
}
type graphqlClient interface {
Run(ctx context.Context, req *graphql.Request, resp interface{}) error
}
func (c *Client) GetPost(id string) (*Post, error) {
req := graphql.NewRequest(`
query($id: String!) {
getPost(id: $id) {
id
title
}
}
`)
req.Var("id", id)
var resp getPostResponse
if err := c.gcl.Run(ctx, req, &resp); err != nil {
return nil, err
}
return resp.Post, nil
}
Now I'd like to add test tables for the GetPost function with a fail case when id is set to empty string which causes an error in the downstream call c.gcl.Run.
What I am struggling with is the way the gcl client can be mocked and forced to return the error (when no real API call happens).
My test so far:
package apiClient
import (
"context"
"errors"
"github.com/aws/aws-sdk-go/aws"
"github.com/google/go-cmp/cmp"
"github.com/machinebox/graphql"
"testing"
)
type graphqlClientMock struct {
graphqlClient
HasError bool
Response interface{}
}
func (g graphqlClientMock) Run(_ context.Context, _ *graphql.Request, response interface{}) error {
if g.HasError {
return errors.New("")
}
response = g.Response
return nil
}
func newTestClient(hasError bool, response interface{}) *Client {
return &Client{
gcl: graphqlClientMock{
HasError: hasError,
Response: response,
},
}
}
func TestClient_GetPost(t *testing.T) {
tt := []struct{
name string
id string
post *Post
hasError bool
response getPostResponse
}{
{
name: "empty id",
id: "",
post: nil,
hasError: true,
},
{
name: "existing post",
id: "123",
post: &Post{id: aws.String("123")},
response: getPostResponse{
Post: &Post{id: aws.String("123")},
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
client := newTestClient(tc.hasError, tc.response)
post, err := client.GetPost(tc.id)
if err != nil {
if tc.hasError == false {
t.Error("unexpected error")
}
} else {
if tc.hasError == true {
t.Error("expected error")
}
if cmp.Equal(post, &tc.post) == false {
t.Errorf("Response data do not match: %s", cmp.Diff(post, tc.post))
}
}
})
}
}
I am not sure if passing the response to the mock like this is the right way to do it. Also, I'm struggling to set the right value to the response, since an interface{} type is passed and I don't know how to convert it to the getPostResponse and set the value to Post there.
Your test cases should not go beyond the implementation. I'm specifically referring to the empty-vs-nonempty input or any kind of input really.
Let's take a look at the code you want to test:
func (c *Client) GetPost(id string) (*Post, error) {
req := graphql.NewRequest(`
query($id: String!) {
getPost(id: $id) {
id
title
}
}
`)
req.Var("id", id)
var resp getPostResponse
if err := c.gcl.Run(ctx, req, &resp); err != nil {
return nil, err
}
return resp.Post, nil
}
Nothing in the implementation above is doing anything based on the id parameter value and therefore nothing in your tests for this piece of code should really care about what input is passed in, if it is irrelevant to the implementation it should also be irrelevant to the tests.
Your GetPost has basically two code branches that are taken based on a single factor, i.e. the "nilness" of the returned err variable. This means that as far as your implementation is concerned there are only two possible outcomes, based on what err value Run returns, and therefore there should only be two test cases, a 3rd or 4th test case would be just a variation, if not an outright copy, of the first two.
Your test client is also doing some unnecessary stuff, the main one being its name, i.e. what you have there is not a mock so calling it that is not helpful. Mocks usually do a lot more than just return predefined values, they ensure that methods are called, in the expected order and with the expected arguments, etc. And actually you don't need a mock here at all so it's a good thing it isn't one.
With that in mind, here's what I would suggest you do with your test client.
type testGraphqlClient struct {
resp interface{} // non-pointer value of the desired response, or nil
err error // the error to be returned by Run, or nil
}
func (g testGraphqlClient) Run(_ context.Context, _ *graphql.Request, resp interface{}) error {
if g.err != nil {
return g.err
}
if g.resp != nil {
// use reflection to set the passed in response value
// (i haven't tested this so there may be a bug or two)
reflect.ValueOf(resp).Elem().Set(reflect.ValueOf(g.resp))
}
return nil
}
... and here are the necessary test cases, all two of them:
func TestClient_GetPost(t *testing.T) {
tests := []struct {
name string
post *Post
err error
client testGraphqlClient
}{{
name: "return error from client",
err: errors.New("bad input"),
client: testGraphqlClient{err: errors.New("bad input")},
}, {
name: "return post from client",
post: &Post{id: aws.String("123")},
client: testGraphqlClient{resp: getPostResponse{Post: &Post{id: aws.String("123")}}},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := Client{gql: tt.client}
post, err := client.GetPost("whatever")
if !cmp.Equal(err, tt.err) {
t.Errorf("got error=%v want error=%v", err, tt.err)
}
if !cmp.Equal(post, tt.post) {
t.Errorf("got post=%v want post=%v", post, tt.post)
}
})
}
}
... there's a bit of repetition going on here, the need to spell out the post and err twice but that's a small price to pay when compared to a more sophisticated/complicated test setup that would populate the test client from the test case's expected output fields.
Addendum:
If you were to update GetPost in such a way that it checks for the empty id and returns an error before it sends a request to graphql then your initial setup would make much more sense:
func (c *Client) GetPost(id string) (*Post, error) {
if id == "" {
return nil, errors.New("empty id")
}
req := graphql.NewRequest(`
query($id: String!) {
getPost(id: $id) {
id
title
}
}
`)
req.Var("id", id)
var resp getPostResponse
if err := c.gcl.Run(ctx, req, &resp); err != nil {
return nil, err
}
return resp.Post, nil
}
... and updating the test cases accordingly:
func TestClient_GetPost(t *testing.T) {
tests := []struct {
name string
id string
post *Post
err error
client testGraphqlClient
}{{
name: "return empty id error",
id: "",
err: errors.New("empty id"),
client: testGraphqlClient{},
}, {
name: "return error from client",
id: "nonemptyid",
err: errors.New("bad input"),
client: testGraphqlClient{err: errors.New("bad input")},
}, {
name: "return post from client",
id: "nonemptyid",
post: &Post{id: aws.String("123")},
client: testGraphqlClient{resp: getPostResponse{Post: &Post{id: aws.String("123")}}},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := Client{gql: tt.client}
post, err := client.GetPost(tt.id)
if !cmp.Equal(err, tt.err) {
t.Errorf("got error=%v want error=%v", err, tt.err)
}
if !cmp.Equal(post, tt.post) {
t.Errorf("got post=%v want post=%v", post, tt.post)
}
})
}
}

How to set up an HTTP GET fake response [duplicate]

I have the following code:
// HTTPPost to post json messages to the specified url
func HTTPPost(message interface{}, url string) (*http.Response, error) {
jsonValue, err := json.Marshal(message)
if err != nil {
logger.Error("Cannot Convert to JSON: ", err)
return nil, err
}
logger.Info("Calling http post with url: ", url)
resp, err := getClient().Post(url, "application/json", bytes.NewBuffer(jsonValue))
if err != nil {
logger.Error("Cannot post to the url: ", url, err)
return nil, err
}
err = IsErrorResp(resp, url)
return resp, err
}
I'd like to write the tests for this, but I am not sure how to use httptest package .
Take a look here:
https://golang.org/pkg/net/http/httptest/#example_Server
Basically, you can create a new "mock" http server using httptest.NewServer function.
You can have your mock server return whatever response you need from the test, and you can also have your mock server store the request that your HTTPPost function made in order to assert over it.
func TestYourHTTPPost(t *testing.T){
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, `response from the mock server goes here`)
// you can also inspect the contents of r (the request) to assert over it
}))
defer ts.Close()
mockServerURL = ts.URL
message := "the message you want to test"
resp, err := HTTPPost(message, mockServerURL)
// assert over resp and err here
}

How to force error on reading response body

I've written http client wrapper in go and I need to test it thoroughly.
I'm reading the response body with ioutil.ReadAll within the wrapper. I'm having a bit of trouble figuring out how I can force a read from the response body to fail with the help of httptest.
package req
func GetContent(url string) ([]byte, error) {
response, err := httpClient.Get(url)
// some header validation goes here
body, err := ioutil.ReadAll(response.Body)
defer response.Body.Close()
if err != nil {
errStr := fmt.Sprintf("Unable to read from body %s", err)
return nil, errors.New(errStr)
}
return body, nil
}
I'm assuming I can set up a fake server as such:
package req_test
func Test_GetContent_RequestBodyReadError(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
ts := httptest.NewServer(http.HandlerFunc(handler))
defer ts.Close()
_, err := GetContent(ts.URL)
if err != nil {
t.Log("Body read failed as expected.")
} else {
t.Fatalf("Method did not fail as expected")
}
}
I'm assuming I need to modify the ResposeWriter. Now, is there any way for me to modify the responseWriter and thereby force the ioutil.ReadAll in the wrapper to fail?
I realize that you seem to think it's a duplicate of this post and while you may believe so or it might be, just marking it as a duplicate doesn't really help me. The code provided in the answers in the "duplicate" post makes very little sense to me in this context.
Check the documentation of Response.Body to see when reading from it might return an error:
// Body represents the response body.
//
// The response body is streamed on demand as the Body field
// is read. If the network connection fails or the server
// terminates the response, Body.Read calls return an error.
//
// The http Client and Transport guarantee that Body is always
// non-nil, even on responses without a body or responses with
// a zero-length body. It is the caller's responsibility to
// close Body. The default HTTP client's Transport may not
// reuse HTTP/1.x "keep-alive" TCP connections if the Body is
// not read to completion and closed.
//
// The Body is automatically dechunked if the server replied
// with a "chunked" Transfer-Encoding.
Body io.ReadCloser
The easiest way is to generate an invalid HTTP response from the test handler.
How to do that? There are many ways, a simple one is to "lie" about the content length:
handler := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Length", "1")
}
This handler tells it has 1 byte body, but actually it sends none. So at the other end (the client) when attempting to read 1 byte from it, obviously that won't succeed, and will result in the following error:
Unable to read from body unexpected EOF
See related question if you would need to simulate error reading from a request body (not from a response body): How do I test an error on reading from a request body?
To expand upon icza's awesome answer, you can also do this with an httptest.Server object:
bodyErrorServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Length", "1")
}))
defer bodyErrorServer.Close()
You can then pass bodyErrorServer.URL in your tests like normal, and you'll always get an EOF error:
package main
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func getBodyFromURL(service string, clientTimeout int) (string, error) {
var netClient = &http.Client{
Timeout: time.Duration(clientTimeout) * time.Millisecond,
}
rsp, err := netClient.Get(service)
if err != nil {
return "", err
}
defer rsp.Body.Close()
if rsp.StatusCode != 200 {
return "", fmt.Errorf("HTTP request error. Response code: %d", rsp.StatusCode)
}
buf, err := ioutil.ReadAll(rsp.Body)
if err != nil {
return "", err
}
return string(bytes.TrimSpace(buf)), nil
}
func TestBodyError(t *testing.T) {
bodyErrorServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Length", "1")
}))
_, err := getBodyFromURL(bodyErrorServer.URL, 1000)
if err.Error() != "unexpected EOF" {
t.Error("GOT AN ERROR")
} else if err == nil {
t.Error("GOT NO ERROR, THATS WRONG!")
} else {
t.Log("Got an unexpected EOF as expected, horray!")
}
}
Playground example here: https://play.golang.org/p/JzPmatibgZn

How to test http calls in go

I have the following code:
// HTTPPost to post json messages to the specified url
func HTTPPost(message interface{}, url string) (*http.Response, error) {
jsonValue, err := json.Marshal(message)
if err != nil {
logger.Error("Cannot Convert to JSON: ", err)
return nil, err
}
logger.Info("Calling http post with url: ", url)
resp, err := getClient().Post(url, "application/json", bytes.NewBuffer(jsonValue))
if err != nil {
logger.Error("Cannot post to the url: ", url, err)
return nil, err
}
err = IsErrorResp(resp, url)
return resp, err
}
I'd like to write the tests for this, but I am not sure how to use httptest package .
Take a look here:
https://golang.org/pkg/net/http/httptest/#example_Server
Basically, you can create a new "mock" http server using httptest.NewServer function.
You can have your mock server return whatever response you need from the test, and you can also have your mock server store the request that your HTTPPost function made in order to assert over it.
func TestYourHTTPPost(t *testing.T){
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, `response from the mock server goes here`)
// you can also inspect the contents of r (the request) to assert over it
}))
defer ts.Close()
mockServerURL = ts.URL
message := "the message you want to test"
resp, err := HTTPPost(message, mockServerURL)
// assert over resp and err here
}