mirror of
https://github.com/vide/matedroid.git
synced 2026-01-20 00:03:17 +08:00
test: add unit tests for Settings and Dashboard ViewModels
- Add SettingsViewModelTest with tests for: - Initial state loading from DataStore - URL and token updates - Connection testing (validation and API calls) - Settings saving - Add DashboardViewModelTest with tests for: - Car loading and selection - Status fetching and refresh - Error handling - Multiple car support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
dfd98376e9
commit
2ee3f2a98a
@@ -0,0 +1,182 @@
|
||||
package com.matedroid.ui.screens.dashboard
|
||||
|
||||
import com.matedroid.data.api.models.CarData
|
||||
import com.matedroid.data.api.models.CarStatus
|
||||
import com.matedroid.data.repository.ApiResult
|
||||
import com.matedroid.data.repository.TeslamateRepository
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class DashboardViewModelTest {
|
||||
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
private lateinit var repository: TeslamateRepository
|
||||
private lateinit var viewModel: DashboardViewModel
|
||||
|
||||
private val testCar = CarData(
|
||||
carId = 1,
|
||||
displayName = "Test Tesla",
|
||||
model = "Model 3",
|
||||
vin = "TEST123"
|
||||
)
|
||||
|
||||
private val testStatus = CarStatus(
|
||||
carId = 1,
|
||||
displayName = "Test Tesla",
|
||||
state = "online",
|
||||
batteryLevel = 75,
|
||||
ratedBatteryRangeKm = 300.0,
|
||||
chargeLimitSoc = 80,
|
||||
pluggedIn = false,
|
||||
locked = true
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
repository = mockk()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
private fun createViewModel(): DashboardViewModel {
|
||||
return DashboardViewModel(repository)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadCars fetches cars and selects first one`() = runTest {
|
||||
coEvery { repository.getCars() } returns ApiResult.Success(listOf(testCar))
|
||||
coEvery { repository.getCarStatus(1) } returns ApiResult.Success(testStatus)
|
||||
|
||||
viewModel = createViewModel()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertFalse(viewModel.uiState.value.isLoading)
|
||||
assertEquals(1, viewModel.uiState.value.cars.size)
|
||||
assertEquals(1, viewModel.uiState.value.selectedCarId)
|
||||
assertEquals(testStatus, viewModel.uiState.value.carStatus)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadCars shows error when api fails`() = runTest {
|
||||
coEvery { repository.getCars() } returns ApiResult.Error("Network error")
|
||||
|
||||
viewModel = createViewModel()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertFalse(viewModel.uiState.value.isLoading)
|
||||
assertEquals("Network error", viewModel.uiState.value.error)
|
||||
assertTrue(viewModel.uiState.value.cars.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `loadCars handles empty car list`() = runTest {
|
||||
coEvery { repository.getCars() } returns ApiResult.Success(emptyList())
|
||||
|
||||
viewModel = createViewModel()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertFalse(viewModel.uiState.value.isLoading)
|
||||
assertTrue(viewModel.uiState.value.cars.isEmpty())
|
||||
assertNull(viewModel.uiState.value.selectedCarId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `selectCar updates selected car and loads status`() = runTest {
|
||||
val car1 = CarData(carId = 1, displayName = "Car 1")
|
||||
val car2 = CarData(carId = 2, displayName = "Car 2")
|
||||
val status2 = testStatus.copy(carId = 2, displayName = "Car 2")
|
||||
|
||||
coEvery { repository.getCars() } returns ApiResult.Success(listOf(car1, car2))
|
||||
coEvery { repository.getCarStatus(1) } returns ApiResult.Success(testStatus)
|
||||
coEvery { repository.getCarStatus(2) } returns ApiResult.Success(status2)
|
||||
|
||||
viewModel = createViewModel()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertEquals(1, viewModel.uiState.value.selectedCarId)
|
||||
|
||||
viewModel.selectCar(2)
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertEquals(2, viewModel.uiState.value.selectedCarId)
|
||||
assertEquals("Car 2", viewModel.uiState.value.carStatus?.displayName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refresh reloads car status`() = runTest {
|
||||
coEvery { repository.getCars() } returns ApiResult.Success(listOf(testCar))
|
||||
coEvery { repository.getCarStatus(1) } returns ApiResult.Success(testStatus)
|
||||
|
||||
viewModel = createViewModel()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
val updatedStatus = testStatus.copy(batteryLevel = 80)
|
||||
coEvery { repository.getCarStatus(1) } returns ApiResult.Success(updatedStatus)
|
||||
|
||||
viewModel.refresh()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertFalse(viewModel.uiState.value.isRefreshing)
|
||||
assertEquals(80, viewModel.uiState.value.carStatus?.batteryLevel)
|
||||
|
||||
coVerify(exactly = 2) { repository.getCarStatus(1) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refresh does nothing when no car selected`() = runTest {
|
||||
coEvery { repository.getCars() } returns ApiResult.Success(emptyList())
|
||||
|
||||
viewModel = createViewModel()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
viewModel.refresh()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
coVerify(exactly = 0) { repository.getCarStatus(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearError clears error state`() = runTest {
|
||||
coEvery { repository.getCars() } returns ApiResult.Error("Test error")
|
||||
|
||||
viewModel = createViewModel()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertEquals("Test error", viewModel.uiState.value.error)
|
||||
|
||||
viewModel.clearError()
|
||||
|
||||
assertNull(viewModel.uiState.value.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `status error is shown when status fetch fails`() = runTest {
|
||||
coEvery { repository.getCars() } returns ApiResult.Success(listOf(testCar))
|
||||
coEvery { repository.getCarStatus(1) } returns ApiResult.Error("Status error")
|
||||
|
||||
viewModel = createViewModel()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertEquals("Status error", viewModel.uiState.value.error)
|
||||
assertNull(viewModel.uiState.value.carStatus)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package com.matedroid.ui.screens.settings
|
||||
|
||||
import com.matedroid.data.local.AppSettings
|
||||
import com.matedroid.data.local.SettingsDataStore
|
||||
import com.matedroid.data.repository.ApiResult
|
||||
import com.matedroid.data.repository.TeslamateRepository
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class SettingsViewModelTest {
|
||||
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
private lateinit var settingsDataStore: SettingsDataStore
|
||||
private lateinit var repository: TeslamateRepository
|
||||
private lateinit var viewModel: SettingsViewModel
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
settingsDataStore = mockk()
|
||||
repository = mockk()
|
||||
|
||||
every { settingsDataStore.settings } returns flowOf(AppSettings())
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
private fun createViewModel(): SettingsViewModel {
|
||||
return SettingsViewModel(settingsDataStore, repository)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state loads settings from datastore`() = runTest {
|
||||
val savedSettings = AppSettings(serverUrl = "https://test.com", apiToken = "token123")
|
||||
every { settingsDataStore.settings } returns flowOf(savedSettings)
|
||||
|
||||
viewModel = createViewModel()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertEquals("https://test.com", viewModel.uiState.value.serverUrl)
|
||||
assertEquals("token123", viewModel.uiState.value.apiToken)
|
||||
assertFalse(viewModel.uiState.value.isLoading)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateServerUrl updates state`() = runTest {
|
||||
viewModel = createViewModel()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
viewModel.updateServerUrl("https://new-server.com")
|
||||
|
||||
assertEquals("https://new-server.com", viewModel.uiState.value.serverUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `updateApiToken updates state`() = runTest {
|
||||
viewModel = createViewModel()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
viewModel.updateApiToken("new-token")
|
||||
|
||||
assertEquals("new-token", viewModel.uiState.value.apiToken)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `testConnection fails with blank url`() = runTest {
|
||||
viewModel = createViewModel()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
viewModel.updateServerUrl("")
|
||||
viewModel.testConnection()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
val result = viewModel.uiState.value.testResult
|
||||
assertTrue(result is TestResult.Failure)
|
||||
assertEquals("Server URL is required", (result as TestResult.Failure).message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `testConnection fails with invalid url scheme`() = runTest {
|
||||
viewModel = createViewModel()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
viewModel.updateServerUrl("ftp://invalid.com")
|
||||
viewModel.testConnection()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
val result = viewModel.uiState.value.testResult
|
||||
assertTrue(result is TestResult.Failure)
|
||||
assertEquals("URL must start with http:// or https://", (result as TestResult.Failure).message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `testConnection succeeds with valid url`() = runTest {
|
||||
coEvery { repository.testConnection(any()) } returns ApiResult.Success(Unit)
|
||||
|
||||
viewModel = createViewModel()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
viewModel.updateServerUrl("https://valid.com")
|
||||
viewModel.testConnection()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
val result = viewModel.uiState.value.testResult
|
||||
assertTrue(result is TestResult.Success)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `testConnection shows failure when api returns error`() = runTest {
|
||||
coEvery { repository.testConnection(any()) } returns ApiResult.Error("Connection refused")
|
||||
|
||||
viewModel = createViewModel()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
viewModel.updateServerUrl("https://unreachable.com")
|
||||
viewModel.testConnection()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
val result = viewModel.uiState.value.testResult
|
||||
assertTrue(result is TestResult.Failure)
|
||||
assertEquals("Connection refused", (result as TestResult.Failure).message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saveSettings calls datastore and triggers callback`() = runTest {
|
||||
coEvery { settingsDataStore.saveSettings(any(), any()) } returns Unit
|
||||
|
||||
viewModel = createViewModel()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
viewModel.updateServerUrl("https://saved.com")
|
||||
viewModel.updateApiToken("saved-token")
|
||||
|
||||
var callbackCalled = false
|
||||
viewModel.saveSettings { callbackCalled = true }
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
coVerify { settingsDataStore.saveSettings("https://saved.com", "saved-token") }
|
||||
assertTrue(callbackCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saveSettings fails with blank url`() = runTest {
|
||||
viewModel = createViewModel()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
viewModel.updateServerUrl("")
|
||||
|
||||
var callbackCalled = false
|
||||
viewModel.saveSettings { callbackCalled = true }
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertFalse(callbackCalled)
|
||||
assertEquals("Server URL is required", viewModel.uiState.value.error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearTestResult clears test result`() = runTest {
|
||||
coEvery { repository.testConnection(any()) } returns ApiResult.Success(Unit)
|
||||
|
||||
viewModel = createViewModel()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
viewModel.updateServerUrl("https://test.com")
|
||||
viewModel.testConnection()
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
|
||||
assertTrue(viewModel.uiState.value.testResult is TestResult.Success)
|
||||
|
||||
viewModel.clearTestResult()
|
||||
|
||||
assertNull(viewModel.uiState.value.testResult)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user