Testing calls to Daily's REST API in Go
Tags: testing, restapi, go, tutorial
Published: 2023-09-08 17:08:21

_By [Liza Shulyayeva](https://www.daily.co/blog/author/liza/)_

Testing external API calls can sometimes be a challenge. Nevertheless, it is important to make sure your logic around external dependencies is behaving as expected.

In this post, I’ll use my [prejoin attendance list demo](https://github.com/daily-demos/daily-prejoin-presence) to present a method of testing calls to [Daily’s REST API](https://docs.daily.co/reference/rest-api) in Go, which is a popular language for backend services.

This post assumes the reader already has some prior experience with Go, so I won’t get too into the basics here. But I will provide links to help you brush up on any relevant concepts if needed as you go.

## An overview of the demo
If you haven’t already read [my tutorial on implementing a prejoin attendance list with Daily](https://www.daily.co/blog/p/8e4e5e58-6d01-4dae-a392-ec4d8b451a67/), let’s cover the basics of the application now. If you are already familiar with the demo, feel free to skip this section.

The prejoin attendance list demo lets a user create a new Daily video call room and then join its pre-call lobby. When this lobby is joined, a list of participants who are already in the call is shown on the right-hand side. This enables the user to get a sneak peek of who’s already there before they hop into the call.


![Call participant in the pre-join lobby, with an attendance lift on the right](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/22f6kng1zl0b2dh02bwo.gif)
_Call participant in the pre-join lobby, with an attendance list on the right_

To run the demo locally, refer to [the README](https://github.com/daily-demos/daily-prejoin-presence#running-locally) in the GitHub repository.

For the purposes of this post, we mostly care about the [server-side components](https://github.com/daily-demos/daily-prejoin-presence/tree/cd231ccf2ab73c2ae6e190707ab850cd86e99284/netlify), which are written as Netlify stateless functions in Go. I have a Netlify endpoint for [creating a room](https://github.com/daily-demos/daily-prejoin-presence/blob/cd231ccf2ab73c2ae6e190707ab850cd86e99284/netlify/functions/createRoom/main.go#L42) and another for [retrieving video call presence](https://github.com/daily-demos/daily-prejoin-presence/blob/cd231ccf2ab73c2ae6e190707ab850cd86e99284/netlify/functions/presence/main.go#L32). I have implemented some [basic tests](https://github.com/daily-demos/daily-prejoin-presence/blob/cd231ccf2ab73c2ae6e190707ab850cd86e99284/netlify/functions/presence/main_test.go#L11) for these. These tests are what I’ll focus on in this post.

With that overview, let’s move into the testing method.

## How I test external API calls in Go
My objective when testing external API calls is to test _my handling_ of the returned data, _not_ to test the external API itself. I want to make sure my own code handles data returned by Daily appropriately.

My preferred method of testing external calls in Go these days is using the [`httptest`](https://pkg.go.dev/net/http/httptest) package, which is part of Go’s standard library. There are other ways, like wrapping external request logic with an [interface](https://go.dev/tour/methods/9) and then mocking said interface, but I find spinning up a little test server for every test case to be a more intuitive approach most of the time.

This is the method I used for testing my calls to Daily’s REST API in my prejoin attendance list demo. Both the room creation and presence retrieval tests follow the same approach. In this post, I’ll use the presence retrieval test as an example.

## Table-driven tests
I opt for using [table-driven tests](https://dave.cheney.net/2019/05/07/prefer-table-driven-tests) when testing Go. A table-driven test is essentially a test that defines a number of inputs and expected outputs within the test itself, and reuses core logic for running the test using that data. You’ll often see a new struct being defined in the test function. This struct dictates the structure of test inputs and outputs. You'd then have a variable specifying test cases according to this structure.

My presence test looks like this:

```
func TestGetPresence(t *testing.T) {
	t.Parallel()
	testCases := []struct {
		name             string
		retCode          int
		retBody          string
		wantErr          error
		wantParticipants []Participant
	}{
		// Multiple test cases here
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			// Shared test logic here
		})

	}
}
```
This test will focus specifically on testing my endpoint’s [`getPresence()`](https://github.com/daily-demos/daily-prejoin-presence/blob/cd231ccf2ab73c2ae6e190707ab850cd86e99284/netlify/functions/presence/main.go#L72) function, which is where the call out to Daily’s REST API takes place.

The test case struct above defines a few key pieces of information:

- `name` is the name of the test case. This should be something you can easily understand in the logs.
- `retCode` is the return code I’ll be simulating from Daily.
- `retBody` is the return body I’ll be simulating from Daily.
- `wantErr` is the error I expect _my_ logic to return after processing Daily’s simulated return data.
- `wantParticipants` is the slice of participants I’ll expect _my_ logic to return after processing Daily’s return data.

After defining the test table and test cases, I have the shared logic that each test case will run through.

- I iterate over every test case and shadow the `tc` variable. `t.Run()` runs the function literal within it in a separate [goroutine](https://go.dev/tour/concurrency/1). In Go, function literals retain references to variables in the outer scope. This means the `tc` variable set as part of the `for` loop declaration can change for a running test case as the loop moves onto its next iteration. By shadowing `tc`, I ensure that the test case data for each iteration is what I expect.
- I then call [`t.Run()`](https://pkg.go.dev/testing#T.Run) and pass it the core logic of the test, which we’ll go through next.

## Defining the core logic of the test
Let’s start with covering the shared components of the test: the things that _every_ test case will run through. This is where the test server will be spun up and destroyed for each test case.

```
func TestGetPresence(t *testing.T) {
	t.Parallel()
	testCases := []struct {
		name             string
		retCode          int
		retBody          string
		wantErr          error
		wantParticipants []Participant
	}{
		// Multiple test cases here
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
				w.WriteHeader(tc.retCode)
				_, err := w.Write([]byte(tc.retBody))
				require.NoError(t, err)
			}))
			defer testServer.Close()
			gotParticipants, gotErr := getPresence("name", "key", testServer.URL)
			require.ErrorIs(t, gotErr, tc.wantErr)
			if tc.wantErr == nil {
				require.EqualValues(t, tc.wantParticipants, gotParticipants)
			}
		})

	}
}
```

There are three main things happening above:

1. I’m configuring the simulation of Daily’s REST API response.
2. I’m invoking my function to be tested.
3. I’m checking that the return values of that function match what I expect.

### Simulating Daily’s REST API response
The first step above is the main one: Creating a test server with [`httptest.NewServer()`](https://pkg.go.dev/net/http/httptest#NewServer). The constructor here takes an instance of [`http.Handler`](https://pkg.go.dev/net/http#Handler), which is an interface defining a single function: `ServeHTTP(ResponseWriter, *Request).`

```
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(tc.retCode)
	_, err := w.Write([]byte(tc.retBody))
	require.NoError(t, err)
}))
defer testServer.Close()
```

Inside my test HTTP handler above, I write the return header I want to simulate (in this case the return code defined in my test case), and the _body_ I want my fake-Daily to return.

I then verify that there are no issues with writing the body with `require.NoError()` from the [`testify`](https://github.com/stretchr/testify) toolkit. This will ensure the test fails if something happens to go wrong at this point.

After creating `testServer`, I add a [`defer`](https://go.dev/tour/flowcontrol/12) statement to close the server. This statement will run as the last call of the test case.

That concludes the primary setup: configuring my test server to return the Daily data that I want my tested function to handle.

### Calling the `getPresence()` function
With the setup all done, I can test my actual function. I do so by calling `getPresence()` and assigning its return values to two `got` variables:

```
gotParticipants, gotErr := getPresence("name", "key", testServer.URL)
```

The `get` and `want` variable prefix format makes it clear to anyone reading the test which pieces of data I _got_ from the thing I’m testing and which pieces of data I _wanted to get_ from the thing I’m testing, and then compare them.

The first two parameters passed to `getPresence` above are just dummy values: they will not impact the behavior of my test server response. The last one, `testServer.URL` is very important.

[When calling `getPresence()` from my live handler code](https://github.com/daily-demos/daily-prejoin-presence/blob/cd231ccf2ab73c2ae6e190707ab850cd86e99284/netlify/functions/presence/main.go#L47), that last parameter is the URL of Daily’s REST API. But in this case, we want to re-route the request through my _fake_ server: the test server I created above. This is why I pass in `testServer.URL` instead.

This may be a good example of how you sometimes need to structure your non-test code to be _testable_.

#### Writing testable code
It would have been very easy to write `getPresence()` to not take a URL parameter at all, and just retrieve my `DAILY_API_UR`L environment variable from within the function. It could even be tempting: Why clutter the function signature with _another_ argument when the data is right there?! But that would have made the testing approach desired here impossible, since I’d have no way to inject my test server into the process.

### Testing the return values
After calling `getPresence()` and consuming the return values, the last thing left to do is confirming that the data returned is what I expected. [I like using testify’s `require` package for this](https://github.com/stretchr/testify/):

```
require.ErrorIs(t, gotErr, tc.wantErr)
if tc.wantErr == nil {
    require.EqualValues(t, tc.wantParticipants, gotParticipants)
}
```

First, I check if the error I got is what I expect. If this `require.ErrorIs()` check fails, the test will immediately fail and not proceed to the next check.

If it succeeds, I check if my wanted error was actually `nil`: i.e., no error at all. In that case, I use `require.EqualValues()` to compare the values of the participants I said I wanted to the values of the participants I actually got. If these do not match, the test will also fail.

That’s it for the primary bulk of our test logic! All that’s left is taking a quick look at a couple of examples of test case data I’m feeding into this test.

## Defining test case data
When writing test cases for external APIs, my first stop is always the documentation. In this case, this would be Daily’s [`/room/:name/presence` endpoint docs](https://docs.daily.co/reference/rest-api/rooms/get-room-presence#example-request). I’m _especially_ interested in examples of API responses, which you can see at the bottom of that page.

That example usually defines my first test case, which I will pop right into the [test cases slice](https://github.com/daily-demos/daily-prejoin-presence/blob/cd231ccf2ab73c2ae6e190707ab850cd86e99284/netlify/functions/presence/main_test.go#L27) I defined up above:

```
func TestGetPresence(t *testing.T) {
	t.Parallel()
	testCases := []struct {
		name             string
		retCode          int
		retBody          string
		wantErr          error
		wantParticipants []Participant
	}{
		{
			name:    "one-participant",
			retCode: 200,
			// This response is copied directly from the presence endpoint docs example:
			// https://docs.daily.co/reference/rest-api/rooms/get-room-presence#example-request
			retBody: `
				{
				  "total_count": 1,
				  "data": [
					{
					  "room": "w2pp2cf4kltgFACPKXmX",
					  "id": "d61cd7b2-a273-42b4-89bd-be763fd562c1",
					  "userId": "pbZ+ismP7dk=",
					  "userName": "Moishe",
					  "joinTime": "2023-01-01T20:53:19.000Z",
					  "duration": 2312
					}
				  ]
				}
        	`,
			wantParticipants: []Participant{
				{
					ID:   "d61cd7b2-a273-42b4-89bd-be763fd562c1",
					Name: "Moishe",
				},
			},
		},
}
```

The `retCode` and `retBod`y values above are taken straight from Daily’s docs.

I then define my `wantParticipants` value based on what I expect that data to _turn into_ once my logic runs.

In this case, I expect `getPresence()` (the function being tested) to return a participant slice which includes one element: a participant with the ID of `"d61cd7b2-a273-42b4-89bd-be763fd562c1"` and the name of `"Moishe"`.

The next thing I usually go for is testing _failure_ responses. My test case for an internal server error returned by Daily looks like this:

```
{
	name:    "failure",
	retCode: 500,
	wantErr: util.ErrFailedDailyAPICall,
},
```

Here, I have no body for my Daily test server to return, and only return a `500` status code. In this case, I expect `getPresence()` to return _no_ slice of participants (i.e., a `nil` value) and a pre-defined [`util.ErrFailedDailyAPICall`](https://github.com/daily-demos/daily-prejoin-presence/blob/cd231ccf2ab73c2ae6e190707ab850cd86e99284/netlify/util/errors.go#L13) error.

These are just two basic examples. You can then flesh out the test with more test cases, such as:

- Testing more participants returned from Daily
- Testing unexpected data that your function does not know how to parse
- Testing unexpectedly long response times

With the core logic of the test remaining the same, testing more variations and code paths within your function becomes a matter of simply introducing another test case to your “table”.

## Conclusion
In this post, I covered one way of testing calls to Daily’s REST API in Go. If you have any questions about testing any other parts of our video APIs, whether in Go or another language, don’t hesitate to [reach out to our support team](https://www.daily.co/company/contact/support/). I’d be very curious to hear about other developers’ approaches to testing with Daily. If you’d like to share, head over to [peerConnection](https://community.daily.co/), our WebRTC community.