Mocking APIs using embedded web servers inside flutter apps
May 03, 2022
(Edit 23.05.2022) I gave a tech talk about this at Flutter Nordics Helsinki Meetup, you can watch it from here: https://youtu.be/pj-ZTEnonE0
Say you are working on an app UI. But required APIs are not ready. Yet you need to deliver the app UI implementation as complete as possible. You could;
- Hardcode API responses into app code
- Use something like mockito and work with tests
- Create a web server app and run it separately.
- Embed a web server inside the flutter app itself.
I ended up taking the last option. Skip to it below if the rest of the options seem apparent. Each option has its pros and cons, so;
Hardcoding API responses
Pros
- Easy.
- App UI behaves as expected.
- Everything can be done in the same codebase.
- No need to have a server running.
Cons
- The code is not final
- Difficult to test HTTP errors
- Difficult to test race conditions
- Difficult to test serialization / deserialization
Developing with mocks/stubs in tests
Pros
- Means that you write tests, nice.
- Ability to test HTTP errors
- Ability to test serialization/deserialization
- Mocks are in the same codebase as the app.
Cons
- Doesn’t work in end-user UI.
- End-users can’t test with it.
- Need to run the tests to work with them.
- Need to write and set up tests (not too bad anyway)
Create a web server app and run it separately
Pros
- Having an actual web server close as it gets to real API implementation
- Ability to test everything related to HTTP and REST
- Ability to test HTTP errors
- Ability to test JSON serialization / deserialization
Cons
- Need to create the web server as a separate project. If you made it in nodejs, you would need to run it separately.
- Need to figure out how to get the correct port relayed to the emulator
- What if the port and URI change? How does the app know
- Distributed release app will need to talk to the web server, so the server needs to be up and running all the time somewhere.
None of these options seemed to be covering everything we needed so. We had this idea;
Put the web server into the app
This is not too different from any app with an embedded web server. This means to have a web server process spawned when the app launches, then serve mock endpoints on localhost:3169 (whatever port you want, really). Then your app talks to itself to fetch from the mock API.
This could, of course, be achieved in multiple ways also. For example, you could embed any web server via a native module, similarly in react-native.
This was particularly simple to do using flutter. In addition, using both the same process and the same language and codebase made it easy to make and extend.
- Server restarts when you hot-restart the flutter app
- What port will be serving is in your code, so your API client knows which port to listen from.
- Re-use data models made for dart.
- Same JSON encoding/decoding approach with the actual app code.
When you compare this with the other options above;
Pros
- Server in the same app
- Same language and project
- Easy to develop
- Lots of shared code
Cons
- Maybe the port would clash with something (but you can change the port)
Implementation of this is based on flutter’s own HTTP server. So use a convenience wrapper around it with shelf package. And for making intercepting requests more declaratively similar to Express / Sinatra / Flask -ish way, used shelf_router package.
So here’s what the server code looks like;
1import 'dart:convert';23import 'package:logging/logging.dart';4import 'package:shelf/shelf.dart';5import 'package:shelf/shelf_io.dart' as shelf_io;6import 'package:shelf_router/shelf_router.dart' as shelf_router;78Logger log = Logger('LocalMockApiServer');910class LocalMockApiServer {11 static final host = 'localhost';12 static final port = 3131;13 static get baseUrl => 'http://$host:$port/';1415 late shelf_router.Router app;1617 LocalMockApiServer() {18 app = shelf_router.Router();1920 app.get('/user/account', (Request req) async {21 return JsonMockResponse.ok({22 'id': req.accessToken,23 });24 });2526 app.get('/user/investments', (Request req) async {27 return JsonMockResponse.ok([28 {29 'key': 'value',30 },31 {32 'key': 'value',33 },34 ], delay: 1200);35 });36 }3738 Future<void> start() async {39 log.info('starting...');4041 var handler = const Pipeline().addMiddleware(42 logRequests(logger: (message, isError) {43 if (isError)44 log.severe(message);45 else46 log.info(message);47 }),48 ).addHandler(app);4950 var server = await shelf_io.serve(handler, host, port);51 server.autoCompress = true;5253 log.info('serving on: $baseUrl');54 }55}5657extension on Request {58 Future<String?> bodyJsonValue(String param) async {59 return jsonDecode(await this.readAsString())?[param];60 }6162 String? get accessToken =>63 this.headers['Authorization']?.split('Bearer ').last;64}6566extension JsonMockResponse on Response {67 static ok<T>(T json, {int delay = 800}) async {68 await Future.delayed(Duration(milliseconds: delay)); // Emulate lag69 return Response.ok(70 jsonEncode(json),71 headers: {'Content-Type': 'application/json'},72 );73 }74}
Let’s break this down and see what is happening;
The gist is that we create a class called LocalMockApiServer,
which sets up the server and has an async start
method. I removed some implementation details from below for brevity. The complete class implementation is above.
1class LocalMockApiServer {2 static final host = 'localhost';3 static final port = 3131;4 static get baseUrl => 'http://$host:$port/';56 late shelf_router.Router app;78 LocalMockApiServer() {9 app = shelf_router.Router();1011 // 1: Request handler here12 // 2: Request handler here13 }1415 Future<void> start() async {16 // 3: Log processor setup here1718 var server = await shelf_io.serve(handler, host, port);19 server.autoCompress = true;2021 log.info('serving on: $baseUrl');22 }23}
Then we call the start method from our main.dart
some place between WidgetsFlutterBinding.ensureInitialized()
and runApp
1void main() async {2 // ...3 WidgetsFlutterBinding.ensureInitialized();4 // ...5 await LocalMockApiServer().start();67 runApp(8 //...9 );10}
Doing this will start the server in the background, and your app should launch as it should before. You should see the logs;
1LocalMockApiServer: starting...2LocalMockApiServer: serving on: http://localhost:3131
await
ing on LocalMockApiServer().start()
will ensure that server is up and running before your app starts calling the endpoints.
Implement your mock API endpoint handlers. These go where I marked // 1
and // 2
above;
Parsing access token from request headers and responding to it as user id (doesn’t make sense but just for testing and visibility)
1app.get('/user/account', (Request req) async {2 return JsonMockResponse.ok({3 'id': req.accessToken,4 });5});
JsonMockResponse.ok
Request.accessToken
are implemented as convenience extensions. I will explain them below.
Responding with an array as the root of the JSON;
1app.get('/inventory', (Request req) async {2 return JsonMockResponse.ok([3 {4 'key': 'value',5 },6 {7 'key': 'value',8 },9 ], delay: 1200); // Maybe you need more than the default delay10});
If you want a post request handler, swap get
with post
after app.
bodyJsonValue
method on Request
is implemented as an extension. Explained below after this block.
1app.post('/account/create', (Request req) async {2 var token = await req.bodyJsonValue('token');3 log.info('create account request for token: $token');4 return JsonMockResponse.ok({5 'id': 'mock_id_${token?.substring(0, 8)}',6 });7});
Set up the log processor. Below, this part replaces // 3
above inside the start method. This is for intercepting the server logs and plugging in whatever logging system you have. I’m using logger.
1Future<void> start() async {2 // ...34 var handler = const Pipeline().addMiddleware(5 logRequests(logger: (message, isError) {6 if (isError)7 log.severe(message);8 else9 log.info(message);10 }),11 ).addHandler(app);1213 // ...14}
And then we have a couple of quality-of-life extensions on top of flutter’s Request
and Response
. Include these after the LocalMockApiServer
definition.
Request extension adds two things;
bodyJsonValue
: Reads request body then decodes it as JSON.accessToken
: Reads standard OAuth Authorization header’s Bearer value.
1extension on Request {2 Future<String?> bodyJsonValue(String param) async {3 return jsonDecode(await this.readAsString())?[param];4 }56 String? get accessToken =>7 this.headers['Authorization']?.split('Bearer ').last;8}
JsonMockResponse is an extension based on Response
with a re-definition of ok
. Which responds with 200 status codes and encodes the given map into a JSON string as the body. Responds with an optionally configurable delay.
Since this code is likely to be repeated in every response, it saves time by not writing Content-Type headers and encoding code repeatedly.
1extension JsonMockResponse on Response {2 static ok<T>(T json, {int delay = 800}) async {3 await Future.delayed(Duration(milliseconds: delay)); // Emulate lag4 return Response.ok(5 jsonEncode(json),6 headers: {'Content-Type': 'application/json'},7 );8 }9}
This is it. Then call these endpoints as you would from any local server, as in http://localhost:3131/inventory
. Don’t forget that this server runs on the phone or emulator/simulator. Unless you are targeting desktop or web. This server won’t be accessible from your desktop environment.
This solution is a simple idea that was surprisingly easy to implement and saved me a lot of time. I was able to implement the API Client code, data models, deserialization, and serialization even before any APIs were developed. It was handy as this mock server is distributed along with the app itself, from Google play internal testing or test flight, etc. Remember that all of these codes are meant to be deleted once the actual APIs exist. Let me know what you think about this idea!