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:
Davide Ferrari
2025-12-14 12:12:49 +01:00
co-authored by Claude Opus 4.5
parent dfd98376e9
commit 2ee3f2a98a
2 changed files with 376 additions and 0 deletions
@@ -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)
}
}