├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ └── themes.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── values-night │ │ │ │ └── themes.xml │ │ │ ├── layout │ │ │ │ └── activity_main.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ └── drawable │ │ │ │ └── ic_launcher_background.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── atdev │ │ │ │ └── unittestingpractice │ │ │ │ ├── App.kt │ │ │ │ ├── other │ │ │ │ ├── Constants.kt │ │ │ │ ├── registrationform │ │ │ │ │ ├── ResourceComparison.kt │ │ │ │ │ └── RegistrationUtil.kt │ │ │ │ ├── Event.kt │ │ │ │ └── Resource.kt │ │ │ │ ├── data │ │ │ │ ├── other │ │ │ │ │ └── Constants.kt │ │ │ │ ├── DataManager.kt │ │ │ │ ├── local │ │ │ │ │ ├── ShoppingDatabase.kt │ │ │ │ │ ├── ShoppingItem.kt │ │ │ │ │ └── ShoppingDao.kt │ │ │ │ └── remote │ │ │ │ │ ├── network │ │ │ │ │ └── PixabayApi.kt │ │ │ │ │ └── repos │ │ │ │ │ ├── ISoppingRepository.kt │ │ │ │ │ └── DefaultShoppingRepoImp.kt │ │ │ │ ├── core │ │ │ │ └── entities │ │ │ │ │ ├── ImageResponse.kt │ │ │ │ │ └── ImageResult.kt │ │ │ │ ├── ui │ │ │ │ ├── MainActivity.kt │ │ │ │ └── ShoppingViewModel.kt │ │ │ │ └── di │ │ │ │ └── AppModule.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── atdev │ │ │ └── unittestingpractice │ │ │ ├── ui │ │ │ └── ShoppingViewModelTest.kt │ │ │ ├── ExampleUnitTest.kt │ │ │ ├── RegistrationUtilTest.kt │ │ │ └── remote │ │ │ └── repos │ │ │ └── FakeShoppingRepo.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── atdev │ │ └── unittestingpractice │ │ ├── ExampleInstrumentedTest.kt │ │ ├── ResourceComparisonTest.kt │ │ ├── util │ │ └── LiveDataUtilAndroidTest.kt │ │ └── data │ │ └── local │ │ └── ShoppingDaoTest.kt ├── proguard-rules.pro └── build.gradle ├── .idea ├── .gitignore ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── vcs.xml ├── compiler.xml ├── runConfigurations.xml ├── misc.xml ├── gradle.xml └── jarRepositories.xml ├── settings.gradle ├── .gitattributes ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── README.md ├── gradlew.bat └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | rootProject.name = "UnitTestingPractice" -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | UnitTestingPractice 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedTawfiqM/UnitTestingPractice/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedTawfiqM/UnitTestingPractice/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedTawfiqM/UnitTestingPractice/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedTawfiqM/UnitTestingPractice/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedTawfiqM/UnitTestingPractice/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedTawfiqM/UnitTestingPractice/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedTawfiqM/UnitTestingPractice/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedTawfiqM/UnitTestingPractice/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedTawfiqM/UnitTestingPractice/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedTawfiqM/UnitTestingPractice/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AhmedTawfiqM/UnitTestingPractice/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/test/java/com/atdev/unittestingpractice/ui/ShoppingViewModelTest.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice.ui 2 | 3 | import org.junit.Assert.* 4 | 5 | class ShoppingViewModelTest -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/atdev/unittestingpractice/App.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class App : Application() { 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/atdev/unittestingpractice/other/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice.other 2 | 3 | object Constants { 4 | 5 | const val DATABASE_NAME = "shopping_db" 6 | const val BASE_URL = "https://pixabay.com/" 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/atdev/unittestingpractice/data/other/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice.data.other 2 | 3 | object Constants { 4 | 5 | const val DATABASE_NAME = "shopping_db" 6 | const val BASE_URL = "https://pixabay.com/" 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/atdev/unittestingpractice/core/entities/ImageResponse.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice.core.entities 2 | 3 | data class ImageResponse( 4 | val total: Int = 0, 5 | val totalHits: Int = 0, 6 | val hits: List, 7 | ) -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun May 02 00:05:29 EET 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | gradle.properties 16 | local.properties 17 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/atdev/unittestingpractice/other/registrationform/ResourceComparison.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice.other.registrationform 2 | 3 | import android.content.Context 4 | 5 | class ResourceComparison { 6 | 7 | fun isEqual(context: Context, resID: Int, res: String) = 8 | context.getString(resID) == res 9 | } -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/java/com/atdev/unittestingpractice/data/DataManager.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice.data 2 | 3 | import com.atdev.unittestingpractice.data.local.ShoppingDao 4 | import com.atdev.unittestingpractice.data.remote.network.PixabayApi 5 | 6 | data class DataManager( 7 | val shoppingDao: ShoppingDao, 8 | val pixabayApi: PixabayApi 9 | ) -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/atdev/unittestingpractice/data/local/ShoppingDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice.data.local 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | 6 | @Database( 7 | entities = [ShoppingItem::class], 8 | version = 1 9 | ) 10 | abstract class ShoppingDatabase : RoomDatabase() { 11 | 12 | abstract fun shoppingDao(): ShoppingDao 13 | } -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/atdev/unittestingpractice/data/local/ShoppingItem.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice.data.local 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | @Entity(tableName = "shopping_item") 7 | data class ShoppingItem( 8 | val name: String, 9 | val amount: Double, 10 | val price: Double, 11 | val imageUrl: String, 12 | @PrimaryKey(autoGenerate = true) 13 | val id: Int? = null 14 | ) -------------------------------------------------------------------------------- /app/src/test/java/com/atdev/unittestingpractice/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | 14 | @Test 15 | fun addition_isCorrect() { 16 | assertEquals(4, 2 + 2) 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/atdev/unittestingpractice/data/remote/network/PixabayApi.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice.data.remote.network 2 | 3 | import com.atdev.unittestingpractice.BuildConfig 4 | import com.atdev.unittestingpractice.core.entities.ImageResponse 5 | import retrofit2.Response 6 | import retrofit2.http.GET 7 | import retrofit2.http.Query 8 | 9 | interface PixabayApi { 10 | 11 | @GET("/api/") 12 | suspend fun searchImages( 13 | @Query("q") searchKey: String, 14 | @Query("key") apiKey: String = BuildConfig.APi_KRY_Pixabay 15 | ): Response 16 | } -------------------------------------------------------------------------------- /app/src/main/java/com/atdev/unittestingpractice/data/local/ShoppingDao.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice.data.local 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.room.* 5 | 6 | @Dao 7 | interface ShoppingDao { 8 | 9 | @Insert(onConflict = OnConflictStrategy.REPLACE) 10 | suspend fun insertShoppingItem(shoppingItem: ShoppingItem) 11 | 12 | @Delete 13 | suspend fun deleteShoppingItem(shoppingItem: ShoppingItem) 14 | 15 | @Query("SELECT * FROM shopping_item") 16 | fun observeAllShoppingItems(): LiveData> 17 | 18 | @Query("SELECT SUM(price * amount) FROM shopping_item") 19 | fun observeTotalPrice(): LiveData 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/atdev/unittestingpractice/other/Event.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice.other 2 | 3 | open class Event(private val content: T) { 4 | 5 | var hasBeenHandled = false 6 | private set // Allow external read but not write 7 | 8 | /** 9 | * Returns the content and prevents its use again. 10 | */ 11 | fun getContentIfNotHandled(): T? { 12 | return if (hasBeenHandled) { 13 | null 14 | } else { 15 | hasBeenHandled = true 16 | content 17 | } 18 | } 19 | 20 | /** 21 | * Returns the content, even if it's already been handled. 22 | */ 23 | fun peekContent(): T = content 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/atdev/unittestingpractice/other/Resource.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice.other 2 | 3 | data class Resource(val status: Status, val data: T?, val message: String?) { 4 | companion object { 5 | fun success(data: T?): Resource { 6 | return Resource(Status.SUCCESS, data, null) 7 | } 8 | 9 | fun error(msg: String, data: T?): Resource { 10 | return Resource(Status.ERROR, data, msg) 11 | } 12 | 13 | fun loading(data: T?): Resource { 14 | return Resource(Status.LOADING, data, null) 15 | } 16 | } 17 | } 18 | 19 | enum class Status { 20 | SUCCESS, 21 | ERROR, 22 | LOADING 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/atdev/unittestingpractice/data/remote/repos/ISoppingRepository.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice.data.remote.repos 2 | 3 | import androidx.lifecycle.LiveData 4 | import com.atdev.unittestingpractice.core.entities.ImageResponse 5 | import com.atdev.unittestingpractice.data.local.ShoppingItem 6 | import com.atdev.unittestingpractice.other.Resource 7 | 8 | interface ISoppingRepository { 9 | suspend fun insertShoppingItem(shoppingItem: ShoppingItem) 10 | suspend fun deleteShoppingItem(shoppingItem: ShoppingItem) 11 | fun observeAllShoppingItems(): LiveData> 12 | fun observeTotalPrice(): LiveData 13 | suspend fun searchForImage(imageQuery: String): Resource 14 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/com/atdev/unittestingpractice/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.atdev.unittestingpractice", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/atdev/unittestingpractice/other/registrationform/RegistrationUtil.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice.other.registrationform 2 | 3 | object RegistrationUtil { 4 | 5 | private val existedUserNames = listOf("Ahmed", "Mona", "Said", "Faten") 6 | 7 | fun validateRegistrationInput( 8 | userName: String?, 9 | password: String?, 10 | confirmedPassword: String?, 11 | ): Boolean { 12 | 13 | if (userName.isNullOrEmpty()) return false 14 | if (password.isNullOrEmpty()) return false 15 | if (confirmedPassword.isNullOrEmpty()) return false 16 | if (existedUserNames.contains(userName)) return false 17 | if (userName in existedUserNames) return false 18 | if (password != confirmedPassword) return false 19 | if (password.length <= 2) return false 20 | //if () 21 | 22 | return true 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/atdev/unittestingpractice/core/entities/ImageResult.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice.core.entities 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class ImageResult( 6 | val id: Int = 0, 7 | val webformatHeight: Int = 0, 8 | val imageWidth: Int = 0, 9 | val favorites: Int = 0, 10 | val previewHeight: Int = 0, 11 | val webformatURL: String = "", 12 | val userImageURL: String = "", 13 | val previewURL: String = "", 14 | val comments: Int = 0, 15 | val imageHeight: Int = 0, 16 | val tags: String = "", 17 | val previewWidth: Int = 0, 18 | val fullHDURL: String = "", 19 | val downloads: Int = 0, 20 | @SerializedName("user_id") 21 | val userId: Int = 0, 22 | val type: String = "", 23 | val largeImageURL: String = "", 24 | val imageURL: String = "", 25 | val pageURL: String = "", 26 | val imageSize: Int = 0, 27 | val webformatWidth: Int = 0, 28 | val user: String = "", 29 | val views: Int = 0, 30 | val likes: Int = 0 31 | ) -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UnitTestingPractice 2 | ### 3 types of Tests in Android 3 | - Unit 4 | - instrumentation (Integration) 5 | - UI 6 | 7 | ### Unit Testing benefits 8 | - confirm code work like a charm 9 | - simulate App End User 10 | - you don't need run real device or emulator to test cases 11 | 12 | ### 3 Types of Tests 13 | ![Tests Types](https://conference.eurostarsoftwaretesting.com/wp-content/uploads/Grood-testing-25-768x434.png) 14 | 15 | 16 | ### TDD, Test Driven Development 17 | ![](https://miro.medium.com/max/480/1*ieVWcSsJmeBbZFo6a_dL5g.png) 18 | 19 | 20 | ### Principles of Test Driven Development 21 | ![](https://www.xenonstack.com/images/blog/test-driven-development-approach.png) 22 | 23 | 24 | ### Tutorial 25 | [TDD Tutorial](https://www.xenonstack.com/blog/test-behaviour-driven-development/) 26 | 27 | [TDD](https://chromatichq.com/blog/principles-testdriven-development) 28 | 29 | [Example](https://github.com/AhmedTawfiqM/UnitTestingPractice/blob/main/app/src/test/java/com/atdev/unittestingpractice/RegistrationUtilTest.kt) 30 | 31 | [Testing in android](https://www.youtube.com/watch?v=EkfVL5vCDmo&list=PLQkwcJG4YTCSYJ13G4kVIJ10X5zisB2Lq&index=1&ab_channel=PhilippLackner) 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/atdev/unittestingpractice/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice.ui 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.appcompat.app.AppCompatActivity 5 | import android.os.Bundle 6 | import android.widget.ImageView 7 | import android.widget.TextView 8 | import androidx.lifecycle.Observer 9 | import androidx.lifecycle.ViewModel 10 | import androidx.lifecycle.ViewModelProvider 11 | import com.atdev.unittestingpractice.R 12 | import com.atdev.unittestingpractice.core.entities.ImageResponse 13 | 14 | class MainActivity : AppCompatActivity() { 15 | 16 | private lateinit var viewModel: ShoppingViewModel 17 | private lateinit var tvImages: TextView 18 | 19 | @SuppressLint("SetTextI18n") 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | setContentView(R.layout.activity_main) 23 | 24 | viewModel = ViewModelProvider(this).get(ShoppingViewModel::class.java) 25 | tvImages = findViewById(R.id.tvImages) 26 | 27 | viewModel.images.observe(this, { 28 | val response = it.peekContent().data 29 | response?.hits?.size?.let { size -> 30 | tvImages.text = "got $size images" 31 | } 32 | 33 | }) 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/atdev/unittestingpractice/ResourceComparisonTest.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice 2 | 3 | import android.content.Context 4 | import androidx.test.core.app.ApplicationProvider 5 | import com.atdev.unittestingpractice.other.registrationform.ResourceComparison 6 | import com.google.common.truth.Truth.assertThat 7 | import org.junit.After 8 | import org.junit.Before 9 | import org.junit.Test 10 | 11 | class ResourceComparisonTest { 12 | 13 | private lateinit var resourceComparison: ResourceComparison 14 | 15 | @Before 16 | fun setup() { 17 | resourceComparison = ResourceComparison() 18 | } 19 | 20 | @After 21 | fun tearDown() { 22 | //resourceComparison = null 23 | } 24 | 25 | @Test 26 | fun sameStringResourcesEqual_returnTrue() { 27 | val context = ApplicationProvider.getApplicationContext() 28 | val result = resourceComparison.isEqual( 29 | context, 30 | R.string.app_name, 31 | "UnitTestingPractice" 32 | ) 33 | 34 | assertThat(result).isTrue() 35 | } 36 | 37 | @Test 38 | fun differentStringResourcesEqual_returnFalse() { 39 | val context = ApplicationProvider.getApplicationContext() 40 | val result = resourceComparison.isEqual( 41 | context, 42 | R.string.app_name, 43 | "UnitTestingPracticeAS" 44 | ) 45 | 46 | assertThat(result).isFalse() 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/atdev/unittestingpractice/ui/ShoppingViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice.ui 2 | 3 | import androidx.hilt.lifecycle.ViewModelInject 4 | import androidx.lifecycle.* 5 | import com.atdev.unittestingpractice.core.entities.ImageResponse 6 | import com.atdev.unittestingpractice.data.local.ShoppingItem 7 | import com.atdev.unittestingpractice.data.remote.repos.ISoppingRepository 8 | import com.atdev.unittestingpractice.other.Event 9 | import com.atdev.unittestingpractice.other.Resource 10 | import kotlinx.coroutines.launch 11 | 12 | class ShoppingViewModel @ViewModelInject constructor( 13 | private val repository: ISoppingRepository 14 | ) : ViewModel() { 15 | 16 | val shoppingItems = repository.observeAllShoppingItems() 17 | 18 | val totalPrice = repository.observeTotalPrice() 19 | 20 | private val _images = MutableLiveData>>() 21 | val images: LiveData>> = _images 22 | 23 | private val _curImageUrl = MutableLiveData() 24 | val curImageUrl: LiveData = _curImageUrl 25 | 26 | private val _insertShoppingItemStatus = MutableLiveData>>() 27 | val insertShoppingItemStatus: LiveData>> = _insertShoppingItemStatus 28 | 29 | fun setCurImageUrl(url: String) { 30 | _curImageUrl.postValue(url) 31 | } 32 | 33 | fun deleteShoppingItem(shoppingItem: ShoppingItem) = viewModelScope.launch { 34 | repository.deleteShoppingItem(shoppingItem) 35 | } 36 | 37 | fun insertShoppingItemIntoDb(shoppingItem: ShoppingItem) = viewModelScope.launch { 38 | repository.insertShoppingItem(shoppingItem) 39 | } 40 | 41 | fun insertShoppingItem(name: String, amountString: String, priceString: String) { 42 | 43 | } 44 | 45 | fun searchForImage(imageQuery: String) { 46 | 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/atdev/unittestingpractice/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice.di 2 | 3 | import android.content.Context 4 | import androidx.room.Room 5 | import com.atdev.unittestingpractice.data.DataManager 6 | import com.atdev.unittestingpractice.data.local.ShoppingDatabase 7 | import com.atdev.unittestingpractice.other.Constants.BASE_URL 8 | import com.atdev.unittestingpractice.other.Constants.DATABASE_NAME 9 | import com.atdev.unittestingpractice.data.remote.network.PixabayApi 10 | import com.atdev.unittestingpractice.data.remote.repos.DefaultShoppingRepoImp 11 | import com.atdev.unittestingpractice.data.remote.repos.ISoppingRepository 12 | import dagger.Module 13 | import dagger.Provides 14 | import dagger.hilt.InstallIn 15 | import dagger.hilt.android.components.ApplicationComponent 16 | import dagger.hilt.android.qualifiers.ApplicationContext 17 | import retrofit2.Retrofit 18 | import retrofit2.converter.gson.GsonConverterFactory 19 | import javax.inject.Singleton 20 | 21 | @Module 22 | @InstallIn(ApplicationComponent::class) 23 | object AppModule { 24 | 25 | @Singleton 26 | @Provides 27 | fun provideAppDatabase(@ApplicationContext context: Context) = 28 | Room.databaseBuilder(context, ShoppingDatabase::class.java, DATABASE_NAME) 29 | .build() 30 | 31 | @Singleton 32 | @Provides 33 | fun provideShoppingRepository(dataManager: DataManager) = 34 | DefaultShoppingRepoImp(dataManager) as ISoppingRepository 35 | 36 | @Singleton 37 | @Provides 38 | fun provideShoppingDao(database: ShoppingDatabase) = 39 | database.shoppingDao() 40 | 41 | @Singleton 42 | @Provides 43 | fun providePixabayApi() = 44 | Retrofit.Builder() 45 | .addConverterFactory(GsonConverterFactory.create()) 46 | .baseUrl(BASE_URL) 47 | .build() 48 | .create(PixabayApi::class.java)!! 49 | 50 | } -------------------------------------------------------------------------------- /app/src/main/java/com/atdev/unittestingpractice/data/remote/repos/DefaultShoppingRepoImp.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice.data.remote.repos 2 | 3 | import androidx.lifecycle.LiveData 4 | import com.atdev.unittestingpractice.core.entities.ImageResponse 5 | import com.atdev.unittestingpractice.data.DataManager 6 | import com.atdev.unittestingpractice.data.local.ShoppingItem 7 | import com.atdev.unittestingpractice.other.Resource 8 | import retrofit2.Response 9 | import java.lang.Exception 10 | import javax.inject.Inject 11 | 12 | class DefaultShoppingRepoImp @Inject constructor( 13 | private val dataManager: DataManager 14 | ) : ISoppingRepository { 15 | 16 | override suspend fun insertShoppingItem(shoppingItem: ShoppingItem) { 17 | dataManager.shoppingDao.insertShoppingItem(shoppingItem) 18 | } 19 | 20 | override suspend fun deleteShoppingItem(shoppingItem: ShoppingItem) { 21 | dataManager.shoppingDao.deleteShoppingItem(shoppingItem) 22 | } 23 | 24 | override fun observeAllShoppingItems(): LiveData> { 25 | return dataManager.shoppingDao.observeAllShoppingItems() 26 | } 27 | 28 | override fun observeTotalPrice(): LiveData { 29 | return dataManager.shoppingDao.observeTotalPrice() 30 | } 31 | 32 | override suspend fun searchForImage(imageQuery: String): Resource { 33 | return try { 34 | val response = dataManager.pixabayApi.searchImages(searchKey = imageQuery) 35 | if (response.isSuccessful) { 36 | response.body()?.let { 37 | return@let Resource.success(it) 38 | } ?: Resource.error("an unKnown error occured", null) 39 | } else 40 | Resource.error("an unKnown error occured", null) 41 | } catch (ex: Exception) { 42 | Resource.error("couldn't reach the server", null) 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/test/java/com/atdev/unittestingpractice/RegistrationUtilTest.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice 2 | 3 | import com.atdev.unittestingpractice.other.registrationform.RegistrationUtil 4 | import com.google.common.truth.Truth.assertThat 5 | import org.junit.Test 6 | 7 | class RegistrationUtilTest { 8 | 9 | @Test 10 | fun `empty userName return false`() { 11 | val result = RegistrationUtil.validateRegistrationInput( 12 | "", 13 | "", 14 | "" 15 | ) 16 | assertThat(result).isFalse() 17 | } 18 | 19 | @Test 20 | fun `valid username and correctly repeated password returns true`() { 21 | val result = RegistrationUtil.validateRegistrationInput( 22 | "Tawfiq", 23 | "123", 24 | "123" 25 | ) 26 | assertThat(result).isTrue() 27 | } 28 | 29 | @Test 30 | fun `username already exists returns false`() { 31 | val result = RegistrationUtil.validateRegistrationInput( 32 | "Mona", 33 | "123", 34 | "123" 35 | ) 36 | assertThat(result).isFalse() 37 | } 38 | 39 | @Test 40 | fun `incorrectly confirmed password returns false`() { 41 | val result = RegistrationUtil.validateRegistrationInput( 42 | "Tawfiq", 43 | "123456", 44 | "abcdefg" 45 | ) 46 | assertThat(result).isFalse() 47 | } 48 | 49 | @Test 50 | fun `empty password returns false`() { 51 | val result = RegistrationUtil.validateRegistrationInput( 52 | "Tawfiq", 53 | "", 54 | "" 55 | ) 56 | assertThat(result).isFalse() 57 | } 58 | 59 | @Test 60 | fun `less than 2 digit password returns false`() { 61 | val result = RegistrationUtil.validateRegistrationInput( 62 | "Tawfiq", 63 | "1", 64 | "dawdaw" 65 | ) 66 | assertThat(result).isFalse() 67 | } 68 | } -------------------------------------------------------------------------------- /app/src/test/java/com/atdev/unittestingpractice/remote/repos/FakeShoppingRepo.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice.remote.repos 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.MutableLiveData 5 | import com.atdev.unittestingpractice.core.entities.ImageResponse 6 | import com.atdev.unittestingpractice.data.local.ShoppingItem 7 | import com.atdev.unittestingpractice.data.remote.repos.ISoppingRepository 8 | import com.atdev.unittestingpractice.other.Resource 9 | 10 | class FakeShoppingRepo : ISoppingRepository { 11 | 12 | private val shoppingItems = mutableListOf() 13 | private val observableShoppingItem = MutableLiveData>(shoppingItems) 14 | private val observableTotalPrice = MutableLiveData() 15 | 16 | private var shouldReturnNetworkError = false 17 | 18 | fun shouldReturnNetworkError(value: Boolean) { 19 | shouldReturnNetworkError = value 20 | } 21 | 22 | private fun refresh() { 23 | observableShoppingItem.postValue(shoppingItems) 24 | observableTotalPrice.postValue(getTotalPrice()) 25 | } 26 | 27 | private fun getTotalPrice(): Float { 28 | return shoppingItems.sumByDouble { it.price }.toFloat() 29 | } 30 | 31 | override suspend fun insertShoppingItem(shoppingItem: ShoppingItem) { 32 | shoppingItems.add(shoppingItem) 33 | refresh() 34 | } 35 | 36 | override suspend fun deleteShoppingItem(shoppingItem: ShoppingItem) { 37 | shoppingItems.remove(shoppingItem) 38 | refresh() 39 | } 40 | 41 | override fun observeAllShoppingItems(): LiveData> { 42 | return observableShoppingItem 43 | } 44 | 45 | override fun observeTotalPrice(): LiveData { 46 | return observableTotalPrice 47 | } 48 | 49 | override suspend fun searchForImage(imageQuery: String): Resource { 50 | return if (shouldReturnNetworkError) { 51 | Resource.error("Error", null) 52 | } else { 53 | Resource.success(ImageResponse(0, 0, emptyList())) 54 | } 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/atdev/unittestingpractice/util/LiveDataUtilAndroidTest.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice.util 2 | 3 | /* 4 | * Copyright (C) 2019 The Android Open Source Project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import androidx.annotation.VisibleForTesting 20 | import androidx.lifecycle.LiveData 21 | import androidx.lifecycle.Observer 22 | import java.util.concurrent.CountDownLatch 23 | import java.util.concurrent.TimeUnit 24 | import java.util.concurrent.TimeoutException 25 | 26 | /** 27 | * Gets the value of a [LiveData] or waits for it to have one, with a timeout. 28 | * 29 | * Use this extension from host-side (JVM) tests. It's recommended to use it alongside 30 | * `InstantTaskExecutorRule` or a similar mechanism to execute tasks synchronously. 31 | */ 32 | @VisibleForTesting(otherwise = VisibleForTesting.NONE) 33 | fun LiveData.getOrAwaitValue( 34 | time: Long = 2, 35 | timeUnit: TimeUnit = TimeUnit.SECONDS, 36 | afterObserve: () -> Unit = {} 37 | ): T { 38 | var data: T? = null 39 | val latch = CountDownLatch(1) 40 | val observer = object : Observer { 41 | override fun onChanged(o: T?) { 42 | data = o 43 | latch.countDown() 44 | this@getOrAwaitValue.removeObserver(this) 45 | } 46 | } 47 | this.observeForever(observer) 48 | 49 | try { 50 | afterObserve.invoke() 51 | 52 | // Don't wait indefinitely if the LiveData is not set. 53 | if (!latch.await(time, timeUnit)) { 54 | throw TimeoutException("LiveData value was never set.") 55 | } 56 | 57 | } finally { 58 | this.removeObserver(observer) 59 | } 60 | 61 | @Suppress("UNCHECKED_CAST") 62 | return data as T 63 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/atdev/unittestingpractice/data/local/ShoppingDaoTest.kt: -------------------------------------------------------------------------------- 1 | package com.atdev.unittestingpractice.data.local 2 | 3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule 4 | import androidx.room.Room 5 | import androidx.test.core.app.ApplicationProvider 6 | import androidx.test.ext.junit.runners.AndroidJUnit4 7 | import androidx.test.filters.SmallTest 8 | import com.atdev.unittestingpractice.util.getOrAwaitValue 9 | import com.google.common.truth.Truth.assertThat 10 | import kotlinx.coroutines.ExperimentalCoroutinesApi 11 | import kotlinx.coroutines.test.runBlockingTest 12 | import org.junit.After 13 | import org.junit.Before 14 | import org.junit.Rule 15 | import org.junit.Test 16 | import org.junit.runner.RunWith 17 | 18 | @RunWith(AndroidJUnit4::class) 19 | @ExperimentalCoroutinesApi 20 | @SmallTest 21 | class ShoppingDaoTest { 22 | 23 | @get: Rule 24 | var instantTaskExecutorRule = InstantTaskExecutorRule() 25 | 26 | private lateinit var database: ShoppingDatabase 27 | private lateinit var dao: ShoppingDao 28 | 29 | @Before 30 | fun setup() { 31 | database = Room.inMemoryDatabaseBuilder( 32 | ApplicationProvider.getApplicationContext(), 33 | ShoppingDatabase::class.java 34 | ).allowMainThreadQueries() 35 | .build() 36 | dao = database.shoppingDao() 37 | } 38 | 39 | @After 40 | fun tearDown() { 41 | database.close() 42 | } 43 | 44 | @Test 45 | fun insertShoppingItem() = runBlockingTest { 46 | val shoppingItem = 47 | ShoppingItem(name = "Ahmed", amount = 12.0, price = 2.0, imageUrl = "", id = 1) 48 | dao.insertShoppingItem(shoppingItem) 49 | 50 | val allShoppingItems = dao.observeAllShoppingItems().getOrAwaitValue() 51 | assertThat(allShoppingItems).contains(shoppingItem) 52 | } 53 | 54 | @Test 55 | fun deleteShoppingItem() = runBlockingTest { 56 | val shoppingItem = 57 | ShoppingItem(name = "Ahmed", amount = 12.0, price = 2.0, imageUrl = "", id = 1) 58 | dao.insertShoppingItem(shoppingItem) 59 | dao.deleteShoppingItem(shoppingItem) 60 | 61 | val allShoppingItems = dao.observeAllShoppingItems().getOrAwaitValue() 62 | assertThat(allShoppingItems).doesNotContain(shoppingItem) 63 | } 64 | 65 | @Test 66 | fun observeTotalPriceShoppingItem() = runBlockingTest { 67 | val shoppingItem1 = 68 | ShoppingItem(name = "Banana", amount = 2.0, price = 2.0, imageUrl = "", id = 1) 69 | val shoppingItem2 = 70 | ShoppingItem(name = "apple", amount = 3.0, price = 3.0, imageUrl = "", id = 2) 71 | val shoppingItem3 = 72 | ShoppingItem(name = "carbret", amount = 4.0, price = 4.0, imageUrl = "", id = 3) 73 | 74 | dao.insertShoppingItem(shoppingItem1) 75 | dao.insertShoppingItem(shoppingItem2) 76 | dao.insertShoppingItem(shoppingItem3) 77 | 78 | val allShoppingItems = dao.observeTotalPrice().getOrAwaitValue() 79 | 80 | assertThat(allShoppingItems).isEqualTo(2 * 2 + 3 * 3 + 4 * 4) 81 | } 82 | } -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 119 | 120 | 122 | 123 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'kotlin-kapt' 5 | id 'dagger.hilt.android.plugin' 6 | } 7 | 8 | android { 9 | compileSdkVersion 30 10 | buildToolsVersion "30.0.3" 11 | 12 | 13 | defaultConfig { 14 | applicationId "com.atdev.unittestingpractice" 15 | minSdkVersion 16 16 | targetSdkVersion 30 17 | versionCode 1 18 | versionName "1.0" 19 | 20 | buildConfigField("String","APi_KRY_Pixabay",APi_KRY_Pixabay) 21 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 22 | } 23 | 24 | buildTypes { 25 | release { 26 | minifyEnabled false 27 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 28 | } 29 | } 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | kotlinOptions { 35 | jvmTarget = '1.8' 36 | } 37 | 38 | } 39 | 40 | dependencies { 41 | 42 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 43 | implementation 'androidx.core:core-ktx:1.3.2' 44 | implementation 'androidx.appcompat:appcompat:1.2.0' 45 | implementation 'com.google.android.material:material:1.3.0' 46 | implementation 'androidx.constraintlayout:constraintlayout:2.0.4' 47 | 48 | // Architectural Components 49 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1" 50 | 51 | // Lifecycle 52 | implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" 53 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1" 54 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1" 55 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1" 56 | 57 | // Room 58 | implementation "androidx.room:room-runtime:2.3.0" 59 | kapt "androidx.room:room-compiler:2.3.0" 60 | 61 | // Kotlin Extensions and Coroutines support for Room 62 | implementation "androidx.room:room-ktx:2.3.0" 63 | 64 | // Retrofit 65 | implementation 'com.squareup.retrofit2:retrofit:2.9.0' 66 | implementation 'com.squareup.retrofit2:converter-gson:2.9.0' 67 | 68 | // Coroutines 69 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1' 70 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2' 71 | 72 | // Coroutine Lifecycle Scopes 73 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1" 74 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.1" 75 | 76 | // Navigation Components 77 | implementation "androidx.navigation:navigation-fragment-ktx:2.3.5" 78 | implementation "androidx.navigation:navigation-ui-ktx:2.3.5" 79 | 80 | // Glide 81 | implementation 'com.github.bumptech.glide:glide:4.11.0' 82 | kapt 'com.github.bumptech.glide:compiler:4.11.0' 83 | 84 | // Activity KTX for viewModels() 85 | implementation "androidx.activity:activity-ktx:1.2.2" 86 | 87 | //Dagger - Hilt 88 | implementation "com.google.dagger:hilt-android:2.31.2-alpha" 89 | kapt "com.google.dagger:hilt-compiler:2.31.2-alpha" 90 | implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03' 91 | 92 | kapt "androidx.hilt:hilt-compiler:1.0.0-beta01" 93 | 94 | // Timber 95 | implementation 'com.jakewharton.timber:timber:4.7.1' 96 | 97 | // Local Unit Tests 98 | implementation "androidx.test:core:1.3.0" 99 | testImplementation "junit:junit:4.13.2" 100 | testImplementation "org.hamcrest:hamcrest-all:1.3" 101 | testImplementation "androidx.arch.core:core-testing:2.1.0" 102 | testImplementation "org.robolectric:robolectric:4.3.1" 103 | testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.2" 104 | testImplementation "com.google.truth:truth:1.1.2" 105 | testImplementation "org.mockito:mockito-core:2.21.0" 106 | 107 | // Instrumented Unit Tests 108 | androidTestImplementation "junit:junit:4.13.2" 109 | androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:2.12.1" 110 | androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.2" 111 | androidTestImplementation "androidx.arch.core:core-testing:2.1.0" 112 | androidTestImplementation "com.google.truth:truth:1.1.2" 113 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 114 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 115 | androidTestImplementation "org.mockito:mockito-core:2.21.0" 116 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | --------------------------------------------------------------------------------