Mocking API Endpoints with Go



  • So I had to mock an API in something I'm writing for testing and I figured I'd post this to show you an easy way to do it in Go.

    First you need to get the JSON structure for the response you'll be mocking. To do this you can just use cURL or whatever tool you want.

    Let's use JSON Placeholder since it's free and doesn't require any authentication. If you go to this URL, you'll see the comments API: https://jsonplaceholder.typicode.com/posts/1/comments

    I use this site to convert JSON to structs. If you paste in the JSON data you get a struct like this:

    type Comment []struct {
        PostID int    `json:"postId"`
        ID     int    `json:"id"`
        Name   string `json:"name"`
        Email  string `json:"email"`
        Body   string `json:"body"`
    }
    

    So we just need to be able to mock this in our unit tests. Here's a small app that reaches out to JSON Placeholder and gets all of the comments that exist for a specific post.

    package main
    
    import (
    	"encoding/json"
    	"fmt"
    	"io/ioutil"
    	"log"
    	"net/http"
    )
    
    type Comment []struct {
    	PostID int    `json:"postId"`
    	ID     int    `json:"id"`
    	Name   string `json:"name"`
    	Email  string `json:"email"`
    	Body   string `json:"body"`
    }
    
    func (c Comment) getComment(u string) {
    
    	req, err := http.Get(u)
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	body, err := ioutil.ReadAll(req.Body)
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	error := json.Unmarshal(body, &c)
    	if error != nil {
    		log.Fatal(error)
    	}
    
    	fmt.Println(c[0].Name)
    
    }
    
    func main() {
    	var c Comment
    
    	c.getComment("https://jsonplaceholder.typicode.com/posts/1/comments")
    }
    

    However, when you're unit testing, you don't want to have to actually reach out to the site. That's more of an integration test. So to be able to test our method, we need to mock that API endpoint. So to do that Go has a test server built in for this reason. You can use it like this:

    package main
    
    import (
    	"io"
    	"net/http"
    	"net/http/httptest"
    	"testing"
    )
    
    type commentTest struct {
    	actual   string
    	expected string
    }
    
    type commentInt struct {
    	actual   int
    	expected int
    }
    
    func TestgetComment(t *testing.T) {
    	data := `
    	[
      		{
        		"postId": 1,
        		"id": 1,
        		"name": "test name",
        		"email": "[email protected]",
        		"body": "this is the body"
      		},
      	]
    	`
    
    	ts := httptest.NewServer(
    		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    			io.WriteString(w, data)
    		}))
    
    	defer ts.Close()
    
    	var c Comment
    
    	c.getComment(ts.URL)
    
    	var tests = []commentTest{
    		{c[0].Name, "test name"},
    		{c[0].Email, "[email protected]"},
    		{c[0].Body, "this is the body"},
    	}
    
    	for _, tt := range tests {
    		if tt.actual != tt.expected {
    			t.Errorf("got %v, want %v", tt.actual, tt.expected)
    		}
    	}
    
    	var testInt = []commentInt {
    		{c[0].PostID, 1},
    		{c[0].ID, 1},
    	}
    
    	for _, ti := range testInt {
    		if ti.actual != ti.expected {
    			t.Errorf("got %v, want %v", ti.actual, ti.expected)
    		}
    	}
    
    }
    

    This particular test happened to be a little long because of the two types of data. There might be an easier way by building one struct for both the int and string types but this way was easy to write really quickly.

    I don't want to get into how testing in Go works. However, the important line is ts := httptest.NewServer(). That's where we define our test server. We tell it to take data and put that into w which is our response. So when we run our c.getComment() method we wrote earlier, we can pass ts.URL which is the URL of our test server and we will get a response of whatever is in data. Then we just compare the results to what we expect them to be and if they aren't the same the test will fail.

    It's good to note that if your JSON data is long, you probably don't want to put it in line. So you could store it in a file alongside your code and pass the data in that way. You would just use something like data, err := ioutil.ReadFile("./data.json").



  • Poor APIs, everyone is always making fun of them.



  • Good info, thanks for posting.



  • I know that Postman provides the ability to perform this type of API testing. Has anyone tried this feature?



  • @Danp said in Mocking API Endpoints with Go:

    I know that Postman

    The Postman Always Mocks Twice

    LOL, oh I'm full of them this week.



  • @scottalanmiller said in Mocking API Endpoints with Go:

    Poor APIs, everyone is always making fun of them.

    Smock.jpg



  • @Danp said in Mocking API Endpoints with Go:

    I know that Postman provides the ability to perform this type of API testing. Has anyone tried this feature?

    I'm sure it does. The problem there though is if you're running in a pipeline you'd have to somehow get Postman downloaded, set up, and configured automatically to test against it. This is just in the standard library so you can unit test in your pipeline automatically.