Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Testing calls to Daily's REST API in Go
Daily profile imageTasha
Tasha forDaily

Posted on • Edited on • Originally published atdaily.co

Testing calls to Daily's REST API in Go

ByLiza Shulyayeva

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 myprejoin attendance list demo to present a method of testing calls toDaily’s 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 readmy tutorial on implementing a prejoin attendance list with Daily, 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
Call participant in the pre-join lobby, with an attendance list on the right

To run the demo locally, refer tothe README in the GitHub repository.

For the purposes of this post, we mostly care about theserver-side components, which are written as Netlify stateless functions in Go. I have a Netlify endpoint forcreating a room and another forretrieving video call presence. I have implemented somebasic tests 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 testmy 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 thehttptest package, which is part of Go’s standard library. There are other ways, like wrapping external request logic with aninterface 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 usingtable-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        })    }}
Enter fullscreen modeExit fullscreen mode

This test will focus specifically on testing my endpoint’sgetPresence() 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 expectmy logic to return after processing Daily’s simulated return data.
  • wantParticipants is the slice of participants I’ll expectmy 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 thetc variable.t.Run() runs the function literal within it in a separategoroutine. In Go, function literals retain references to variables in the outer scope. This means thetc variable set as part of thefor loop declaration can change for a running test case as the loop moves onto its next iteration. By shadowingtc, I ensure that the test case data for each iteration is what I expect.
  • I then callt.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 thatevery 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)            }        })    }}
Enter fullscreen modeExit fullscreen mode

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 withhttptest.NewServer(). The constructor here takes an instance ofhttp.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()
Enter fullscreen modeExit fullscreen mode

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 thebody I want my fake-Daily to return.

I then verify that there are no issues with writing the body withrequire.NoError() from thetestify toolkit. This will ensure the test fails if something happens to go wrong at this point.

After creatingtestServer, I add adefer 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 thegetPresence() function

With the setup all done, I can test my actual function. I do so by callinggetPresence() and assigning its return values to twogot variables:

gotParticipants, gotErr := getPresence("name", "key", testServer.URL)
Enter fullscreen modeExit fullscreen mode

Theget andwant variable prefix format makes it clear to anyone reading the test which pieces of data Igot from the thing I’m testing and which pieces of data Iwanted to get from the thing I’m testing, and then compare them.

The first two parameters passed togetPresence 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 callinggetPresence() from my live handler code, that last parameter is the URL of Daily’s REST API. But in this case, we want to re-route the request through myfake server: the test server I created above. This is why I pass intestServer.URL instead.

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

Writing testable code

It would have been very easy to writegetPresence() to not take a URL parameter at all, and just retrieve myDAILY_API_URL environment variable from within the function. It could even be tempting: Why clutter the function signature withanother 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 callinggetPresence() 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’srequire package for this:

require.ErrorIs(t, gotErr, tc.wantErr)if tc.wantErr == nil {    require.EqualValues(t, tc.wantParticipants, gotParticipants)}
Enter fullscreen modeExit fullscreen mode

First, I check if the error I got is what I expect. If thisrequire.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 actuallynil: i.e., no error at all. In that case, I userequire.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. I’mespecially 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 thetest cases slice 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",                },            },        },}
Enter fullscreen modeExit fullscreen mode

TheretCode andretBody values above are taken straight from Daily’s docs.

I then define mywantParticipants value based on what I expect that data toturn into once my logic runs.

In this case, I expectgetPresence() (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 testingfailure responses. My test case for an internal server error returned by Daily looks like this:

{    name:    "failure",    retCode: 500,    wantErr: util.ErrFailedDailyAPICall,},
Enter fullscreen modeExit fullscreen mode

Here, I have no body for my Daily test server to return, and only return a500 status code. In this case, I expectgetPresence() to returnno slice of participants (i.e., anil value) and a pre-definedutil.ErrFailedDailyAPICall 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 toreach out to our support team. I’d be very curious to hear about other developers’ approaches to testing with Daily. If you’d like to share, head over topeerConnection, our WebRTC community.

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

More fromDaily

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp