Quality assurance is a fundamental part of software engineering. It allows you to have confidence in the product being deployed into production, at the same time it ensures regression, allowing you to change the code later for refactoring or adding new features without breaking anything else.
The good thing about testing is that most of the times they are possible to automate in the scope of a continuous delivery pipeline (e.g. through Jenkins), so we don’t have to run them manually each time we need to release.
This post is not covering automation, it introduces Golang’s testing semantics and how it’s applied to test routines and channels.
One may want to refer to my last post on a Simple Data Processing Pipeline with Golang for a full example using Golang routines and channels. The Github’s repository includes a full set of tests for the pipeline components.
Testing in Golang
Testing in Golang is no different than testing in any other language, as the basic concepts are the same. While this is a fact, it’s also true that unlike many other languages, Golang provides an excelent testing package out of the box.
Golang testing package is designed to not exit upon a fail unless explicitly instructed to do so.
It’s pretty much similar with an exam on the high school, but in this case you need to have a 100% to succeed. Once the student delivers the exam, the teacher will check for every response and note down if the answer is right or wrong. If the student fails, there’s usually another opportunity to repeat the exam and (hopefully) provide the correct responses.
Imagine how this world would be if the the teacher checked for one response at a time, give you the exam back, you study and fix the response, teacher checks, you fix it, …, teacher checks, you fix it, teacher checks, you fix it…
Not efficient delivery
You can still force the exit upon failure if there’s no point on continuing the test. In our short metaphor, if the student does not show up, the fail is immediate.
This makes sense, if we think of a test as just a set of checks, in which some of them can be prohibitive to proceed, and some do not. To demonstrate this, let’s say we want to build an indexed counter, meaning we can have a count indexed by a string. An logic-free implementation could look like the following:
One possible test for this counter could look like:
Not being able to create the counter instance is a fatal error, we will not be able to proceed with any other check without it so we use t.FailNow() to indicate that the test shall be terminated immediately. If we run the test just now, we will have something like:
Now let’s develop our counter properly, starting with the factory method.
If we run the test now, it will be able to create the counter instance, and it will give you a report with all the check failures until the next failNow or the end of the test.
Let’s implement the increment method and run the test again.
The result of the test would be:
Testing Routines and Channels
Testing routines and channels is a bit more complex than testing anything else in Golang, but not that much, one just needs to be fully aware of how the channel primitives work. Picking up the data pipeline example example, let’s say we have a function that reads from an input channel, executes some operation over the input and writes the result to an output channel.
In order to be able to use this function in the scope of a pipeline, we need to trigger a routine that reads the input and executes the operation, another routine that waits for the operation to be complete, and return immediately the output channel so it can be used to build the rest of the pipeline.
The test for the flow above will need to do the following:
- Create an input channel
- Write some test data into it
- Call the Process function with the input channel as an argument
- Read data from the output channel
- Assert the results
It would be clean and simple though the above will not work due to the blocking nature of channel operations. Writing to a channel blocks until a reader is available for reading it. Go is able to detect deadlocks at runtime, so we would end up with an error like this:
To sort this out, one could use a buffered channel, for which writing operation only blocks if the buffer is full.
As buffered channels have a different semantics, they may not suit your test scenario. If that’s the case, the other option is to ensure that the write to the channel happens in a separate routine, though that will reduce the test readability, as shown below.
Golang testing package offers a way of easily benchmarking the critical flows. In our design we isolate our heavy operation in a separate function in order to be able to benchmark it. Let’s build a benchmark test for it.
If we tweak a bit our heavy operation just for demonstration purposes…
… the result will be enlightening:
Golang offers a complete testing package that allows not only for validating your application functionally, but also to benchmark it in a really simple way. This post demonstrated how can we test a routing that consumes from an input channel and produces to an output channel.
The testing package works having in mind that a test is a sequence of checks and the test does not need to finish because of a check fail, though it can if instructed to do so.
If you are a Java developer and a Golang enthusiast, you might want to know that Dan’s JGoTesting library brings these testing concepts into JUnit. Feel free to take a look.
subscribe via RSS