If you have a mobile app which communicates with a server through HTTP, then you (hopefully) have tests which ensure this communication works as expected.
This usually involves providing canned responses in your tests, and doing this separately for iOS and Android. Pretty annoying, right? You’re effectively duplicating work across multiple platforms, and the static nature of the responses make it difficult to test requests that cause data to be updated in the backend. Thankfully, there’s a better way. At Intercom we’ve improved on this by recreating our server logic in Go and embedding it into our iOS and Android apps.
The old way ?
Previously we had two solutions for providing these server responses.
Android: We had a mock HTTP client return static JSON which was defined in our test code. This suffered from the typical problem where backend data updates were hard to test. A test which required four new messages to be added to a conversation required five canned responses.
iOS: We had a Node.js server which emulated our server logic, and we ran it locally on the same machine as the iOS simulator we were testing on. This allowed us to test the updating of data easily, but it meant that a testing environment required the ability to run external programs. This prevented us from using remote testing services like Firebase Test Lab and Xamarin Test Cloud.
We had almost resigned ourselves to using the Node.js server on Android too, but we decided to investigate other options and discovered gomobile, an experimental Go tool which makes it easy to use Go code inside Android and iOS apps. We decided to try using this to make our apps talk to a Go web server running on the same device.
The new way ?
The first part of this was implementing our server logic in Go. This was actually much easier than a real backend implementation as we didn’t need to worry about scaling, persistence, etc. It just needed to return the same HTTP responses as our API did in production. We used Mux to route the requests and relied on Go’s built-in JSON support to create and marshal our model objects. The common Speakeasy package defines a server that can take an Engine which decides how requests are handled.
func (server *Server) Start() {
server.Engine.Setup(server)
log.Fatal(http.ListenAndServe(":3000", server.Router))
}
func (engine *SampleEngine) Setup(server *speakeasy.Server) {
handler := engine.WrapHandler(engine.channelsResponse)
server.Router.HandleFunc("/channels", handler).Methods("GET")
}
func (engine *SampleEngine) channelsResponse(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(ChannelsResponse{engine.AppState.ChannelNameList()})
}
The second part of this is embedding the server code in your app. You can do this using the Speakeasy tool which will generate an AAR file for Android and a framework for iOS. In our apps we distribute these through internal Maven and Cocoapods repos for easy integration and versioning. After including these in your app you can access your Go code through generated JNI methods in Android and C/Objective-C methods in iOS.
// SpeakeasyApplication.java
serverEngine = Backend.newEngine();
final Server speakeasyServer = Speakeasy.newServer(serverEngine);
Thread serverThread = new Thread(new Runnable() {
@Override public void run() {
speakeasyServer.start();
}
}, "SpeakeasyServer");
serverThread.start();
How this has helped us
We’ve seen a lot of benefits to using this:
Any change to the mock server logic only needs to be done once between Android and iOS, cutting the workload in half! The less effort a test change makes, the more likely you are to update your tests and write new ones.
As well as using this for our HTTP API, we’ve also been able to use this for our WebSockets-based real-time API too. This allows us to easily simulate events like seen states or typing indicators for a message.
The app is completely self-contained, so your tests can run on any device or emulator, with or without an internet connection. You can even add buttons to your debug build which trigger push events from the server.
Building a sandbox version of your app is incredibly easy. You can add buttons to your app to send data from the server to the client (e.g. over WebSockets), making testing real-time communication much simpler.
When a developer working on one platform makes changes to the server logic, it makes it clear to developers working on other platforms that their tests need updating. If they don’t, then there’s a gap in test coverage
We’re working on ways to check that the behaviour of your mock implementation stays completely aligned with the behaviour of the real server. If they behave differently then your tests won’t be very useful ?
How to use it
Speakeasy is available on Github. In the repo you’ll find a sample Android app and setup scripts. Using it in your app is simple:
Run go get github.com/intercom/speakeasy/cmd/speakeasy
Run $GOPATH/src/github.com/intercom/speakeasy/setup.sh to set up all the dependencies needed
Create your own Go package with a type that implements the speakeasy.Engine interface
Run speakeasy build {your package path}
Add the server as a dependency in your app
Create a new engine object in your app, create a server with it and start the server on a background thread
We’ve found Speakeasy really useful over the past months, and think you will too. If you’ve any questions, drop me a mail, or come ask me a question at the Dublin Go Meetup at the Intercom office on May 4th.
Comentarios