Skip to content

Mock Data Strategies for Faster Android App Development

Updated: at 11:36 PM

When developing Android applications that interact with backend services, developers often face challenges like backend unavailability or incomplete APIs. Using mock data and mock servers can significantly speed up the development process.

Table of contents

Open Table of contents

Introduction

Imagine we’re building a GitHub app to display user profiles and repositories. The backend team has provided API contracts for these features, but the APIs won’t be available until the weekend. Meanwhile, the management team wants the app ready for testing by the beginning of next week.

Here are the API contracts:

  1. Get user’s profile
GET /users/{username:String}
Response:
{  
  "login": "String",  
  "name": "String",  
  "avatar_url": "String",  
  "bio": "String",  
  "public_repos": Int  
}
  1. Get user’s repository
GET / users/{username:String}/repos
Response:
[  
  {  
    "id": Int,  
    "name": "String",  
    "html_url": "String",  
    "description": "String",  
    "stargazers_count": Int  
  }
]

Now, the API will be available by the weekend, but the management team wants the app to be ready for testing on the first day of next week.

Based on the provided contract, we can first create the API service interface:

interface GitHubApiService {  
    @GET("users/{username}")  
    suspend fun getUser(  
        @Path("username") username: String  
    ): GitHubUserResponse  
  
    @GET("users/{username}/repos")  
    suspend fun getUserRepos(  
        @Path("username") username: String  
    ): List<GitHubRepoResponse>  
}

Next, we will explore several techniques to handle mock implementations in the Android app, allowing us to deliver an app that meets the timeline.

Implement Mock at API service level

To work without backend APIs, we can implement a mock at the API service level to respond with mock data. With this approach, we can even run the app without an internet connection.

Here is how you can implement a mock from the API service above:

class GitHubApiMock(  
    private val context: Context  
) : GitHubApiService {  
  
    private val gson = Gson()  
  
    private fun loadJsonFromAsset(fileName: String): String {  
        return context.assets.open(fileName)
	        .bufferedReader()
	        .use { it.readText() }  
    }  
  
    override suspend fun getUser(username: String): GitHubUserResponse {  
        val json = loadJsonFromAsset("mock_user_response.json")  
        return gson.fromJson(json, GitHubUserResponse::class.java)  
    }  
  
    override suspend fun getUserRepos(
	    username: String
    ): List<GitHubRepoResponse> {  
        val json = loadJsonFromAsset("mock_repo_response.json")  
        val listType = 
	        object : TypeToken<List<GitHubRepoResponse>>() {}.type  
        return gson.fromJson(json, listType)  
    }  
}

Next, we will create response files in src/main/assets folder:

Add mock JSON mock_user_response.json to handle getUser function.

{  
  "login": "mocktocat",  
  "name": "The Mocktocat",  
  "avatar_url": "https://avatars.githubusercontent.com/u/583231?v=4",  
  "bio": "This is mock data",  
  "public_repos": 10  
}

Add mock JSON mock_repo_response.json to handle getUserRepos function.

[  
  {  
    "id": 1,  
    "name": "Hello-World",  
    "html_url": "https://github.com/octocat/Hello-World",  
    "description": "This is your first repository",  
    "stargazers_count": 42  
  },  
  {  
    "id": 2,  
    "name": "Spoon-Knife",  
    "html_url": "https://github.com/octocat/Spoon-Knife",  
    "description": "This repo is for demonstration purposes only.",  
    "stargazers_count": 30  
  }  
]

Finally, add a feature flag to switch environments.

@Module  
@InstallIn(SingletonComponent::class)  
object NetworkModule {  
    private const val IS_MOCK_ENABLED = true
    
    // fun provideOkHttpClient(): OkHttpClient 
    // fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit
    
    @Provides  
    @Singleton fun provideGitHubApiService(
        @ApplicationContext context: Context,
        retrofit: Retrofit,
    ): GitHubApiService { 
        return if (IS_MOCK_ENABLED) { 
            GitHubApiMock(context)
        } else {
            retrofit.create(GitHubApiService::class.java)
        }
    }
}

With this approach, it is lightweight and does not require any additional dependencies. It is easy to configure and adapt when there are API changes since the mock implementation is constrained by the interface.

Implement Mock using Retrofit MockWebServer

Retrofit’s MockWebServer is a powerful tool when using the Retrofit framework. It allows the device to run a localhost server that can mimic the responses from the backend server.

First, we need to add the mockwebserver dependency.

implementation("com.squareup.okhttp3:mockwebserver:4.12.0")

Next, implement the MockServer to respond with mock responses located in assets folder.

object MockServer {  
    private var mockWebServer: MockWebServer? = null  
    private val serverStartDeferred = CompletableDeferred<String>()  
    private val responseMap = mutableMapOf<String, MockResponse>()  
  
    fun start(context: Context) {  
        CoroutineScope(Dispatchers.IO).launch {  
            mockWebServer = MockWebServer().apply {  
                dispatcher = object : Dispatcher() {  
                    override fun dispatch(request: RecordedRequest): MockResponse {  
                        responseMap[request.path]?.let { return it }  
                        return MockResponse().setResponseCode(404)  
                    }  
                }  
                start(8080)  
                serverStartDeferred.complete(url("/").toString())  
            }  
            enqueueMockResponse(  
                context = context,  
                fileName = "mock_user_response.json",  
                code = 200,  
                path = "/users/octocat",  
            )  
            enqueueMockResponse(  
                context = context,  
                fileName = "mock_repo_response.json",  
                code = 200,  
                path = "/users/octocat/repos",  
            )  
        }  
    }  
  
    fun shutdown() {  
        CoroutineScope(Dispatchers.IO).launch {  
            mockWebServer?.shutdown()  
        }  
    }  
  
    private fun enqueueMockResponse(  
        context: Context,  
        fileName: String,  
        code: Int,  
        path: String  
    ) {  
        CoroutineScope(Dispatchers.IO).launch {  
            val json = loadJsonFromAsset(context, fileName)  
            val mockResponse = MockResponse()  
                .setResponseCode(code)  
                .setBody(json)  
            responseMap[path] = mockResponse  
        }  
    }  
  
    private fun loadJsonFromAsset(context: Context, fileName: String): String {  
        return context.assets.open(fileName).bufferedReader().use(BufferedReader::readText)  
    }  
  
    suspend fun getBaseUrl(): String {  
        return serverStartDeferred.await()  
    }  
}

To run and stop localhost mock server, implement this in our Application class.

@HiltAndroidApp
class MyApp : Application() {
  
    override fun onCreate() {
        super.onCreate()
        MockServer.start(this)
    }
  
    override fun onTerminate() {
        super.onTerminate()
        MockServer.shutdown()
    }
}

Finally, update the dependency code to accept the MockServer URL when the mock is enabled.

@Module  
@InstallIn(SingletonComponent::class)  
object NetworkModule {  
  
    private const val IS_MOCK_ENABLED = true
    private const val BASE_URL = "https://api.github.com/"  
    
    @Provides
    @Singleton fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {  
        val baseUrl = if (IS_MOCK_ENABLED) runBlocking { MockServer.getBaseUrl() } else BASE_URL  
        return Retrofit.Builder()  
            .baseUrl(baseUrl)  
            .client(okHttpClient)  
            .addConverterFactory(GsonConverterFactory.create())  
            .build()  
    }  
  
    @Provides  
    @Singleton fun provideGitHubApiService(retrofit: Retrofit): GitHubApiService {  
        return retrofit.create(GitHubApiService::class.java)  
    }
}

With this approach, it is fully customized and possible to run without the internet. However, it is quite complicated to set up, and we have to remember to add or remove responses from a path when it is updated. Personally, I find that implementing a mock at the API service level is cleaner since it is constrained by the interface. If any updates are made, the mock implementation should be updated accordingly.

Implement Mock using Charles Proxy

Charles Proxy is an amazing tool that enables developers to view all requests, responses, and the HTTP headers between the client and the server.

It is a tools not just for debugging but also for returning mock responses when needed, by simply pointing the local mapping to dedicated mock files.

Here is an example of local mapping to a local path that contains the mock responses.

Charles Local Mapping

With this approach, we don’t have to make any code changes. However, due to security reasons, it is not practical to map HTTPS routes. Therefore, on the Android app, we should switch to HTTP for easier configuration.

Implement Mock by Creating a Mock Server

This approach involves setting up a server that mimics the behaviors of a real backend server, responding to HTTP requests with mock responses. Personally, I prefer using Node.js and Express, because it is lightweight and easy to setup, making it fast and easy to see the outcome results.

After installing Node.js and Express, move the mock files mock_user_response.json and mock_repo_response.json to the same folder as server.js as shown below.

const express = require('express');
const fs = require('fs').promises;
const path = require('path');

const app = express();
const port = 3000;

// Helper function to read JSON file asynchronously
const readJsonFile = async (filePath) => {
    try {
        const data = await fs.readFile(filePath, 'utf8');
        return JSON.parse(data);
    } catch (err) {
        console.error('Error reading file:', err);
        throw err;
    }
};

// Route for user information
app.get('/users/:username', async (req, res) => {
    const username = req.params.username;
    console.log(`Fetching data for user: ${username}`);
    
    if (username !== 'octocat') {
        return res.status(404).json({ error: 'User not found' });
    }

    try {
        const data = await readJsonFile(path.join(__dirname, 'mock_user_response.json'));
        res.status(200).json(data);
    } catch (err) {
        res.status(500).json({ error: 'Internal Server Error' });
    }
});

// Route for user repositories
app.get('/users/:username/repos', async (req, res) => {
    const username = req.params.username;
    console.log(`Fetching repositories for user: ${username}`);
    
    if (username !== 'octocat') {
        return res.status(404).json({ error: 'User not found' });
    }

    try {
        const data = await readJsonFile(path.join(__dirname, 'mock_repo_response.json'));
        res.status(200).json(data);
    } catch (err) {
        res.status(500).json({ error: 'Internal Server Error' });
    }
});

// Start the server
app.listen(port, () => {
    console.log(`Mock server running at http://localhost:${port}`);
});

To start the mock server, we can simply run the command node server.js.

Next, update the Android code to connect to mock server when the configuration flag is enabled.

@Module  
@InstallIn(SingletonComponent::class)  
object NetworkModule {

    private const val MOCK_ENABLED = true  
    private const val BASE_URL = "https://api.github.com/"  
    private const val MOCK_URL = "http://192.168.0.105:3000/"  
    
    @Provides  
    @Singleton fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {  
        val baseUrl = if (MOCK_ENABLED) MOCK_URL else BASE_URL  
        return Retrofit.Builder()  
            .baseUrl(baseUrl)  
            .client(okHttpClient)  
            .addConverterFactory(GsonConverterFactory.create())  
            .build()  
    }
}

With this approach, it is fully customizable, allowing us to simulate errors from the server, such as responding with invalid JSON data that is not defined in the contract, handling error 500, or handling some edge cases that are impossible to reproduce when the backend server is stable.

Conclusion

In my experience, any developer working on the client side should learn how to implement mock data. These techniques speed up development, enhance testing and debugging, and lead to more robust applications. By using these methods, I have successfully met tight deadlines on various projects. I hope they prove valuable in your development process as well.

Finally, here is a screenshot of the app with mock data loaded:

App