pull/1268/merge
Jaehwa Noh 1 month ago committed by GitHub
commit a693a53406
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -17,11 +17,18 @@ plugins {
alias(libs.plugins.nowinandroid.android.library) alias(libs.plugins.nowinandroid.android.library)
alias(libs.plugins.nowinandroid.android.library.jacoco) alias(libs.plugins.nowinandroid.android.library.jacoco)
alias(libs.plugins.nowinandroid.hilt) alias(libs.plugins.nowinandroid.hilt)
alias(libs.plugins.nowinandroid.android.room)
id("kotlinx-serialization") id("kotlinx-serialization")
} }
android { android {
namespace = "com.google.samples.apps.nowinandroid.core.data" namespace = "com.google.samples.apps.nowinandroid.core.data"
defaultConfig {
testInstrumentationRunner =
"com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner"
}
testOptions { testOptions {
unitTests { unitTests {
isIncludeAndroidResources = true isIncludeAndroidResources = true
@ -42,4 +49,7 @@ dependencies {
testImplementation(libs.kotlinx.serialization.json) testImplementation(libs.kotlinx.serialization.json)
testImplementation(projects.core.datastoreTest) testImplementation(projects.core.datastoreTest)
testImplementation(projects.core.testing) testImplementation(projects.core.testing)
androidTestImplementation(projects.core.testing)
androidTestImplementation(libs.androidx.test.core)
} }

@ -0,0 +1,117 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.data
import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import com.google.samples.apps.nowinandroid.core.data.repository.DefaultSearchContentsRepository
import com.google.samples.apps.nowinandroid.core.data.repository.SearchContentsRepository
import com.google.samples.apps.nowinandroid.core.database.NiaDatabase
import com.google.samples.apps.nowinandroid.core.database.dao.NewsResourceDao
import com.google.samples.apps.nowinandroid.core.database.dao.NewsResourceFtsDao
import com.google.samples.apps.nowinandroid.core.database.dao.TopicDao
import com.google.samples.apps.nowinandroid.core.database.dao.TopicFtsDao
import com.google.samples.apps.nowinandroid.core.testing.database.newsResourceEntitiesTestData
import com.google.samples.apps.nowinandroid.core.testing.database.topicEntitiesTestData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals
/**
* Instrumentation tests for [SearchContentsRepository].
*/
class SearchContentsRepositoryTest {
private lateinit var newsResourceDao: NewsResourceDao
private lateinit var topicDao: TopicDao
private lateinit var newsResourceFtsDao: NewsResourceFtsDao
private lateinit var topicFtsDao: TopicFtsDao
private lateinit var db: NiaDatabase
private lateinit var searchContentsRepository: SearchContentsRepository
@Before
fun setUp() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder(
context,
NiaDatabase::class.java,
).build()
newsResourceDao = db.newsResourceDao()
topicDao = db.topicDao()
newsResourceFtsDao = db.newsResourceFtsDao()
topicFtsDao = db.topicFtsDao()
searchContentsRepository = DefaultSearchContentsRepository(
newsResourceDao = newsResourceDao,
newsResourceFtsDao = newsResourceFtsDao,
topicDao = topicDao,
topicFtsDao = topicFtsDao,
ioDispatcher = Dispatchers.IO,
)
}
@After
fun closeDb() = db.close()
@Test
fun ftsEntities_insertAllTwice_countMatches() = runTest {
allDataPreSetting()
repeat(2) {
searchContentsRepository.populateFtsData()
}
advanceUntilIdle()
assertEquals(7, searchContentsRepository.getSearchContentsCount().first())
}
@Test
fun searchQuery_searchAndroid_findResult() = runTest {
allDataPreSetting()
searchContentsRepository.populateFtsData()
val searchQuery = "Android"
val topicIds = listOf("2")
val newsResourceIds = listOf("1", "2")
assertEquals(
topicIds,
searchContentsRepository
.searchContents(searchQuery = searchQuery)
.first()
.topics
.map { it.id },
)
assertEquals(
newsResourceIds,
searchContentsRepository
.searchContents(searchQuery = searchQuery)
.first()
.newsResources
.map { it.id },
)
}
private suspend fun allDataPreSetting() {
newsResourceDao.upsertNewsResources(newsResourceEntities = newsResourceEntitiesTestData)
topicDao.upsertTopics(entities = topicEntitiesTestData)
}
}

@ -46,7 +46,7 @@ internal class DefaultSearchContentsRepository @Inject constructor(
override suspend fun populateFtsData() { override suspend fun populateFtsData() {
withContext(ioDispatcher) { withContext(ioDispatcher) {
newsResourceFtsDao.insertAll( newsResourceFtsDao.deleteAllAndInsertAll(
newsResourceDao.getNewsResources( newsResourceDao.getNewsResources(
useFilterTopicIds = false, useFilterTopicIds = false,
useFilterNewsIds = false, useFilterNewsIds = false,
@ -54,7 +54,7 @@ internal class DefaultSearchContentsRepository @Inject constructor(
.first() .first()
.map(PopulatedNewsResource::asFtsEntity), .map(PopulatedNewsResource::asFtsEntity),
) )
topicFtsDao.insertAll(topicDao.getOneOffTopicEntities().map { it.asFtsEntity() }) topicFtsDao.deleteAllAndInsertAll(topicDao.getOneOffTopicEntities().map { it.asFtsEntity() })
} }
} }

@ -30,6 +30,7 @@ dependencies {
implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.datetime)
androidTestImplementation(projects.core.testing)
androidTestImplementation(libs.androidx.test.core) androidTestImplementation(libs.androidx.test.core)
androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.kotlinx.coroutines.test) androidTestImplementation(libs.kotlinx.coroutines.test)

@ -0,0 +1,84 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.database.dao
import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import com.google.samples.apps.nowinandroid.core.database.NiaDatabase
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceFtsEntity
import com.google.samples.apps.nowinandroid.core.database.model.asFtsEntity
import com.google.samples.apps.nowinandroid.core.testing.database.newsResourceEntitiesTestData
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals
/**
* Instrumentation tests for [NewsResourceFtsDao].
*/
class NewsResourceFtsDaoTest {
private lateinit var newsResourceFtsDao: NewsResourceFtsDao
private lateinit var db: NiaDatabase
private lateinit var newsResourceFtsEntities: List<NewsResourceFtsEntity>
@Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder(
context,
NiaDatabase::class.java,
).build()
newsResourceFtsDao = db.newsResourceFtsDao()
newsResourceFtsEntities = newsResourceEntitiesTestData.map {
it.asFtsEntity()
}
}
@After
fun closeDb() = db.close()
@Test
fun newsResourceFts_insertAllOnce_countMatches() = runTest {
insertAllNewsResourceFtsEntities()
assertEquals(newsResourceFtsEntities.size, newsResourceFtsDao.getCount().first())
}
@Test
fun newsResourceFts_insertAllTwice_countMatches() = runTest {
repeat(2) {
newsResourceFtsDao.insertAll(newsResources = newsResourceFtsEntities)
}
assertEquals(newsResourceFtsEntities.size * 2, newsResourceFtsDao.getCount().first())
}
@Test
fun newsResourceFts_insertAllThreeTimes_countMatches() = runTest {
repeat(3) {
newsResourceFtsDao.deleteAllAndInsertAll(newsResources = newsResourceFtsEntities)
}
assertEquals(newsResourceFtsEntities.size, newsResourceFtsDao.getCount().first())
}
private suspend fun insertAllNewsResourceFtsEntities() =
newsResourceFtsDao.insertAll(newsResources = newsResourceFtsEntities)
}

@ -0,0 +1,84 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.database.dao
import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import com.google.samples.apps.nowinandroid.core.database.NiaDatabase
import com.google.samples.apps.nowinandroid.core.database.model.TopicFtsEntity
import com.google.samples.apps.nowinandroid.core.database.model.asFtsEntity
import com.google.samples.apps.nowinandroid.core.testing.database.topicEntitiesTestData
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import kotlin.test.assertEquals
/**
* Instrumentation tests for [TopicFtsDao].
*/
class TopicFtsDaoTest {
private lateinit var topicFtsDao: TopicFtsDao
private lateinit var db: NiaDatabase
private lateinit var topicFtsEntities: List<TopicFtsEntity>
@Before
fun setUp() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder(
context,
NiaDatabase::class.java,
).build()
topicFtsDao = db.topicFtsDao()
topicFtsEntities = topicEntitiesTestData.map {
it.asFtsEntity()
}
}
@After
fun closeDb() = db.close()
@Test
fun topicFts_insertAllOnce_countMatches() = runTest {
insertAllNewsResourceFtsEntities()
assertEquals(topicFtsEntities.size, topicFtsDao.getCount().first())
}
@Test
fun topicFts_insertAllTwice_countMatches() = runTest {
repeat(2) {
topicFtsDao.insertAll(topics = topicFtsEntities)
}
assertEquals(topicFtsEntities.size * 2, topicFtsDao.getCount().first())
}
@Test
fun topicFts_insertAllThreeTimes_countMatches() = runTest {
repeat(3) {
topicFtsDao.deleteAllAndInsertAll(topics = topicFtsEntities)
}
assertEquals(topicFtsEntities.size, topicFtsDao.getCount().first())
}
private suspend fun insertAllNewsResourceFtsEntities() =
topicFtsDao.insertAll(topics = topicFtsEntities)
}

@ -63,7 +63,7 @@ import com.google.samples.apps.nowinandroid.core.database.util.InstantConverter
@TypeConverters( @TypeConverters(
InstantConverter::class, InstantConverter::class,
) )
internal abstract class NiaDatabase : RoomDatabase() { abstract class NiaDatabase : RoomDatabase() {
abstract fun topicDao(): TopicDao abstract fun topicDao(): TopicDao
abstract fun newsResourceDao(): NewsResourceDao abstract fun newsResourceDao(): NewsResourceDao
abstract fun topicFtsDao(): TopicFtsDao abstract fun topicFtsDao(): TopicFtsDao

@ -20,6 +20,7 @@ import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceFtsEntity import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceFtsEntity
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -36,4 +37,17 @@ interface NewsResourceFtsDao {
@Query("SELECT count(*) FROM newsResourcesFts") @Query("SELECT count(*) FROM newsResourcesFts")
fun getCount(): Flow<Int> fun getCount(): Flow<Int>
@Query(
"""
DELETE FROM newsResourcesFts
""",
)
suspend fun deleteAll()
@Transaction
suspend fun deleteAllAndInsertAll(newsResources: List<NewsResourceFtsEntity>) {
deleteAll()
insertAll(newsResources = newsResources)
}
} }

@ -20,6 +20,7 @@ import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction
import com.google.samples.apps.nowinandroid.core.database.model.TopicFtsEntity import com.google.samples.apps.nowinandroid.core.database.model.TopicFtsEntity
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -36,4 +37,17 @@ interface TopicFtsDao {
@Query("SELECT count(*) FROM topicsFts") @Query("SELECT count(*) FROM topicsFts")
fun getCount(): Flow<Int> fun getCount(): Flow<Int>
@Query(
"""
DELETE FROM topicsFts
""",
)
suspend fun deleteAll()
@Transaction
suspend fun deleteAllAndInsertAll(topics: List<TopicFtsEntity>) {
deleteAll()
insertAll(topics = topics)
}
} }

@ -36,3 +36,12 @@ data class NewsResourceFtsEntity(
@ColumnInfo(name = "content") @ColumnInfo(name = "content")
val content: String, val content: String,
) )
/**
* Transform [NewsResourceEntity] into [NewsResourceFtsEntity].
*/
fun NewsResourceEntity.asFtsEntity() = NewsResourceFtsEntity(
newsResourceId = id,
title = "title",
content = "content",
)

@ -40,6 +40,9 @@ data class TopicFtsEntity(
val longDescription: String, val longDescription: String,
) )
/**
* Transform [TopicEntity] into [TopicFtsEntity].
*/
fun TopicEntity.asFtsEntity() = TopicFtsEntity( fun TopicEntity.asFtsEntity() = TopicFtsEntity(
topicId = id, topicId = id,
name = name, name = name,

@ -0,0 +1,75 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.testing.database
import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceEntity
import kotlinx.datetime.Instant
/**
* Test data list of [NewsResourceEntity].
*/
val newsResourceEntitiesTestData: List<NewsResourceEntity> = listOf(
NewsResourceEntity(
id = "1",
title = "Android Basics with Compose",
content = "We released the first two units of Android Basics with Compose, " +
"our first free course that teaches Android Development with Jetpack Compose to anyone; " +
"you do not need any prior programming experience other than basic computer literacy to get started. " +
"Youll learn the fundamentals of programming in Kotlin while building Android apps using Jetpack Compose, " +
"Androids modern toolkit that simplifies and accelerates native UI development. " +
"These two units are just the beginning; more will be coming soon. " +
"Check out Android Basics with Compose to get started on your Android development journey",
url = "https://android-developers.googleblog.com/2022/05/new-android-basics-with-compose-course.html",
headerImageUrl = "https://developer.android.com/images/hero-assets/android-basics-compose.svg",
publishDate = Instant.parse("2021-11-09T00:00:00.000Z"),
type = "Codelab",
),
NewsResourceEntity(
id = "2",
title = "Thanks for helping us reach 1M YouTube Subscribers",
content = "Thank you everyone for following the Now in Android series and everything the " +
"Android Developers YouTube channel has to offer. During the Android Developer " +
"Summit, our YouTube channel reached 1 million subscribers! Heres a small video to " +
"thank you all.",
url = "https://youtu.be/-fJ6poHQrjM",
headerImageUrl = "https://i.ytimg.com/vi/-fJ6poHQrjM/maxresdefault.jpg",
publishDate = Instant.parse("2021-11-09T00:00:00.000Z"),
type = "Video 📺",
),
NewsResourceEntity(
id = "3",
title = "Transformations and customisations in the Paging Library",
content = "A demonstration of different operations that can be performed " +
"with Paging. Transformations like inserting separators, when to " +
"create a new pager, and customisation options for consuming " +
"PagingData.",
url = "https://youtu.be/ZARz0pjm5YM",
headerImageUrl = "https://i.ytimg.com/vi/ZARz0pjm5YM/maxresdefault.jpg",
publishDate = Instant.parse("2021-11-01T00:00:00.000Z"),
type = "Video 📺",
),
NewsResourceEntity(
id = "4",
title = "New Jetpack Release",
content = "New Jetpack release includes updates to libraries such as CameraX, Benchmark, and" +
"more!",
url = "https://developer.android.com/jetpack/androidx/versions/all-channel",
headerImageUrl = "",
publishDate = Instant.parse("2022-10-01T00:00:00.000Z"),
type = "",
),
)

@ -0,0 +1,49 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid.core.testing.database
import com.google.samples.apps.nowinandroid.core.database.model.TopicEntity
/**
* Test data list of [TopicEntity].
*/
val topicEntitiesTestData: List<TopicEntity> = listOf(
TopicEntity(
id = "2",
name = "Headlines",
shortDescription = "News we want everyone to see",
longDescription = "Stay up to date with the latest events and announcements from Android!",
imageUrl = "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_Headlines.svg?alt=media&token=506faab0-617a-4668-9e63-4a2fb996603f",
url = "",
),
TopicEntity(
id = "3",
name = "UI",
shortDescription = "Material Design, Navigation, Text, Paging, Accessibility (a11y), Internationalization (i18n), Localization (l10n), Animations, Large Screens, Widgets",
longDescription = "Learn how to optimize your app's user interface - everything that users can see and interact with. Stay up to date on topics such as Material Design, Navigation, Text, Paging, Compose, Accessibility (a11y), Internationalization (i18n), Localization (l10n), Animations, Large Screens, Widgets, and many more!",
imageUrl = "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_UI.svg?alt=media&token=0ee1842b-12e8-435f-87ba-a5bb02c47594",
url = "",
),
TopicEntity(
id = "4",
name = "Testing",
shortDescription = "CI, Espresso, TestLab, etc",
longDescription = "Testing is an integral part of the app development process. By running tests against your app consistently, you can verify your app's correctness, functional behavior, and usability before you release it publicly. Stay up to date on the latest tricks in CI, Espresso, and Firebase TestLab.",
imageUrl = "https://firebasestorage.googleapis.com/v0/b/now-in-android.appspot.com/o/img%2Fic_topic_Testing.svg?alt=media&token=a11533c4-7cc8-4b11-91a3-806158ebf428",
url = "",
),
)
Loading…
Cancel
Save