Please purchase the course to watch this video.

Full Course
Full Course
Testing code that interacts with HTTP services is crucial for ensuring reliability, yet traditional testing methods can be problematic due to dependencies on external networks which may not always be available. This lesson introduces effective strategies for testing HTTP requests without relying on third-party services, highlighting the use of the net/http/httptest
package in Go. By creating a local, controlled test server, developers can validate that their requests are functioning correctly without the risk of flaky tests or rate limits. Key techniques discussed include using integration tests to send actual requests to a custom server, validating the response, and asserting the properties of HTTP requests. This approach empowers developers to conduct comprehensive tests that simulate real-world scenarios while maintaining full control over the testing environment.
No links available for this lesson.
When it comes to writing code that works with an HTTP service, at some point you're going to want to be able to test that your code works correctly. However, knowing how to write tests that interface with HTTP can somewhat be elusive, given the fact that you don't actually want your tests to be making network requests. Instead, you want to be controlling the environment that your test runs in, so that you can test all of the different strategies that come back, and you're not dependent on a third-party dependency, which could or couldn't break.
To show how this works, here I have a really simple project. This project has a function called getRequest
, which accepts a context.context
as the first parameter and a URL string, and it'll make a simple getRequest
to that URL and return the actual body. You can find a link to this code in the lesson description down below if you want to follow along.
If I go ahead and run this code using the go run
command, you can see it currently makes a request to the HTTP bin.org
service, at /get
, which is defined inside of the main
function, where we're actually calling this function, parsing in our context, which is a signal.notify
context, and parsing in the URL of the HTTP bin service. Pretty simple.
However, we want to actually go ahead and test this code, in order to make sure it does what it says it's doing, which is sending a request to the provided URL, and just returning the body in a byte slice. In order to do so, I've defined a simple test sending request function, but we don't want to just send up a request to any service and check the result that comes back. For starters, we may get rate limited by the actual service, as they won't appreciate us sending tests in an automated way, but we also run into the issue that this test will be flaky.
For instance, if our application runs on an environment that doesn't have an internet access, say we're writing some code and we're traveling on a plane, or if there just happens to be some network issues at the time, or if the service that we're testing against ever goes down. Therefore, in order to solve this, we have two approaches we can take. The first is to use mocking and interfaces, which we did talk about earlier in this course. And whilst this approach works, sometimes it can be a bit much.
Instead, it would be nice to be able to test that we actually sent a proper request to a service. So instead, we're going to make use of an integration test, which is where we will send an actual request, but we'll send it to a server that we own and have configured, and we can read the results from. To do so, we're going to make use of the HTTP test package, found under net/http/httptest
, which provides a number of utilities for HTTP testing.
Here you can see we can use it to capture responses, and we can also use it to create a test HTTP server. As the documentation says, a server is an HTTP server listening on a system chosen port on the local loopback interface, i.e. 127.0.0.1
, for use in end-to-end HTTP tests. And it gives us a nice little example of how we can use it. As you can see, it's pretty simple.
So let's go ahead and actually implement this for our own testing. To begin, let's go ahead and create a new variable called testServer
, or TS
, and we'll set it to be the http.test.newServer
function, in which case we need to specify an HTTP handler. Let's go ahead and set this to be an HTTP handler function, as follows, and then we'll set the func(w http.ResponseWriter, r *http.Request)
.
As you can see, this function starts to get quite long. Then with our HTTP test server defined, let's go ahead and just write some data back on the actual writer. Because the W
value of the HTTP response writer conforms to the IO.writer
interface, then we can go ahead and just use the fmt.Fprintf
method, let's say, passing in the writer as follows.
And we'll just go ahead and print out, hello, world
. With that, the test server should now be up and running, and we can go ahead and use it. Let's go ahead and add a deferred statement to TS.Close
, just so we close down this test server once we're done. Next, we can go ahead and actually call our getRequest
method, which takes a context.context
. We'll just go ahead and pass in a context.Background
for the moment. You may want to actually pass in a timeout context in this, just so the request doesn't take too long, but a background context is fine in this situation.
Then we need to go ahead and pass in the URL, preferably the one of our HTTP test server. Just as an aside, whenever you need to test with HTTP, it's good to be able to inject the URL you wish to send to, whether it's in the actual function itself, or if you're creating something like an HTTP client, which we will do in the final module of this course, such as follows:
type Client struct {
BaseURL string
}
func (c *Client) Get(...) {
// method implementation
}
Then you could go ahead and just set this to be a property of the actual client struct, and you would then create a method called, say, Get
, let's say, and then you have access to the base URL within this. That way, you could just inject the actual base URL to this client, and if you use a constructor or something similar, you can always set a default value that way.
In our case, however, we're going to be accepting the URL as an actual parameter, so we can go ahead and pass this in to our request as follows, which we can do by using the URL field of the actual test server, which provides a fully formed URL that we can use. Let's go ahead and capture the data, and we'll capture an error as follows.
Now, all we need to do is go ahead and validate that this is correct. First, let's go ahead and make sure that the error is actually nil
, so we can go ahead and do if err != nil
, and if it does exist, we'll just go ahead and do t.Fatal(fmt.Errorf("error exists when it shouldn't"))
, let's say, and then we can go ahead and actually check that our data is correct. Let's go ahead and begin by defining our want, which in this case is going to be hello world
, and then we could do if want != string(data)
.
We're going to cast that, and scroll down a bit as well. Then again, we can go ahead and do the t.Fatal
, passing in expected %s, got %s
, want, string(data). It's been a while since we wrote these. I would recommend using your assertion library if you have it available.
Anyway, with that defined, we can go ahead and now run our test function, and I'm going to pass in the -v
flag so that we can get some verbose output, and as you can see, our test is passing. Just to be sure that this is actually the case, let's go ahead and actually print out fmt.Println(data)
and then fmt.Println(string(data))
.
Now, if I go ahead and run this again, we should see the data being printed out, which it is, and that's the data that we're getting back from our results. As you can see, this is pretty good for being able to test the response that comes back from a predetermined server.
But what if you want to be able to test some of the properties that you actually send to the server? Well, in this case, you can just do test assertions within your actual test server handler. For example, let's say we want to make sure that the request body that we get is an actual get request.
Well, we can just do so by checking the actual request method. So if r.Method != http.MethodPost
, then we can go ahead and just call t.Fatal(fmt.Errorf("expected POST, got %s", r.Method))
. Now, if we go ahead and run this, you can see our test is failing, and we also get an error back as well.
This is because the HTTP server is failing, so I wouldn't use a t.Fatal
. I would do a t.Log
, and we can do a t.Log(fmt.Errorf("expected POST, got %s", r.Method))
, and then we can just do a t.Fail()
here just so that we mark the test fail and then we won't actually panic in the server.
Now, when I go ahead and run this, you can see that the data comes back correctly. We don't get an error, but we do get a failure letting us know that we expected POST, but we got a GET. By using this approach, you can then test all of the other various methods that you receive from your HTTP request, such as the actual request body, the request URL, and any query parameters that you may receive within.
That's a very basic, high-level overview of how you're able to test HTTP requests when it comes to writing code. As I mentioned before, the HTTP test package provides a lot of functionality in order to be able to test HTTP, such as using the HTTP response recorder type, which if you were testing a server or an HTTP handler, you could actually just pass into this response writer and it would give you access to all of the properties that could potentially be written, or if it's just creating a brand new HTTP request that you can use for sending up.
With that, we've managed to take a look at a lot more advanced concepts when it comes to working in both the Go file system and when it comes to working with HTTP. Over the next module, we're going to start applying all of these advanced concepts that we've learned into actually building a number of powerful command line applications that will make use of the file system of HTTP requests and everything else that we've learned throughout this course.
If you're watching in the early bird special, then this is the point where I'm breaking in order to get the course released. So expect to see the rest of the modules for this course coming over the next couple of months. In any case, I hope you've enjoyed the course so far, and if you have any feedback for me, then please do let me know.
I'd love to hear your thoughts, what I can improve, what else you would like to see, and how I can make the course better for you. Otherwise, I look forward to seeing you in the next section, and thank you so much for watching up to this point. I'm glad to see you made it all the way through the course so far. Again, a big thank you from me. This is my first course that I've ever done, and it's been a huge amount of work so far, but I've absolutely loved creating it and I really hope that you've got value out of it as well.