Testing Iris RESTful API
26 Sep 2016Iris and Mainflux
Iris is a HTTP framework written in Go. We have choosen Iris as HTTP API protocol development framework for Mainflux IoT platform because of it’s impressive bechmarks and simplicity. As robustness and consequently test coverage is very important for any serious and professional project, this article explains Iris RESTful API testing techniques on the example of Mainflux server.
EDIT 1 - Due to the needs for leanest approach possible, Mainflux project decided to strip Iris (and any other framework) and stick with bare Go standard lib for HTTP development.
EDIT 2 - Interesting discussion on this subject can be found on Reddit.
Mainflux project adopts TDD approach, and Go is a great language with powerful testing capabilities - so it is well suited for writing test scenarios in an organized and well designed manner.
Go Testing Framework
Go Testing
framework is nicely explained here.
In short, recipe for testing <my_module>.go
is following:
- Create
<my_module>_test.go
and make it belong to the same package (it is actually package we are testing, not only this particular module) - In your test, use
import "testing"
- Define function
func TestSomething(t *testing.T)
- Optionally, if some prerequisite set-up is needed you can use
func TestMain(m *testing.M)
As explained here, tests can be run via go test
in the current dir. In order to test the whole project, Travis for example uses:
go test -v ./...
So this is the way you should test your project from the project root.
Testing Go HTTP RESTful APIs
Usually, for testing HTTP servers Go already provides testing library called httptest. Some examples of using this library can be found here, here, here or here. Or even here.
However, Iris does not use Go’s net/http
- it uses fasthttp (look at the imports here). This means that you can not use httptest
to test the server.
Gin Gonic for example uses net/http
(as can be seen in imports here, so you can write tests like this.
@kataras, Iris author, reccomends usage of the httpexpect testing framework for testing Iris-based HTTP servers - please refer to this link.
Httpexpect, Fasthttp and Iris
This is a hard part ;). Let’s go slowly in code disecting.
In this httpexpect
example we can see that func IrisHandler()
is returning fasthttp.RequestHandler
, which is api.Router
, where api
is iris.New()
, i.e. Iris instance. Then here in irisTester()
function we use this handler (Router
of the Iris instance) to create httpexpect
instance.
Further, when we look FrameworkAPI
interface over here, we can see that this structure contains member function Tester()
, defined in the following way: Tester(*testing.T) *httpexpect.Expect
, which is basically a getter which fetches testFramework
member of the structure Framework
. This member is initialized via function NewTester()
in which httpexpect.Expect
instance is created based on api.Router
.
This means that every Iris server already have member testFramework
which is correct httpexpect.Expect
instance for our fasthttp handler. It is static member (begins with lowercase letter, thus not reachable from other packages) and can be obtained via api.Tester()
getter.
Mainflux Iris Test Example
Mainflux http_server_test.go
uses iris.Tester(t)
to fetch correct httpexpect
instance of the current Iris Framework started in HttpServer()
gorutine just before.
Code of the test function is following:
func TestServer(t *testing.T) {
// Config
var cfg config.Config
cfg.Parse()
go HttpServer(cfg)
// prepare test framework
if ok := <-iris.Available; !ok {
t.Fatal("Unexpected error: server cannot start, please report this as bug!!")
}
e := iris.Tester(t)
r := e.Request("GET", "/status").Expect().Status(iris.StatusOK).JSON()
fmt.Println("%v", r)
}
One more thinng should be noted - and that is the use of Available
channel. It is explained here, where we can see the comment in the code:
// Available is a channel type of bool, fired to true when the server is opened and all plugins ran
// never fires false, if the .Close called then the channel is re-allocating.
// the channel remains open until you close it.
//
// look at the http_test.go file for a usage example
Available chan bool
In test function we use this flag to block (we are syncing on the channel) untill HTTP server is started. Then we can continue with the test (i.e. sending HTTP requests).
Data Mocking, Interfaces and DockerMock
One more non-trivial thing makes Mainflux server testing more complicated - and that is the use of database. Usual practice during the test phase is that databse responses are mocked (stubbed). Similar approach is taken for other external libraries. This way we focus to testing of our module, simulating the responses from external functions (like database calls).
For this mocking strategy, Go uses interfaces - as explained here or here.
Interfaces are very important concept in Go, and be sure to know them. There is very good article that will help you understand them well.
Never the less, mocking all Mongo functions is not a trivial job. A more realistic and modern way of this kind of testing can be achieved using Docker, as described in this article. Following this principle, a dockertest project implements dockerized testing environment that firs perfectly for Mainflux use-case and can be used even with Travis CI.
Mainflux http_server_test.go uses dockertest
which is initialized in the TestMain()
function. This function is Go testing framework’s special function and you can read more about it here
Conclusion
In the conclusion of this short tutorial, I hope that it brings better unerstanding of several concepts needed for Iris HTTP server with MongoDB testing:
- Fasthttp
- Httpexpect
iris.Tester()
getter andiris.Available
sync channel- Dockertest