Add instrumented test to test repository together with room database. Improve data handling on first boot.
Some checks failed
continuous-integration/drone/push Build was killed

This commit is contained in:
2023-02-17 15:19:00 +01:00
parent 4108512139
commit a611de6da4
9 changed files with 331 additions and 80 deletions

View File

@@ -1,22 +0,0 @@
package me.zobrist.tichucounter
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("me.zobrist.tichucounter", appContext.packageName)
}
}

View File

@@ -0,0 +1,280 @@
package me.zobrist.tichucounter
import android.content.Context
import androidx.compose.runtime.collectAsState
import androidx.lifecycle.asLiveData
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import me.zobrist.tichucounter.data.AppDatabase
import me.zobrist.tichucounter.data.GameDao
import me.zobrist.tichucounter.data.GameWithScores
import me.zobrist.tichucounter.data.RoundDao
import me.zobrist.tichucounter.repository.GameRepository
import org.junit.After
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.io.IOException
import java.util.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class RepositoryInstrumentedTest {
private lateinit var gameDao: GameDao
private lateinit var roundDao: RoundDao
private lateinit var repository: GameRepository
private lateinit var db: AppDatabase
@Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder(
context, AppDatabase::class.java).build()
roundDao = db.roundDao()
gameDao = db.gameDao()
repository = GameRepository(gameDao, roundDao)
}
@After
@Throws(IOException::class)
fun closeDb() {
db.close()
}
@Test
@Throws(Exception::class)
fun gameInitialisation() = runTest {
repository.getActiveGameFlow().take(1).collect {
assertEquals("TeamA", it.game.nameA)
assertEquals("TeamB", it.game.nameB)
assertTrue(it.game.active)
assertEquals(0, it.rounds.count())
}
}
@Test
@Throws(Exception::class)
fun modifyNames() = runTest {
repository.getActiveGameFlow().take(1).collect {
}
repository.updateActiveTeamName(nameA ="aaa")
repository.getActiveGameFlow().take(1).collect {
assertEquals("aaa", it.game.nameA)
assertEquals("TeamB", it.game.nameB)
}
repository.updateActiveTeamName(nameB ="bbb")
repository.getActiveGameFlow().take(1).collect {
assertEquals("aaa", it.game.nameA)
assertEquals("bbb", it.game.nameB)
}
}
@Test
@Throws(Exception::class)
fun newGame() = runTest {
repository.getActiveGameFlow().take(1).collect {
}
repository.newGame()
repository.newGame()
repository.newGame()
repository.newGame()
repository.newGame()
repository.getAllWithRoundFlow().take(1).collect() { it ->
assertEquals(6, it.count())
var uid: Long = 1
it.forEach {game ->
assertEquals(uid++, game.game.uid)
assertEquals(0, game.rounds.count())
}
}
}
@Test
@Throws(Exception::class)
fun setActive() = runTest {
repository.getActiveGameFlow().take(1).collect {
}
repository.newGame()
repository.newGame()
repository.newGame()
repository.newGame()
repository.newGame()
repository.getAllWithRoundFlow().take(1).collect() { it ->
val filtered = it.filter { it.game.active }
assertEquals(1, filtered.count())
assertEquals(6, filtered.first().game.uid)
}
repository.setActive(2)
repository.getAllWithRoundFlow().take(1).collect() { it ->
val filtered = it.filter { it.game.active }
assertEquals(1, filtered.count())
assertEquals(2, filtered.first().game.uid)
}
}
@Test
@Throws(Exception::class)
fun addRoundToActiveGame() = runTest {
repository.getActiveGameFlow().take(1).collect {
}
repository.newGame()
repository.newGame()
repository.newGame()
repository.newGame()
repository.newGame()
repository.addRoundToActiveGame(1,1)
repository.addRoundToActiveGame(2,2)
repository.addRoundToActiveGame(3,3)
repository.addRoundToActiveGame(4,4)
repository.addRoundToActiveGame(5,5)
repository.addRoundToActiveGame(6,6)
repository.getAllWithRoundFlow().take(1).collect() { it ->
val filtered = it.filter { it.rounds.isNotEmpty() }
assertEquals(1, filtered.count())
assertEquals(6, filtered.first().rounds.count())
}
}
@Test
@Throws(Exception::class)
fun lastRound() = runTest {
repository.getActiveGameFlow().take(1).collect {
}
repository.newGame()
repository.newGame()
repository.newGame()
repository.newGame()
repository.newGame()
assertNull(repository.getLastRound())
repository.addRoundToActiveGame(1,1)
repository.addRoundToActiveGame(2,2)
repository.addRoundToActiveGame(3,3)
repository.addRoundToActiveGame(4,4)
repository.addRoundToActiveGame(5,5)
repository.addRoundToActiveGame(6,6)
var lastRound = repository.getLastRound()
assertEquals(6, lastRound?.scoreA)
assertEquals(6, lastRound?.scoreB)
repository.deleteLastRound()
lastRound = repository.getLastRound()
assertEquals(5, lastRound?.scoreA)
assertEquals(5, lastRound?.scoreB)
repository.deleteLastRound()
repository.deleteLastRound()
repository.deleteLastRound()
repository.deleteLastRound()
repository.deleteLastRound()
assertNull(repository.getLastRound())
// No error thrown
repository.deleteLastRound()
}
@Test
@Throws(Exception::class)
fun deleteInactive() = runTest {
repository.getActiveGameFlow().take(1).collect {
}
for (i in 1..6) {
repository.newGame()
repository.addRoundToActiveGame(1,1)
repository.addRoundToActiveGame(2,2)
repository.addRoundToActiveGame(3,3)
repository.addRoundToActiveGame(4,4)
repository.addRoundToActiveGame(5,5)
repository.addRoundToActiveGame(6,6)
}
assertEquals(6 * 6, roundDao.getAll().count())
repository.deleteAllInactive()
// Consists of two transactions. Delete games then delete rounds.
repository.getAllWithRoundFlow().take(1).collect() { it ->
assertEquals(1, it.count())
assertEquals(6, it.first().rounds.count())
}
assertEquals(6, roundDao.getAll().count())
}
@Test
@Throws(Exception::class)
fun deleteById() = runTest {
repository.getActiveGameFlow().take(1).collect {
}
for (i in 1..6) {
repository.newGame()
repository.addRoundToActiveGame(1,1)
repository.addRoundToActiveGame(2,2)
repository.addRoundToActiveGame(3,3)
repository.addRoundToActiveGame(4,4)
repository.addRoundToActiveGame(5,5)
repository.addRoundToActiveGame(6,6)
}
// Non existing Id
repository.deleteGame(10)
repository.getAllWithRoundFlow().take(1).collect() { it ->
assertEquals(7, it.count())
}
// Non existing Id
val toDelete: Long = 3
repository.deleteGame(toDelete)
repository.getAllWithRoundFlow().take(1).collect() { it ->
assertEquals(6, it.count())
assertEquals(0, it.count { it.game.uid == toDelete })
}
}
}

View File

@@ -1,9 +1,6 @@
package me.zobrist.tichucounter.data package me.zobrist.tichucounter.data
import androidx.room.Dao import androidx.room.*
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Update
@Dao @Dao
interface DaoBase<T> { interface DaoBase<T> {

View File

@@ -12,7 +12,7 @@ interface GameDao : DaoBase<Game> {
fun getAll(): Flow<List<Game>> fun getAll(): Flow<List<Game>>
@Transaction @Transaction
@Query("SELECT * FROM game where uid ") @Query("SELECT * FROM game")
fun getGamesWithRounds(): Flow<List<GameWithScores>> fun getGamesWithRounds(): Flow<List<GameWithScores>>
@Transaction @Transaction
@@ -20,10 +20,13 @@ interface GameDao : DaoBase<Game> {
fun getActiveWithRounds(): Flow<GameWithScores?> fun getActiveWithRounds(): Flow<GameWithScores?>
@Query("SELECT * FROM game WHERE uid is :gameId") @Query("SELECT * FROM game WHERE uid is :gameId")
fun getGameById(gameId: Long): Flow<Game> fun getGameById(gameId: Long): Game
@Query("SELECT * FROM game WHERE active is 1") @Query("SELECT * FROM game WHERE active is 1")
fun getActive(): Flow<Game?> fun getActiveAsFlow(): Flow<Game?>
@Query("SELECT * FROM game WHERE active is 1")
fun getActive(): Game?
@Query("UPDATE game SET active = 1 WHERE uid is :gameId;") @Query("UPDATE game SET active = 1 WHERE uid is :gameId;")

View File

@@ -5,13 +5,14 @@ import androidx.room.Entity
import androidx.room.Relation import androidx.room.Relation
import me.zobrist.tichucounter.data.entity.Game import me.zobrist.tichucounter.data.entity.Game
import me.zobrist.tichucounter.data.entity.Round import me.zobrist.tichucounter.data.entity.Round
import java.util.*
@Entity @Entity
data class GameWithScores( data class GameWithScores(
@Embedded val game: Game, @Embedded val game: Game = Game(),
@Relation( @Relation(
parentColumn = "uid", parentColumn = "uid",
entityColumn = "gameId" entityColumn = "gameId"
) )
val rounds: List<Round> val rounds: List<Round> = emptyList()
) )

View File

@@ -6,10 +6,10 @@ import java.util.*
@Entity @Entity
data class Game( data class Game(
var active: Boolean, var active: Boolean = true,
var nameA: String, var nameA: String = "TeamA",
var nameB: String, var nameB: String = "TeamB",
val created: Date, val created: Date = Date(),
var modified: Date, var modified: Date = Date(),
@PrimaryKey(autoGenerate = true) override val uid: Long = 0 @PrimaryKey(autoGenerate = true) override val uid: Long = 0
) : IEntity ) : IEntity

View File

@@ -1,11 +1,7 @@
package me.zobrist.tichucounter.repository package me.zobrist.tichucounter.repository
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.zobrist.tichucounter.data.GameDao import me.zobrist.tichucounter.data.GameDao
import me.zobrist.tichucounter.data.GameWithScores import me.zobrist.tichucounter.data.GameWithScores
import me.zobrist.tichucounter.data.RoundDao import me.zobrist.tichucounter.data.RoundDao
@@ -19,20 +15,15 @@ class GameRepository @Inject constructor(
private val roundDao: RoundDao private val roundDao: RoundDao
) { ) {
private var _activeGame: Game? = null private var activeGame: Game = Game(true, "TeamA", "TeamB", Date(), Date())
val activeGame: Game
get() {
return _activeGame!!
}
init { init {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
gameDao.getActive().collect { gameDao.getActiveAsFlow().collect {
if (it == null) { if (it == null) {
gameDao.insert(Game(true, "TeamA", "TeamB", Date(), Date())) newGame()
} else { } else {
_activeGame = it activeGame = it
} }
} }
} }
@@ -40,16 +31,25 @@ class GameRepository @Inject constructor(
suspend fun newGame() { suspend fun newGame() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val id = val id = gameDao.insert(Game(true, activeGame.nameA, activeGame.nameB, Date(), Date()))
gameDao.insert(Game(true, activeGame.nameA, activeGame.nameB, Date(), Date()))
setActive(id) setActive(id)
} }
} }
suspend fun updateGame(game: Game) { suspend fun updateActiveTeamName(nameA: String? = null, nameB: String? = null) {
game.modified = Date()
val newA = nameA ?: activeGame.nameA
val newB = nameB ?: activeGame.nameB
if(newA == activeGame.nameA && newB == activeGame.nameB) {
return
}
activeGame.modified = Date()
activeGame.nameA = newA
activeGame.nameB = newB
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
gameDao.update(game) gameDao.update(activeGame)
} }
} }
@@ -92,11 +92,10 @@ class GameRepository @Inject constructor(
suspend fun deleteGame(uid: Long) { suspend fun deleteGame(uid: Long) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
gameDao.getGameById(uid).take(1).collect { val game = gameDao.getGameById(uid)
gameDao.delete(it) gameDao.delete(game)
val rounds = roundDao.getAllForGame(it.uid) val rounds = roundDao.getAllForGame(game.uid)
roundDao.delete(rounds) roundDao.delete(rounds)
}
} catch (_: NullPointerException) { } catch (_: NullPointerException) {
} }
} }
@@ -107,9 +106,8 @@ class GameRepository @Inject constructor(
try { try {
gameDao.getAll().take(1).collect { games -> gameDao.getAll().take(1).collect { games ->
val activeId = games.first { it.active }.uid val gamesToDelete = games.filter { it.uid != activeGame.uid }
val gamesToDelete = games.filter { !it.active } val roundsToDelete = roundDao.getAll().filter { it.gameId != activeGame.uid }
val roundsToDelete = roundDao.getAll().filter { it.gameId != activeId }
gameDao.delete(gamesToDelete) gameDao.delete(gamesToDelete)
roundDao.delete(roundsToDelete) roundDao.delete(roundsToDelete)
@@ -119,8 +117,8 @@ class GameRepository @Inject constructor(
} }
} }
fun getActiveGameFlow(): Flow<GameWithScores?> { fun getActiveGameFlow(): Flow<GameWithScores> {
return gameDao.getActiveWithRounds() return gameDao.getActiveWithRounds().filter { it != null }.map { it!! }
} }
fun getAllWithRoundFlow(): Flow<List<GameWithScores>> { fun getAllWithRoundFlow(): Flow<List<GameWithScores>> {

View File

@@ -34,17 +34,15 @@ class MainViewModel @Inject constructor(
gameRepository.getActiveGameFlow().collect { gameRepository.getActiveGameFlow().collect {
activeGameHasRounds = it?.rounds?.isNotEmpty() == true activeGameHasRounds = it.rounds.isNotEmpty() == true
if (it != null) { isUndoActionActive = it.rounds.isNotEmpty()
isUndoActionActive = it.rounds.isNotEmpty()
if (expectedRoundCount != it.rounds.count()) { if (expectedRoundCount != it.rounds.count()) {
redoRounds.clear() redoRounds.clear()
}
expectedRoundCount = it.rounds.count()
} }
expectedRoundCount = it.rounds.count()
} }
} }
} }

View File

@@ -245,17 +245,13 @@ class CounterViewModel @Inject constructor(
override fun updateNameA(value: String) { override fun updateNameA(value: String) {
viewModelScope.launch { viewModelScope.launch {
val game = gameRepository.activeGame gameRepository.updateActiveTeamName(nameA = value)
game.nameA = value
gameRepository.updateGame(game)
} }
} }
override fun updateNameB(value: String) { override fun updateNameB(value: String) {
viewModelScope.launch { viewModelScope.launch {
val game = gameRepository.activeGame gameRepository.updateActiveTeamName(nameB = value)
game.nameB = value
gameRepository.updateGame(game)
} }
} }