Golang Blazing Fast Unit Tests - Fiber/fasthttp/http Internals and Optimizing HTTP Server Tests
Tags: go, performance, programming, webdev
Published: 2022-09-12 07:35:46


![greeting](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1689fqydeywmfxbf2wxy.png)

Greetings, in this article, I will tell you how we can increase the test performance of our HTTP servers in the projects we have developed with Go.

Recently, while I was working on a new feature for an application we were developing, I realized that I could commute to get coffee while waiting for unit tests to complete. And it was not possible to run the tests in parallel. If we wanted to run it in parallel in its current form, this test period could have taken much longer.

Now, let's take a brief look at the content and move on to the details of our subject.

### Contents
#### Go HTTP Internals
- http.ListenAndServe
- net.Conn & net.Listener
- http.Client Dial Function
- net.Pipe

### Testing in Different Packages
#### Fiber
- app.Test Internals
- app.ServeConn

#### fasthttp
- fasthttputil.NewInmemoryListener

#### Go HTTP
- httptest.NewUnstartedServer
- Mock InMemListener Example

#### Our Test Performance Improvement
#### Conclusion

---

## Go HTTP Internals
Let's take a look at what we need to do when we want to run a simple HTTP server with the Go language.

{% embed https://gist.github.com/mstrYoda/d39ebfaf4c0b910647b8ccf3a60e83d1 %}

### http.ListenAndServe
When we want to run a server that we wrote using the Go standard library, we use the ListenAndServe method.

Let's take a look at the details of the **ListenAndServe** method:

{% embed https://gist.github.com/mstrYoda/ce88e7b47ce65d0f82858d0ac138b67c %}

Now let's look at the details of the **server.ListenAndServe()** method:

{% embed https://gist.github.com/mstrYoda/125090a98d3a7ebb40ec36d67e77b2ad %}

Finally, let's look at the details of the **srv.Serve(ln)** method. I've removed some code so it's not too long.

{% embed https://gist.github.com/mstrYoda/856f6b4f7e541c705c8b76f3133be088 %}

The part that we should pay attention to here is the **l.Accept** method.

The summary of the process done in the codes I have shared so far is actually:

- http.ListenAndServe method creates a server in the background and server ListenAndServe method is run

- The server creates a net.Listener object that accepts a request from the port specified by the `net.Listen("tcp", addr)` method call and runs the Serve method.

- The Serve method acts on the net.Conn object that represents the new incoming connections with the `Accept()` method of the net.Listener object that it receives as a parameter.

So far, we have seen the process of creating an API in its basic form.

Now, I can hear you slowly saying, "Well, sir, what will they do in real life?"

Don't worry, we'll soon see how we can improve our testing performance using this information.


### net.Listen & net.Conn
Now let's take a closer look at these two basic types included in the HTTP package.

{% embed https://gist.github.com/mstrYoda/1a9d14aa208bbda0924fa557191f083f %}

{% embed https://gist.github.com/mstrYoda/04c879e09264c99cd8176520192e0660 %}

We see that these two types are actually interfaces.

Well, what does it mean? We can give special structs that we implement these interfaces as parameters to methods that expect these interface types as parameters. This is an important point for us.

### http.Client Dial Function
Now let's look at how the http.Client object is used, which allows us to make HTTP requests with Go.
There are two methods of sending requests using the Go HTTP package.
- The first method is to use the **http.Get http.Post** methods directly
- The second method is to use the http.Client object `resp, _ := http.Get("http://localhost:8080/test")`

{% embed https://gist.github.com/mstrYoda/757ca7ffb42f1688609befb267191441 %}

Actually, the first method, http.Get, also uses the http.Client object in the background.
Let's take a brief look:

{% embed https://gist.github.com/mstrYoda/31d47514325df39fd13a082fdddb73bc %}

In other words, both methods do the same thing in the background.

Now let's look at the fields that we can customize when creating the **http.Client** object.

http.Client provides all connection establishment processes with **RoundTripper** interface. And it uses the **Transport** object as a RoundTripper implementation in itself.

If we want, we can create this Transport object ourselves when creating http.Client.

{% embed https://gist.github.com/mstrYoda/dbe73cddd2549d8deeff9cd22097601d %}

The Transport object we created has a method called **DialContext**. This method is the method that enables connection opening in the background, it returns a value of type **net.Conn** as a variable.

In a moment, we will combine this information and the information that the server.Serve method we just saw is listening for connections with net.Conn using the accept method of the net.Listener variable that takes as a parameter.

Little by little the pieces are falling into place, aren't they?

---

### net.Pipe
Finally, there is another method I would like to mention: `net.Pipe()`. It returns two net.Conn type variables as return value.

{% embed https://gist.github.com/mstrYoda/784241d5a3fb05d3acd583f80ad900a6 %}

One of these variables can be used as a writing point and the other as a reading point. So what does this mean? How can we use this?

If you remember, the **Listen()** method of the Listener object returned the variable **net.Conn**. The **Dial()** method of the HTTP client Transport object also returns the variable **net.Conn**.

Starting from here, when we give one end of the connections returned by the `net.Pipe()` method to the **Dial** method of our HTTP client and the other end to the `Listen` method of our **Listener** object, the request we made with the HTTP client will create the connection expected by the **Listener**.

---

## Testing in Different Packages
Now, let's create an API using several different web application libraries common in the Go language and write tests for them.

### [Fiber](https://github.com/gofiber/fiber)
First, let's take a look at the Fiber library, which we often use in our projects and to which I contribute.

Writing a test with Fiber is quite simple because it contains a direct Test method.

Now let's look at a sample Fiber application and how it was tested:

{% embed https://gist.github.com/mstrYoda/8178e96cf5d15b964fbc38b211a087cc %}

Here we need to pay attention to the line `app.Test(r, -1)`. 

We're sending our HTTP request here so we can test it.
So how did this HTTP request go without calling `app.Listen(":8080")` which we normally do to run a Fiber application and make it listen on a port?

Let's take a look at what's behind the `app.Test()` method (you can access all the code [here](https://github.com/gofiber/fiber/blob/master/app.go/#L845)):

{% embed https://gist.github.com/mstrYoda/13f37b3c97dac5fbd409956efef2c6fc %}

When we examine the code, we see that Fiber actually creates its own connection objects by `conn := new(testConn)` in order to test the application, as we explained in the `net.Pipe()` section. 
It writes the HTTP request to this conn object and sends this conn object to the server with the `app.server.ServeConn(conn)` call.

So what happens when we say `app.Listen(":8080")` in Fiber app normally? Let's take a quick look at this:

{% embed https://gist.github.com/mstrYoda/f75c548a99dce1ad49133d0bc6a8a354 %}

Here we see that Fiber does the `net.Listen()` operation within itself and gives the listener object it created to the server by saying `app.server.Serve(ln)`. The server listens to the connections from this listener object and passes it as a parameter to the `serveConn()` method.

In other words, it implements an approach that brings together the Listener and net.Conn objects that we explained at the beginning, allowing us to test it without physically listening to the port.

{% embed https://github.com/gofiber/fiber %}

---

[fasthttp](https://github.com/valyala/fasthttp)

We said that Fiber is using the Fasthttp package in the background. If we want, we can develop high-performance API applications directly using the Fasthttp package.
Now let's see how we can test an application we developed with Fasthttp.

In the code above, we created a simple server and defined a handler, and when a GET request was made to the /test endpoint, we returned the "OK" response.

Here, unlike the previous code, we see a definition like `ln := fasthttputil.NewInmemoryListener()`.

Next, we define a http.Client and give the Dial method of the ln variable we created to the DailContext method of the Transport object.

So does this remind you of anything? Yes, we mentioned above that we can give the net.Listener object to our http.Client variable and test it in-memory. 
Fasthttp does exactly this approach with a utility package in the background.
Those who are curious about the details of the code can read it [here](https://github.com/valyala/fasthttp/blob/master/fasthttputil/inmemory_listener.go).

{% embed https://github.com/valyala/fasthttp %}

---

### Go HTTP
Now, let's write a test for an API that we developed with Go's own packages without using any external libraries.

Let's say we developed an API like above. So how do we test this?
We can write our tests by using the techniques we learned above here as well. Go language has a package called httptest which contains some utility methods. In this package, a method named `NewUnstartedServer()` returns us an unstarted test server object.

Let's take a look at the source code:

{% embed https://gist.github.com/mstrYoda/201a3e959ae1719dfc49af46d89d92de %}

When we create a Server with the `httptest.NewUnstartedServer()` method, a Listener is created in the background that listens to a random port with the newLocalListener() internal method.

When we say start, it listens for connections from the listener in a goroutine with the s.goServe() method.
What comes to mind when we see Listener? As in Fasthttp, if we create an InMemoryListener and give it to our server, we can perform our test without listening a physical port.

{% embed https://gist.github.com/mstrYoda/ecba8e844d3c0905e0afc7d0f10c6a35 %}

In the above code, we run our server by actually listening to a physical port in the test named TestHttpServer. I put the time.Sleep to wait for the server to start. Using the `srv.Listener.Addr().String()` method, we can obtain the address (including the port randomly assigned by the OS) to which we need to request.

In the test called **TestHttpServerInMemory**, we performed the http server-client communication via an in-memory listener we created without listening to a physical port.
Here I used the `fasthttputil.NewInMemoryListener` method for convenience. If we want, we can do the same thing by creating a struct and implementing the Listener interface.
Let's take a quick look at it:

{% embed https://gist.github.com/mstrYoda/ff206996489c5c64fc3e8c7c1b74cd11 %}

As we saw above, we can provide in-memory server-client communication by implementing the Listener interface ourselves and using the net.Pipe() method. The reason why I gave connection objects to our InMemListener struct over a channel is that the server calls the Accept() method in a loop and waits for a new connection. Thus, only if we give a connection to our Listener object, the Server side will receive a new connection.

So why isn't this in-memory server approach provided by default by Go?

In fact, there is a timely issue about it and there have been some people working on it, but the process has not been finalized yet.

{% embed https://github.com/golang/go/issues/14200 %}

---

## Our Test Performance Improvements
Now let's see how we improved our own test performance using the methods I described above.
We developed our application with fasthttp and this application was acting as a basic reverse proxy. We also use some functionality provided by fasthttp for reverse proxy operation.

For our tests, we need a reverse proxy (our application) and a fake API to represent the server we are redirecting in the background.

We used `httptest.NewUnstartedServer` for this and we used `fasthttptest.NewInmemoryListener`.

![ss-our-code](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9z4odgrebfhmlkn4xoo9.png)

Previously, we were actually listening on a port to run test servers in our tests, and this slowed down our testing process quite a bit. When I looked from our CI pipeline, our testing process was taking about 10 seconds.

As a result of our development, after using the in-memory listener approach, all our tests were completed in ~0.3 seconds.

In the image below, we can see all our tests and total uptime.

![ss-test-time](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mvn4vnzhye8h21v1xjge.png)

---

## Conclusion
- When we want to test an HTTP server, we can perform our test faster without listening to a physical PORT.

- Our tests run much faster with this technique

- We can simulate in-memory net operations using net.Pipe(), net.Listener, `net.Conn`

- We can use the `app.Test()` method when testing our applications with fiber.

- We can create in-memory Listener using `fasthttputil.InMemoryListener()`

---

I hope this was a fun and informative article for you. See you in the next post :)


**Let's Connect:**
[Twitter](https://twitter.com/mstrYoda_)
[Github](https://github.com/mstrYoda)

You can support me on Github: [Support mstrYoda on Github](https://github.com/sponsors/mstrYoda/)