3.5 KiB
title | date | draft | tags | ||
---|---|---|---|---|---|
Adding probability to testing: Golang | 2019-05-23T17:17:17-04:00 | false |
|
In my work as a Software Engineer at Kickback Rewards Systems, I recently wrote an application that essentially reads packets from a TCP stream. It's amazing to me that writing a concurrent application that reads bytes from a TCP stream is SO EASY with Go, in less than 150 LOC! We've had similar applications in C and Python that are much larger than that at my company! Anyway, this application merely needed to mock the functionality of the production system, so that we could test the transmission of a new packet type from our client code. The client expected some entity acting as the server in a socket connection that would first read bytes from the stream to determine the length of the payload, then the payload itself. The server has to respond to the client in a similar manner, length preamble, and payload of response.
An interesting requirement arose amid development of this mock server. The requirement was that the server should simulate an actual internet connection, sometimes terminating the client's connection too early, sometimes delaying a response to the client, and sometimes just not responding at all. The trick to this though is that this wacky behavior needed to be inconsistent, something that happened seemingly randomly. To accomplish this, I developed a neat pattern using closures and function pointers.
Enter our goroutine, which is obviously not my real production code:
// Main and rest of program not included, it's not important to this example.
// Our goroutine.. called in the main socket listening loop
func handleConn(conn net.Conn) {
// Do some work and respond.
}
This is a pretty standard pattern, develop a function intended to be used as a goroutine, and call it in some loop passing a connection from a successful
net.Listener.Accept()
call. What I did to add a little chaos to this function, was utilize a little bit of closure trickery >:). To introduce any
amount of chaos to your application, a function like this will do:
// Flip a coin, and do the action on tails...
func chaos(action func()) {
if rand.Intn(10) > 5 {
// Executing our function pointer.
action()
}
}
On its own, this function may seem harmless. One may ask why you would want to flip a coin then do something if we achieve a certain result. The answer to that is simple, and that's where closures come in. Let's modify my goroutine from before to do something nasty.
func handleConn(conn net.Conn) {
// Now let's introduce some... chaos
chaos(func() {
fmt.Println("Oh no, something bad happened!")
conn.Close()
// don't worry about this.
runtime.Goexit()
})
fmt.Println("Phew, nothing's gonna happen, let's continue.")
// Do the work.
}
The way this works is simple, we pass an anonymous function as the argument to chaos, noted above as type func()
. This anonymous function that we
create has access to any variables defined in its enclosing scope. conn
, a function parameter, is defined in the same scope as the anonymous function,
thus it is perfectly valid to reference it from within the function. Now, when the function is executed in chaos
like this: action()
, it is still
holding a valid reference to the conn object, resulting in conn.Close()
being called terminating the client's connection with the server. Use this
pattern any time you would like to introduce a little bit of chaos to your testing.