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
Feature | Traditional API | MBaaS |
---|---|---|
Authentication | Custom endpoints | Managed (OAuth, Social Login) |
Data Sync | Request/Response | Real-time listeners |
Offline Mode | Manual implementation | Built-in caching |
File Storage | Custom upload endpoints | Managed storage buckets |
Push Notifications | Third-party services | Integrated messaging |
Testing Approach | HTTP mocking | SDK 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:
- Unit tests with mocked SDKs
- Integration tests with emulators
- E2E tests with staging MBaaS instances
- Monitor production metrics
MBaaS platforms accelerate development but require disciplined testing to ensure reliability, offline support, and cost optimization.