Mobile Backend as a Service (MBaaS) platforms accelerate development by providing ready-made backend infrastructure. However, testing MBaaS integrations requires different strategies than traditional API testing. This guide covers testing Firebase, AWS Amplify, and Supabase in mobile applications.

Understanding MBaaS Testing Challenges

Unique Characteristics

FeatureTraditional APIMBaaS
AuthenticationCustom endpointsManaged (OAuth, Social Login)
Data SyncRequest/ResponseReal-time listeners
Offline ModeManual implementationBuilt-in caching
File StorageCustom upload endpointsManaged storage buckets
Push NotificationsThird-party servicesIntegrated messaging
Testing ApproachHTTP mockingSDK mocking + Emulators

Testing Strategy

1. Unit Tests → Mock MBaaS SDK
2. Integration Tests → Use Emulators (Firebase) or Staging Projects
3. E2E Tests → Real MBaaS instance with test data
4. Performance Tests → Monitor quotas and throttling

When testing MBaaS platforms, API performance testing becomes crucial for monitoring quotas, latency, and real-time synchronization efficiency.

Firebase Testing

Firebase Local Emulator Suite

Setup:

# Install Firebase CLI
npm install -g firebase-tools

# Initialize Firebase in project
firebase init

# Start emulators
firebase emulators:start

# Available emulators:
# - Authentication (port 9099)
# - Firestore (port 8080)
# - Realtime Database (port 9000)
# - Cloud Storage (port 9199)
# - Cloud Functions (port 5001)

firebase.json Configuration:

{
  "emulators": {
    "auth": {
      "port": 9099
    },
    "firestore": {
      "port": 8080
    },
    "storage": {
      "port": 9199
    },
    "functions": {
      "port": 5001
    },
    "ui": {
      "enabled": true,
      "port": 4000
    }
  }
}

Android Firebase Testing

Setup (build.gradle.kts):

dependencies {
    implementation(platform("com.google.firebase:firebase-bom:32.7.0"))
    implementation("com.google.firebase:firebase-firestore-ktx")
    implementation("com.google.firebase:firebase-auth-ktx")

    androidTestImplementation("androidx.test:rules:1.5.0")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}

Firestore Test (UserRepositoryTest.kt):

import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.FirebaseFirestoreSettings
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import androidx.test.ext.junit.runners.AndroidJUnit4

@RunWith(AndroidJUnit4::class)
class UserRepositoryTest {

    private lateinit var firestore: FirebaseFirestore
    private lateinit var userRepository: UserRepository

    @Before
    fun setUp() {
        // Connect to emulator
        firestore = FirebaseFirestore.getInstance().apply {
            useEmulator("10.0.2.2", 8080) // Android emulator localhost
            firestoreSettings = FirebaseFirestoreSettings.Builder()
                .setPersistenceEnabled(false)
                .build()
        }

        userRepository = UserRepository(firestore)
    }

    @Test
    fun testCreateUser() = runTest {
        val user = User(
            id = "test-user-1",
            username = "testuser",
            email = "test@example.com"
        )

        userRepository.createUser(user)

        // Verify user exists in Firestore
        val snapshot = firestore.collection("users")
            .document("test-user-1")
            .get()
            .await()

        assertTrue(snapshot.exists())
        assertEquals("testuser", snapshot.getString("username"))
    }

    @Test
    fun testRealtimeUserUpdates() = runTest {
        val updates = mutableListOf<User>()

        // Listen to real-time updates
        val job = launch {
            userRepository.observeUser("test-user-1").collect { user ->
                user?.let { updates.add(it) }
            }
        }

        delay(100) // Wait for listener to attach

        // Create user
        val user = User(id = "test-user-1", username = "original")
        userRepository.createUser(user)

        delay(100)

        // Update user
        userRepository.updateUsername("test-user-1", "updated")

        delay(100)

        job.cancel()

        // Verify we received both updates
        assertEquals(2, updates.size)
        assertEquals("original", updates[0].username)
        assertEquals("updated", updates[1].username)
    }

    @Test
    fun testOfflineMode() = runTest {
        // Enable offline persistence
        firestore.firestoreSettings = FirebaseFirestoreSettings.Builder()
            .setPersistenceEnabled(true)
            .build()

        val user = User(id = "offline-user", username = "offline-test")

        // Write while "online"
        userRepository.createUser(user)

        // Simulate offline mode
        firestore.disableNetwork().await()

        // Read from cache
        val cachedUser = userRepository.getUser("offline-user")
        assertNotNull(cachedUser)
        assertEquals("offline-test", cachedUser?.username)

        // Attempt write while offline
        userRepository.updateUsername("offline-user", "updated-offline")

        // Re-enable network
        firestore.enableNetwork().await()

        delay(500) // Wait for sync

        // Verify offline write was synced
        val syncedUser = userRepository.getUser("offline-user")
        assertEquals("updated-offline", syncedUser?.username)
    }
}

Authentication Testing:

import com.google.firebase.auth.FirebaseAuth

class AuthRepositoryTest {

    private lateinit var auth: FirebaseAuth

    @Before
    fun setUp() {
        auth = FirebaseAuth.getInstance().apply {
            useEmulator("10.0.2.2", 9099)
        }
    }

    @Test
    fun testEmailPasswordSignUp() = runTest {
        val email = "newuser@test.com"
        val password = "password123"

        val result = auth.createUserWithEmailAndPassword(email, password).await()

        assertNotNull(result.user)
        assertEquals(email, result.user?.email)
        assertTrue(result.user?.isEmailVerified == false)
    }

    @Test
    fun testEmailVerification() = runTest {
        val user = auth.createUserWithEmailAndPassword(
            "verify@test.com",
            "password123"
        ).await().user

        assertNotNull(user)

        // Send verification email (mocked in emulator)
        user!!.sendEmailVerification().await()

        // In emulator, we can manually verify
        // In real tests, use Firebase Admin SDK to verify
    }

    @Test
    fun testInvalidCredentials() = runTest {
        assertThrows<FirebaseAuthException> {
            auth.signInWithEmailAndPassword(
                "nonexistent@test.com",
                "wrongpassword"
            ).await()
        }
    }
}

iOS Firebase Testing

Understanding mobile testing for iOS and Android platforms is essential when implementing MBaaS solutions across different mobile environments.

Setup (Package.swift):

dependencies: [
    .package(url: "https://github.com/firebase/firebase-ios-sdk", from: "10.20.0")
]

Firestore Test (UserRepositoryTests.swift):

import XCTest
import FirebaseCore
import FirebaseFirestore
@testable import YourApp

class UserRepositoryTests: XCTestCase {
    var firestore: Firestore!
    var userRepository: UserRepository!

    override func setUpWithError() throws {
        try super.setUpWithError()

        // Configure Firebase for testing
        let settings = FirestoreSettings()
        settings.host = "localhost:8080"
        settings.isSSLEnabled = false

        firestore = Firestore.firestore()
        firestore.settings = settings

        userRepository = UserRepository(firestore: firestore)
    }

    func testCreateUser() async throws {
        let user = User(
            id: "test-user-1",
            username: "testuser",
            email: "test@example.com"
        )

        try await userRepository.createUser(user)

        let snapshot = try await firestore
            .collection("users")
            .document("test-user-1")
            .getDocument()

        XCTAssertTrue(snapshot.exists)
        XCTAssertEqual("testuser", snapshot.data()?["username"] as? String)
    }

    func testRealtimeUpdates() async throws {
        var updates: [User] = []
        let expectation = expectation(description: "Receive updates")
        expectation.expectedFulfillmentCount = 2

        let listener = userRepository.observeUser(id: "test-user-1") { user in
            if let user = user {
                updates.append(user)
                expectation.fulfill()
            }
        }

        // Create user
        try await userRepository.createUser(
            User(id: "test-user-1", username: "original", email: "test@example.com")
        )

        // Update user
        try await Task.sleep(nanoseconds: 100_000_000)
        try await userRepository.updateUsername(id: "test-user-1", username: "updated")

        await fulfillment(of: [expectation], timeout: 2.0)

        listener.remove()

        XCTAssertEqual(2, updates.count)
        XCTAssertEqual("original", updates[0].username)
        XCTAssertEqual("updated", updates[1].username)
    }
}

AWS Amplify Testing

Amplify Mock Backend

Setup:

# Install Amplify CLI
npm install -g @aws-amplify/cli

# Initialize Amplify
amplify init

# Add API (GraphQL or REST)
amplify add api

# Start mock server
amplify mock api

# Start all mocks (auth, api, storage)
amplify mock

Amplify Configuration (amplifyconfiguration.json):

{
  "api": {
    "plugins": {
      "awsAPIPlugin": {
        "todoAPI": {
          "endpointType": "GraphQL",
          "endpoint": "http://localhost:20002/graphql",
          "region": "us-east-1",
          "authorizationType": "API_KEY"
        }
      }
    }
  }
}

Android Amplify Testing

Setup (build.gradle.kts):

dependencies {
    implementation("com.amplifyframework:aws-api:2.14.0")
    implementation("com.amplifyframework:aws-datastore:2.14.0")
    implementation("com.amplifyframework:aws-auth-cognito:2.14.0")

    testImplementation("org.mockito:mockito-core:5.8.0")
    testImplementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
}

GraphQL API Test:

import com.amplifyframework.api.graphql.model.ModelQuery
import com.amplifyframework.api.graphql.model.ModelMutation
import com.amplifyframework.datastore.generated.model.Todo

class TodoRepositoryTest {

    private lateinit var todoRepository: TodoRepository

    @Before
    fun setUp() {
        // Configure Amplify to use mock endpoint
        Amplify.configure(
            AmplifyConfiguration.builder(
                ApplicationProvider.getApplicationContext()
            )
            .addPlugin(AWSApiPlugin())
            .build(),
            ApplicationProvider.getApplicationContext()
        )

        todoRepository = TodoRepository()
    }

    @Test
    fun testCreateTodo() = runTest {
        val todo = Todo.builder()
            .name("Test Todo")
            .description("Testing Amplify")
            .build()

        val result = todoRepository.createTodo(todo)

        assertNotNull(result)
        assertEquals("Test Todo", result.name)
    }

    @Test
    fun testQueryTodos() = runTest {
        // Create test data
        val todo1 = Todo.builder().name("Todo 1").build()
        val todo2 = Todo.builder().name("Todo 2").build()

        todoRepository.createTodo(todo1)
        todoRepository.createTodo(todo2)

        // Query all todos
        val todos = todoRepository.getAllTodos()

        assertTrue(todos.size >= 2)
        assertTrue(todos.any { it.name == "Todo 1" })
        assertTrue(todos.any { it.name == "Todo 2" })
    }

    @Test
    fun testSubscribeToTodos() = runTest {
        val updates = mutableListOf<Todo>()
        val subscription = todoRepository.subscribeTo Todos { todo ->
            updates.add(todo)
        }

        delay(100)

        // Create new todo
        todoRepository.createTodo(
            Todo.builder().name("Subscribed Todo").build()
        )

        delay(500)

        subscription.cancel()

        assertTrue(updates.any { it.name == "Subscribed Todo" })
    }
}

DataStore Offline Testing

import com.amplifyframework.datastore.DataStoreConfiguration

class OfflineDataStoreTest {

    @Test
    fun testOfflineDataSync() = runTest {
        // Configure DataStore with custom sync
        val config = DataStoreConfiguration.builder()
            .syncInterval(1, TimeUnit.MINUTES)
            .build()

        Amplify.addPlugin(AWSDataStorePlugin(config))
        Amplify.configure(context)

        // Save data while offline
        val todo = Todo.builder()
            .name("Offline Todo")
            .build()

        Amplify.DataStore.save(todo).await()

        // Verify data is in local storage
        val localTodos = Amplify.DataStore.query(Todo::class.java).await()
        assertTrue(localTodos.any { it.name == "Offline Todo" })

        // Start DataStore sync (simulating going online)
        Amplify.DataStore.start().await()

        delay(2000) // Wait for sync

        // Verify data synced to cloud
        val cloudTodos = Amplify.API.query(
            ModelQuery.list(Todo::class.java)
        ).await()

        assertTrue(cloudTodos.data.any { it.name == "Offline Todo" })
    }
}

Supabase Testing

Supabase Local Development

Setup:

# Install Supabase CLI
npm install -g supabase

# Initialize Supabase
supabase init

# Start local Supabase
supabase start

# Services available:
# - PostgreSQL (port 54322)
# - API (port 54321)
# - Auth (port 54321)
# - Storage (port 54321)
# - Studio (port 54323)

Android Supabase Testing

Setup (build.gradle.kts):

dependencies {
    implementation("io.github.jan-tennert.supabase:postgrest-kt:2.0.0")
    implementation("io.github.jan-tennert.supabase:realtime-kt:2.0.0")
    implementation("io.github.jan-tennert.supabase:gotrue-kt:2.0.0")

    testImplementation("io.kotest:kotest-runner-junit5:5.8.0")
}

Supabase Client Configuration:

val supabase = createSupabaseClient(
    supabaseUrl = "http://localhost:54321",
    supabaseKey = "test-anon-key"
) {
    install(Postgrest)
    install(GoTrue)
    install(Realtime)
}

Database Test:

import io.github.jan.supabase.postgrest.from

class UserRepositorySupabaseTest {

    private val supabase = createTestSupabaseClient()
    private val userRepository = UserRepository(supabase)

    @Test
    fun testCreateUser() = runTest {
        val user = UserDto(
            username = "testuser",
            email = "test@example.com"
        )

        val created = supabase.from("users")
            .insert(user)
            .decodeSingle<UserDto>()

        assertEquals("testuser", created.username)
        assertNotNull(created.id)
    }

    @Test
    fun testQueryWithFilters() = runTest {
        // Insert test data
        supabase.from("users").insert(
            listOf(
                UserDto(username = "alice", email = "alice@example.com"),
                UserDto(username = "bob", email = "bob@example.com")
            )
        )

        // Query with filter
        val users = supabase.from("users")
            .select {
                filter {
                    eq("username", "alice")
                }
            }
            .decodeList<UserDto>()

        assertEquals(1, users.size)
        assertEquals("alice", users[0].username)
    }

    @Test
    fun testRealtimeSubscription() = runTest {
        val updates = mutableListOf<UserDto>()

        val channel = supabase.realtime.createChannel("users")
        channel.postgresChangeFlow<PostgresAction.Insert>(
            schema = "public",
            table = "users"
        ).onEach { change ->
            updates.add(change.record)
        }.launchIn(this)

        channel.subscribe()

        delay(200)

        // Insert new user
        supabase.from("users").insert(
            UserDto(username = "realtime-user", email = "rt@example.com")
        )

        delay(500)

        channel.unsubscribe()

        assertTrue(updates.any { it.username == "realtime-user" })
    }
}

Testing Best Practices

1. Isolate Test Data

@Before
fun setUp() {
    // Use test-specific collections/tables
    val testCollectionPrefix = "test_${UUID.randomUUID()}_"
    firestore.collection("${testCollectionPrefix}users")
}

@After
fun tearDown() {
    // Clean up test data
    deleteTestCollections()
}

2. Mock External Dependencies

class UserRepository(
    private val firestore: FirebaseFirestore,
    private val analytics: Analytics = FirebaseAnalytics.getInstance()
) {
    // In tests, inject mock analytics
}

// Test
val mockAnalytics = mock<Analytics>()
val repository = UserRepository(firestore, mockAnalytics)

3. Test Offline/Online Transitions

@Test
fun testOnlineOfflineTransition() = runTest {
    // Start online
    val user = userRepository.getUser("user-1")
    assertNotNull(user)

    // Go offline
    firestore.disableNetwork().await()

    // Read from cache
    val cachedUser = userRepository.getUser("user-1")
    assertEquals(user, cachedUser)

    // Go back online
    firestore.enableNetwork().await()

    // Verify sync
    val syncedUser = userRepository.getUser("user-1")
    assertEquals(user, syncedUser)
}

4. Performance and Quota Testing

@Test
fun testFirestoreReadQuotas() = runTest {
    val startReads = getFirestoreReadCount()

    // Perform operations
    userRepository.batchGetUsers(userIds)

    val endReads = getFirestoreReadCount()
    val readsUsed = endReads - startReads

    // Verify we're not exceeding expected reads
    assertTrue(readsUsed <= userIds.size)
}

Security is paramount when working with authentication and data storage in MBaaS platforms. Implementing mobile security testing practices ensures your backend services are protected against common vulnerabilities.

CI/CD Integration

GitHub Actions with Firebase Emulators

name: MBaaS Tests

on: [push, pull_request]

jobs:
  firebase-tests:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install Firebase CLI
        run: npm install -g firebase-tools

      - name: Start Firebase Emulators
        run: firebase emulators:start --only firestore,auth &

      - name: Wait for Emulators
        run: sleep 10

      - name: Run Android Tests
        run: ./gradlew connectedAndroidTest

      - name: Stop Emulators
        run: firebase emulators:stop

Conclusion

MBaaS testing requires:

  • Local Emulators: Firebase, Amplify mock, Supabase local
  • Real-time Testing: Subscriptions, listeners, data sync
  • Offline Testing: Cache validation, sync verification
  • Performance Monitoring: Quota tracking, optimization

Testing Strategy:

  1. Unit tests with mocked SDKs
  2. Integration tests with emulators
  3. E2E tests with staging MBaaS instances
  4. Monitor production metrics

MBaaS platforms accelerate development but require disciplined testing to ensure reliability, offline support, and cost optimization.