diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index 8c839fa8e..3f72d9a1a 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -17,11 +17,18 @@ plugins { alias(libs.plugins.nowinandroid.android.library) alias(libs.plugins.nowinandroid.android.library.jacoco) alias(libs.plugins.nowinandroid.hilt) + alias(libs.plugins.nowinandroid.android.room) id("kotlinx-serialization") } android { namespace = "com.google.samples.apps.nowinandroid.core.data" + + defaultConfig { + testInstrumentationRunner = + "com.google.samples.apps.nowinandroid.core.testing.NiaTestRunner" + } + testOptions { unitTests { isIncludeAndroidResources = true @@ -42,4 +49,7 @@ dependencies { testImplementation(libs.kotlinx.serialization.json) testImplementation(projects.core.datastoreTest) testImplementation(projects.core.testing) + + androidTestImplementation(projects.core.testing) + androidTestImplementation(libs.androidx.test.core) } diff --git a/core/data/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/core/data/SearchContentsRepositoryTest.kt b/core/data/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/core/data/SearchContentsRepositoryTest.kt new file mode 100644 index 000000000..b303484df --- /dev/null +++ b/core/data/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/core/data/SearchContentsRepositoryTest.kt @@ -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() + + 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) + } +} diff --git a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/DefaultSearchContentsRepository.kt b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/DefaultSearchContentsRepository.kt index 3bacb8a14..4cba4809c 100644 --- a/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/DefaultSearchContentsRepository.kt +++ b/core/data/src/main/kotlin/com/google/samples/apps/nowinandroid/core/data/repository/DefaultSearchContentsRepository.kt @@ -46,7 +46,7 @@ internal class DefaultSearchContentsRepository @Inject constructor( override suspend fun populateFtsData() { withContext(ioDispatcher) { - newsResourceFtsDao.insertAll( + newsResourceFtsDao.deleteAllAndInsertAll( newsResourceDao.getNewsResources( useFilterTopicIds = false, useFilterNewsIds = false, @@ -54,7 +54,7 @@ internal class DefaultSearchContentsRepository @Inject constructor( .first() .map(PopulatedNewsResource::asFtsEntity), ) - topicFtsDao.insertAll(topicDao.getOneOffTopicEntities().map { it.asFtsEntity() }) + topicFtsDao.deleteAllAndInsertAll(topicDao.getOneOffTopicEntities().map { it.asFtsEntity() }) } } diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index 8bab355b4..792d776aa 100644 --- a/core/database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { implementation(libs.kotlinx.datetime) + androidTestImplementation(projects.core.testing) androidTestImplementation(libs.androidx.test.core) androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.kotlinx.coroutines.test) diff --git a/core/database/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceFtsDaoTest.kt b/core/database/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceFtsDaoTest.kt new file mode 100644 index 000000000..5f03ba281 --- /dev/null +++ b/core/database/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceFtsDaoTest.kt @@ -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 + + @Before + fun createDb() { + val context = ApplicationProvider.getApplicationContext() + 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) +} diff --git a/core/database/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicFtsDaoTest.kt b/core/database/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicFtsDaoTest.kt new file mode 100644 index 000000000..a640948d5 --- /dev/null +++ b/core/database/src/androidTest/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicFtsDaoTest.kt @@ -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 + + @Before + fun setUp() { + val context = ApplicationProvider.getApplicationContext() + 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) +} diff --git a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/NiaDatabase.kt b/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/NiaDatabase.kt index 87fd82af1..fd6b75e49 100644 --- a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/NiaDatabase.kt +++ b/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/NiaDatabase.kt @@ -63,7 +63,7 @@ import com.google.samples.apps.nowinandroid.core.database.util.InstantConverter @TypeConverters( InstantConverter::class, ) -internal abstract class NiaDatabase : RoomDatabase() { +abstract class NiaDatabase : RoomDatabase() { abstract fun topicDao(): TopicDao abstract fun newsResourceDao(): NewsResourceDao abstract fun topicFtsDao(): TopicFtsDao diff --git a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceFtsDao.kt b/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceFtsDao.kt index 86cc5529e..942069fdb 100644 --- a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceFtsDao.kt +++ b/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/NewsResourceFtsDao.kt @@ -20,6 +20,7 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.Transaction import com.google.samples.apps.nowinandroid.core.database.model.NewsResourceFtsEntity import kotlinx.coroutines.flow.Flow @@ -36,4 +37,17 @@ interface NewsResourceFtsDao { @Query("SELECT count(*) FROM newsResourcesFts") fun getCount(): Flow + + @Query( + """ + DELETE FROM newsResourcesFts + """, + ) + suspend fun deleteAll() + + @Transaction + suspend fun deleteAllAndInsertAll(newsResources: List) { + deleteAll() + insertAll(newsResources = newsResources) + } } diff --git a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicFtsDao.kt b/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicFtsDao.kt index 25dea5b83..03ee9c72f 100644 --- a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicFtsDao.kt +++ b/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/dao/TopicFtsDao.kt @@ -20,6 +20,7 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.Transaction import com.google.samples.apps.nowinandroid.core.database.model.TopicFtsEntity import kotlinx.coroutines.flow.Flow @@ -36,4 +37,17 @@ interface TopicFtsDao { @Query("SELECT count(*) FROM topicsFts") fun getCount(): Flow + + @Query( + """ + DELETE FROM topicsFts + """, + ) + suspend fun deleteAll() + + @Transaction + suspend fun deleteAllAndInsertAll(topics: List) { + deleteAll() + insertAll(topics = topics) + } } diff --git a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/model/NewsResourceFtsEntity.kt b/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/model/NewsResourceFtsEntity.kt index 0ba625024..3f2920ecf 100644 --- a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/model/NewsResourceFtsEntity.kt +++ b/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/model/NewsResourceFtsEntity.kt @@ -36,3 +36,12 @@ data class NewsResourceFtsEntity( @ColumnInfo(name = "content") val content: String, ) + +/** + * Transform [NewsResourceEntity] into [NewsResourceFtsEntity]. + */ +fun NewsResourceEntity.asFtsEntity() = NewsResourceFtsEntity( + newsResourceId = id, + title = "title", + content = "content", +) diff --git a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/model/TopicFtsEntity.kt b/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/model/TopicFtsEntity.kt index 23d56f2df..d25e00c76 100644 --- a/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/model/TopicFtsEntity.kt +++ b/core/database/src/main/kotlin/com/google/samples/apps/nowinandroid/core/database/model/TopicFtsEntity.kt @@ -40,6 +40,9 @@ data class TopicFtsEntity( val longDescription: String, ) +/** + * Transform [TopicEntity] into [TopicFtsEntity]. + */ fun TopicEntity.asFtsEntity() = TopicFtsEntity( topicId = id, name = name, diff --git a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/database/NewsResourceEntitiesTestData.kt b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/database/NewsResourceEntitiesTestData.kt new file mode 100644 index 000000000..9c7b49833 --- /dev/null +++ b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/database/NewsResourceEntitiesTestData.kt @@ -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 = 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. " + + "You’ll learn the fundamentals of programming in Kotlin while building Android apps using Jetpack Compose, " + + "Android’s 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! Here’s 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 = "", + ), +) diff --git a/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/database/TopicEntitiesTestData.kt b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/database/TopicEntitiesTestData.kt new file mode 100644 index 000000000..c4f450d7b --- /dev/null +++ b/core/testing/src/main/kotlin/com/google/samples/apps/nowinandroid/core/testing/database/TopicEntitiesTestData.kt @@ -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 = 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 = "", + ), +)