WebSockets offer duplex communication from a non-trusted source to a server that we own across a TCP socket connection. This means that, instead of continually polling the web server for updates and having to perform the whole TCP dance with each request, we can maintain a single TCP socket connection and then send and listen to messages on said connection.
In Go’s ecosystem there are few different implementations of the WebSocket protocol. Some libraries are pure implementation of the protocol. Others though, have chosen to build on top of the WebSocket protocol to create better abstractions for their particular use-case.
Here’s a non-exhaustive list of Go WebSocket protocol implementations:
x/net/websocket(from Go sub-repository packages)
In this article we will use the excellent
gorilla/websocket implementation of
the WebSocket protocol, from the Gorilla Web
Toolkit project. You will notice that
testing WebSocket is not much different from testing HTTP
servers. Still, there are aspects of
WebSockets that we have to take into account while testing.
One of the businesses whose backbone is real-time communication are online auction houses. During an auction, seconds make the difference between winning or losing a collectible item that you have been wanting for so long.
Let’s use a simple auction application powered by
gorilla/websocket as an
example for this article.
First, we will define two very simple entities
Auction that we will
use in our WebSocket handlers. The
Auction will receive a
Bid method that
we will use to place a new bid on the
Let’s look at the
Bid types, in all of their glory:
Both of the types are fairly simple, encapsulating very little data. The
NewAuction constructor function builds an auction with a duration,
and a slice of
We will place a bid on an auction through the
Bid method is where the bidding magic happens. It takes an
amount and a
userID as arguments and adds a
Bid to the
it checks if the
Auction has already closed and that the new bid
larger than the
amount of the largest bid. If any of these conditions are not
true, it will return an appropriate error to the caller.
Having the types and the
Bid method out of the way, let’s dive into the
Imagine a web frontend that can place bids on an auction in real time. With
every JSON message it sends over WebSockets it will supply the identifier of
the user placing the bid (
UserID) and the amount (
Amount) of the bid. Once
the server accepts the message, it will place the bid and reply with a
meaningful answer to the client.
On the server side, this communication will be done by a
It will handle all of the WebSocket intricacies, with a few notable steps:
- Upgrade the incoming HTTP connection to a WebSocket one
- Accept incoming messages from a client
- Decode bid from the inbound message
- Place the bid
- Send an outbound message with the reply to the client
Let’s write such a handler.
First, let’s define the
outbound message types:
Both of them represent the in/outbound messages respectively, which will be the
data flowing between the client and the server. The
inbound message will
represent a bid, while the
outbound type represents a simple message with
some text in its
Next, let’s define the
bidsHandler, including its
containing the HTTP connection upgrade:
First, we define a
websocket.Upgrader, which takes the
*http.Request from the handler and upgrades the connection. Because
this is just an example application, the
upgrader.CheckOrigin method will
only return a
true bool, without checking the origin of the incoming request.
upgrader finishes with the connection upgrade, it returns a
*websocket.Conn object, stored in the
ws variable. The
will receive all of the incoming messages, where our handler will be reading
from. Also, the handler will be writing messages to the
which will send an outbound message to the client.
Let’s add the message loop next:
for loop does a few things. First, it reads a new WebSocket message
ws.ReadMessage(), which returns the type of the message (binary or
text), the message itself (
m) and a potential error (
err). Then, it checks
the error if the client has closed the connection unexpectedly.
Once the error handling is completed and the message is retrieved, we decode it
json.Unmarshal into the
in inbound message. Once
in is available,
bh.auction.Bid which places a bid on the auction, using the amount
of the bid (
in.Amount) and the ID of the bidder (
in.UserID) as arguments.
Bid method returns a bid (
bid) and an error (
After the bid is placed, we use
json.Marshal to convert an
with the bid confirmation message encapsulated to slice of bytes (
Then we send the bytes to the client using the
ws.WriteMessage method, which
concludes the request-response server loop.
We can ignore the client side for now. Let’s now see how we can test this WebSockets handler code.
Testing WebSockets handlers
Although writing WebSocket handlers is more involved relative to ordinary HTTP handlers, testing them is simple. In fact, testing WebSockets handlers is as simple as testing HTTP handlers. This is because WebSockets are built on HTTP, so testing WebSockets is done using the same tools that testing HTTP servers is done with.
We will begin by adding the test setup:
First, we begin by defining the testcase type. It has a
name, which is the
human-readable name of the test case. Also, each testcase has a
duration which will be used to create a test
Auction with. The
testcase also has an
message and an
reply - which is
what the test case will send to and expect in return from the handler.
After, in the
TestBidsHandler we add three different test cases – one where
the client wants to place a bad bid, that is lower than the largest bid,
another test case where the client adds a good bid and a third one where the
client bids on an expired auction.
for loop, for each of the test cases, we create a subtest which uses
NewAuction constructor to create a new test auction. We also create a
bidsHandler that takes the newly created
Auction as an attribute.
Let’s finish off the subtest function:
We added few new functions to the subtest function body. The
create a test server and upgrade it to a WebSocket connection, returning both
the server and the WebSocket connections. Then, the
sendMessage function will
send the message from the test case to the test server throught the WebSocket
connection. After that, through the
receiveWSMessage we will retrieve the
reply from the server and assert for its correctness by comparing it to the
reply of the test case.
So, what do each of these small functions do? Let’s break them down one by one.
newWSServer function will use the
function to mount the handler on a test HTTP server. Once that is done, it will
convert the server’s
URL to a WebSocket URL through the
(It simply replaces the
http protocol to a
in the URL.`)
To establish a WebSocket connection, we use the
which is a dialer with all fields set to the default values. We invoke the
Dial method on the dialer, with the WebSocket server URL (
returns the WebSocket connection.
sendMessage function takes an
inbound message as argument with the WebSocket
ws). It marshals the message into a JSON and it sends it over the
WebSocket connection as a binary message.
receiveWSMessage takes the WebSocket connection (
ws) as argument and it
fetches a message using
ws.ReadMessage(). Once the message is successfully
retrieved, it unmarshals it into a
outbound message using
As a last step,
receiveWSMessage returns the
outbound message to the test,
so the test can continue with its assertions.
If we would run the tests, we will see them passing:
You can see the example code on Github.
More WebSockets reading
If you would like to learn more about the details of the WebSocket protocol,
I recommend reading
RFC 6455 which defines
the protocol itself. In addition, you can read more in follow-up RFCs regarding
the WebSocket protocol:
- Clarifying Registry Procedures for the WebSocket Subprotocol Name Registry
- Well-Known URIs for the WebSocket Protocol
- Bootstrapping WebSockets with HTTP/2