├── .gitignore ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── mockito_fundamentals ├── .gitignore ├── build.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── techyourchance │ │ └── mockitofundamentals │ │ ├── example7 │ │ ├── LoginUseCaseSync.java │ │ ├── authtoken │ │ │ └── AuthTokenCache.java │ │ ├── eventbus │ │ │ ├── EventBusPoster.java │ │ │ └── LoggedInEvent.java │ │ └── networking │ │ │ ├── LoginHttpEndpointSync.java │ │ │ └── NetworkErrorException.java │ │ ├── example8 │ │ ├── Address.java │ │ ├── PhoneNumber.java │ │ ├── User.java │ │ ├── UserMess.java │ │ └── UserObject.java │ │ └── exercise5 │ │ ├── UpdateUsernameUseCaseSync.java │ │ ├── description.txt │ │ ├── eventbus │ │ ├── EventBusPoster.java │ │ └── UserDetailsChangedEvent.java │ │ ├── networking │ │ ├── NetworkErrorException.java │ │ └── UpdateUsernameHttpEndpointSync.java │ │ └── users │ │ ├── User.java │ │ └── UsersCache.java │ └── test │ └── java │ └── com │ └── techyourchance │ └── mockitofundamentals │ ├── example7 │ └── LoginUseCaseSyncTest.java │ └── exercise5 │ ├── ExerciseSolution5.java │ └── UpdateUsernameUseCaseSyncTest.java ├── settings.gradle ├── test_doubles_fundamentals ├── build.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── techyourchance │ │ └── testdoublesfundamentals │ │ ├── example4 │ │ ├── LoginUseCaseSync.java │ │ ├── authtoken │ │ │ └── AuthTokenCache.java │ │ ├── eventbus │ │ │ ├── EventBusPoster.java │ │ │ └── LoggedInEvent.java │ │ └── networking │ │ │ ├── LoginHttpEndpointSync.java │ │ │ └── NetworkErrorException.java │ │ ├── example5 │ │ ├── FullNameValidator.java │ │ ├── ServerUsernameValidator.java │ │ └── UserInputValidator.java │ │ ├── example6 │ │ ├── Counter.java │ │ └── FitnessTracker.java │ │ └── exercise4 │ │ ├── FetchUserProfileUseCaseSync.java │ │ ├── description.txt │ │ ├── networking │ │ └── UserProfileHttpEndpointSync.java │ │ └── users │ │ ├── User.java │ │ └── UsersCache.java │ └── test │ └── java │ └── com │ └── techyourchance │ └── testdoublesfundamentals │ ├── example4 │ └── LoginUseCaseSyncTest.java │ ├── example5 │ └── UserInputValidatorTest.java │ ├── example6 │ └── FitnessTrackerTest.java │ └── exercise4 │ ├── ExerciseSolution4.java │ └── FetchUserProfileUseCaseSyncTest.java ├── test_driven_development ├── .gitignore ├── build.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── techyourchance │ │ └── testdrivendevelopment │ │ ├── example10 │ │ ├── PingServerSyncUseCase.java │ │ └── networking │ │ │ └── PingServerHttpEndpointSync.java │ │ ├── example11 │ │ ├── FetchCartItemsUseCase.java │ │ ├── cart │ │ │ └── CartItem.java │ │ └── networking │ │ │ ├── CartItemSchema.java │ │ │ └── GetCartItemsHttpEndpoint.java │ │ ├── example9 │ │ ├── AddToCartUseCaseSync.java │ │ └── networking │ │ │ ├── AddToCartHttpEndpointSync.java │ │ │ ├── CartItemScheme.java │ │ │ └── NetworkErrorException.java │ │ ├── exercise6 │ │ ├── FetchUserUseCaseSync.java │ │ ├── description.txt │ │ ├── networking │ │ │ ├── FetchUserHttpEndpointSync.java │ │ │ └── NetworkErrorException.java │ │ └── users │ │ │ ├── User.java │ │ │ └── UsersCache.java │ │ ├── exercise7 │ │ ├── description.txt │ │ └── networking │ │ │ └── GetReputationHttpEndpointSync.java │ │ └── exercise8 │ │ ├── contacts │ │ └── Contact.java │ │ ├── description.txt │ │ └── networking │ │ ├── ContactSchema.java │ │ └── GetContactsHttpEndpoint.java │ └── test │ └── java │ └── com │ └── techyourchance │ └── testdrivendevelopment │ ├── example10 │ └── PingServerSyncUseCaseTest.java │ ├── example11 │ ├── FetchCartItemsManualTestDoublesUseCaseTest.java │ └── FetchCartItemsUseCaseTest.java │ ├── example9 │ └── AddToCartUseCaseSyncTest.java │ └── exercise6 │ └── FetchUserUseCaseSyncTestRef.java ├── tutorial_android_application ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── techyourchance │ │ │ └── unittesting │ │ │ ├── common │ │ │ ├── BaseObservable.java │ │ │ ├── Constants.java │ │ │ ├── CustomApplication.java │ │ │ ├── dependencyinjection │ │ │ │ ├── CompositionRoot.java │ │ │ │ └── ControllerCompositionRoot.java │ │ │ └── time │ │ │ │ └── TimeProvider.java │ │ │ ├── description_exercise10.txt │ │ │ ├── description_exercise11.txt │ │ │ ├── description_exercise9.txt │ │ │ ├── hint_exercise11.txt │ │ │ ├── networking │ │ │ ├── StackoverflowApi.java │ │ │ └── questions │ │ │ │ ├── FetchLastActiveQuestionsEndpoint.java │ │ │ │ ├── FetchQuestionDetailsEndpoint.java │ │ │ │ ├── QuestionDetailsResponseSchema.java │ │ │ │ ├── QuestionSchema.java │ │ │ │ └── QuestionsListResponseSchema.java │ │ │ ├── questions │ │ │ ├── FetchLastActiveQuestionsUseCase.java │ │ │ ├── FetchQuestionDetailsUseCase.java │ │ │ ├── Question.java │ │ │ └── QuestionDetails.java │ │ │ └── screens │ │ │ ├── common │ │ │ ├── ViewMvcFactory.java │ │ │ ├── controllers │ │ │ │ ├── BackPressDispatcher.java │ │ │ │ ├── BackPressedListener.java │ │ │ │ ├── BaseActivity.java │ │ │ │ └── BaseFragment.java │ │ │ ├── fragmentframehelper │ │ │ │ ├── FragmentFrameHelper.java │ │ │ │ ├── FragmentFrameWrapper.java │ │ │ │ └── HierarchicalFragment.java │ │ │ ├── main │ │ │ │ └── MainActivity.java │ │ │ ├── navdrawer │ │ │ │ ├── DrawerItems.java │ │ │ │ ├── NavDrawerHelper.java │ │ │ │ ├── NavDrawerViewMvc.java │ │ │ │ └── NavDrawerViewMvcImpl.java │ │ │ ├── screensnavigator │ │ │ │ └── ScreensNavigator.java │ │ │ ├── toastshelper │ │ │ │ └── ToastsHelper.java │ │ │ ├── toolbar │ │ │ │ └── ToolbarViewMvc.java │ │ │ └── views │ │ │ │ ├── BaseObservableViewMvc.java │ │ │ │ ├── BaseViewMvc.java │ │ │ │ ├── ObservableViewMvc.java │ │ │ │ └── ViewMvc.java │ │ │ ├── questiondetails │ │ │ ├── QuestionDetailsController.java │ │ │ ├── QuestionDetailsFragment.java │ │ │ ├── QuestionDetailsViewMvc.java │ │ │ └── QuestionDetailsViewMvcImpl.java │ │ │ └── questionslist │ │ │ ├── QuestionsListController.java │ │ │ ├── QuestionsListFragment.java │ │ │ ├── QuestionsListViewMvc.java │ │ │ ├── QuestionsListViewMvcImpl.java │ │ │ ├── QuestionsRecyclerAdapter.java │ │ │ └── questionslistitem │ │ │ ├── QuestionsListItemViewMvc.java │ │ │ └── QuestionsListItemViewMvcImpl.java │ └── res │ │ ├── drawable-hdpi │ │ ├── ic_arrow_back.png │ │ ├── ic_menu.png │ │ └── ic_view_list.png │ │ ├── drawable-mdpi │ │ ├── ic_arrow_back.png │ │ ├── ic_menu.png │ │ └── ic_view_list.png │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable-xhdpi │ │ ├── ic_arrow_back.png │ │ ├── ic_menu.png │ │ └── ic_view_list.png │ │ ├── drawable-xxhdpi │ │ ├── ic_arrow_back.png │ │ ├── ic_menu.png │ │ └── ic_view_list.png │ │ ├── drawable-xxxhdpi │ │ ├── ic_arrow_back.png │ │ ├── ic_menu.png │ │ └── ic_view_list.png │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── element_toolbar.xml │ │ ├── layout_content_frame.xml │ │ ├── layout_drawer.xml │ │ ├── layout_question_details.xml │ │ ├── layout_question_list_item.xml │ │ ├── layout_questions_list.xml │ │ └── layout_toolbar.xml │ │ ├── menu │ │ └── menu_drawer.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.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 │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── techyourchance │ └── unittesting │ ├── questions │ ├── FetchLastActiveQuestionsUseCaseTest.java │ └── FetchQuestionDetailsUseCaseSolutionTest.java │ ├── screens │ ├── questiondetails │ │ └── QuestionDetailsControllerSolutionTest.java │ └── questionslist │ │ └── QuestionsListControllerTest.java │ └── testdata │ ├── QuestionDetailsTestData.java │ └── QuestionsTestData.java ├── unit_testing_fundamentals ├── build.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── techyourchance │ │ └── unittestingfundamentals │ │ ├── example1 │ │ └── PositiveNumberValidator.java │ │ ├── example2 │ │ └── StringReverser.java │ │ ├── example3 │ │ ├── Interval.java │ │ └── IntervalsOverlapDetector.java │ │ ├── exercise1 │ │ ├── NegativeNumberValidator.java │ │ └── description.txt │ │ ├── exercise2 │ │ ├── StringDuplicator.java │ │ └── description.txt │ │ └── exercise3 │ │ ├── IntervalsAdjacencyDetector.java │ │ └── description.txt │ └── test │ └── java │ └── com │ └── techyourchance │ └── unittestingfundamentals │ ├── example1 │ └── PositiveNumberValidatorTest.java │ ├── example2 │ └── StringReverserTest.java │ ├── example3 │ └── IntervalsOverlapDetectorTest.java │ ├── exercise1 │ ├── ExerciseSolution1.java │ └── NegativeNumberValidatorTest.java │ ├── exercise2 │ ├── ExerciseSolution2.java │ └── StringDuplicatorTest.java │ └── exercise3 │ ├── ExerciseSolution3.java │ └── IntervalsAdjacencyDetectorTest.java └── unit_testing_in_android ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src ├── main ├── AndroidManifest.xml ├── java │ └── com │ │ └── techyourchance │ │ └── unittestinginandroid │ │ ├── example12 │ │ └── StringRetriever.java │ │ ├── example13 │ │ └── AndroidUnitTestingProblems.java │ │ └── example14 │ │ └── MyActivity.java └── res │ └── values │ └── strings.xml └── test └── java └── com └── techyourchance └── unittestinginandroid ├── example12 └── StringRetrieverTest.java ├── example13 └── AndroidUnitTestingProblemsTest.java └── example14 └── MyActivityTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | #built application files 3 | *.apk 4 | *.ap_ 5 | 6 | # files for the dex VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # generated files 13 | bin/ 14 | gen/ 15 | 16 | # Local configuration file (sdk path, etc) 17 | local.properties 18 | 19 | # Windows thumbnail db 20 | Thumbs.db 21 | 22 | # OSX files 23 | .DS_Store 24 | 25 | # Android Studio 26 | .idea/ 27 | .gradle 28 | build/ 29 | *.iml 30 | captures/ 31 | .externalNativeBuild -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android Unit Testing and Test Driven Development 2 | 3 | Tutorial application for [my course about unit testing in Android](https://go.techyourchance.com/android-unit-testing-course-github) 4 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | mavenCentral() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:8.7.3' 11 | 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | mavenCentral() 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | android.enableJetifier=true 13 | android.useAndroidX=true 14 | org.gradle.jvmargs=-Xmx1536m 15 | 16 | # When configured, Gradle will run in incubating parallel mode. 17 | # This option should only be used with decoupled projects. More details, visit 18 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 19 | # org.gradle.parallel=true 20 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /mockito_fundamentals/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /mockito_fundamentals/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | 3 | dependencies { 4 | implementation fileTree(dir: 'libs', include: ['*.jar']) 5 | testImplementation 'junit:junit:4.13.2' 6 | testImplementation 'org.mockito:mockito-core:2.18.3' 7 | } 8 | -------------------------------------------------------------------------------- /mockito_fundamentals/src/main/java/com/techyourchance/mockitofundamentals/example7/LoginUseCaseSync.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mockitofundamentals.example7; 2 | 3 | import com.techyourchance.mockitofundamentals.example7.authtoken.AuthTokenCache; 4 | import com.techyourchance.mockitofundamentals.example7.eventbus.EventBusPoster; 5 | import com.techyourchance.mockitofundamentals.example7.eventbus.LoggedInEvent; 6 | import com.techyourchance.mockitofundamentals.example7.networking.LoginHttpEndpointSync; 7 | import com.techyourchance.mockitofundamentals.example7.networking.NetworkErrorException; 8 | 9 | public class LoginUseCaseSync { 10 | 11 | public enum UseCaseResult { 12 | SUCCESS, 13 | FAILURE, 14 | NETWORK_ERROR 15 | } 16 | 17 | private final LoginHttpEndpointSync mLoginHttpEndpointSync; 18 | private final AuthTokenCache mAuthTokenCache; 19 | private final EventBusPoster mEventBusPoster; 20 | 21 | public LoginUseCaseSync(LoginHttpEndpointSync loginHttpEndpointSync, 22 | AuthTokenCache authTokenCache, 23 | EventBusPoster eventBusPoster) { 24 | mLoginHttpEndpointSync = loginHttpEndpointSync; 25 | mAuthTokenCache = authTokenCache; 26 | mEventBusPoster = eventBusPoster; 27 | } 28 | 29 | public UseCaseResult loginSync(String username, String password) { 30 | LoginHttpEndpointSync.EndpointResult endpointEndpointResult; 31 | try { 32 | endpointEndpointResult = mLoginHttpEndpointSync.loginSync(username, password); 33 | } catch (NetworkErrorException e) { 34 | return UseCaseResult.NETWORK_ERROR; 35 | } 36 | 37 | if (isSuccessfulEndpointResult(endpointEndpointResult)) { 38 | mAuthTokenCache.cacheAuthToken(endpointEndpointResult.getAuthToken()); 39 | mEventBusPoster.postEvent(new LoggedInEvent()); 40 | return UseCaseResult.SUCCESS; 41 | } else { 42 | return UseCaseResult.FAILURE; 43 | } 44 | } 45 | 46 | private boolean isSuccessfulEndpointResult(LoginHttpEndpointSync.EndpointResult endpointResult) { 47 | return endpointResult.getStatus() == LoginHttpEndpointSync.EndpointResultStatus.SUCCESS; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /mockito_fundamentals/src/main/java/com/techyourchance/mockitofundamentals/example7/authtoken/AuthTokenCache.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mockitofundamentals.example7.authtoken; 2 | 3 | public interface AuthTokenCache { 4 | 5 | void cacheAuthToken(String authToken); 6 | 7 | String getAuthToken(); 8 | } 9 | -------------------------------------------------------------------------------- /mockito_fundamentals/src/main/java/com/techyourchance/mockitofundamentals/example7/eventbus/EventBusPoster.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mockitofundamentals.example7.eventbus; 2 | 3 | public interface EventBusPoster { 4 | 5 | void postEvent(Object event); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /mockito_fundamentals/src/main/java/com/techyourchance/mockitofundamentals/example7/eventbus/LoggedInEvent.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mockitofundamentals.example7.eventbus; 2 | 3 | public class LoggedInEvent {} 4 | -------------------------------------------------------------------------------- /mockito_fundamentals/src/main/java/com/techyourchance/mockitofundamentals/example7/networking/LoginHttpEndpointSync.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mockitofundamentals.example7.networking; 2 | 3 | public interface LoginHttpEndpointSync { 4 | 5 | /** 6 | * Log in using provided credentials 7 | * @return the aggregated result of login operation 8 | * @throws NetworkErrorException if login attempt failed due to network error 9 | */ 10 | EndpointResult loginSync(String username, String password) throws NetworkErrorException; 11 | 12 | enum EndpointResultStatus { 13 | SUCCESS, 14 | AUTH_ERROR, 15 | SERVER_ERROR, 16 | GENERAL_ERROR 17 | } 18 | 19 | class EndpointResult { 20 | private final EndpointResultStatus mStatus; 21 | private final String mAuthToken; 22 | 23 | public EndpointResult(EndpointResultStatus status, String authToken) { 24 | mStatus = status; 25 | mAuthToken = authToken; 26 | } 27 | 28 | public EndpointResultStatus getStatus() { 29 | return mStatus; 30 | } 31 | 32 | public String getAuthToken() { 33 | return mAuthToken; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /mockito_fundamentals/src/main/java/com/techyourchance/mockitofundamentals/example7/networking/NetworkErrorException.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mockitofundamentals.example7.networking; 2 | 3 | public class NetworkErrorException extends Exception { 4 | } 5 | -------------------------------------------------------------------------------- /mockito_fundamentals/src/main/java/com/techyourchance/mockitofundamentals/example8/Address.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mockitofundamentals.example8; 2 | 3 | public class Address { 4 | private final String mCountry; 5 | private final String mCity; 6 | private final String mStreet; 7 | private final int mHomeNumber; 8 | 9 | public Address(String country, String city, String street, int homeNumber) { 10 | mCountry = country; 11 | mCity = city; 12 | mStreet = street; 13 | mHomeNumber = homeNumber; 14 | } 15 | 16 | public String getCountry() { 17 | return mCountry; 18 | } 19 | 20 | public String getCity() { 21 | return mCity; 22 | } 23 | 24 | public String getStreet() { 25 | return mStreet; 26 | } 27 | 28 | public int getHomeNumber() { 29 | return mHomeNumber; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /mockito_fundamentals/src/main/java/com/techyourchance/mockitofundamentals/example8/PhoneNumber.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mockitofundamentals.example8; 2 | 3 | public class PhoneNumber { 4 | private final String mCountryCode; 5 | private final String mNumber; 6 | 7 | public PhoneNumber(String countryCode, String number) { 8 | mCountryCode = countryCode; 9 | mNumber = number; 10 | } 11 | 12 | public String getCountryCode() { 13 | return mCountryCode; 14 | } 15 | 16 | public String getNumber() { 17 | return mNumber; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /mockito_fundamentals/src/main/java/com/techyourchance/mockitofundamentals/example8/User.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mockitofundamentals.example8; 2 | 3 | public class User { 4 | private final String mFullName; 5 | private final Address mAddress; 6 | private final PhoneNumber mPhoneNumber; 7 | 8 | public User(String fullName, Address address, PhoneNumber phoneNumber) { 9 | mFullName = fullName; 10 | mAddress = address; 11 | mPhoneNumber = phoneNumber; 12 | } 13 | 14 | public String getFullName() { 15 | return mFullName; 16 | } 17 | 18 | public Address getAddress() { 19 | return mAddress; 20 | } 21 | 22 | public PhoneNumber getPhoneNumber() { 23 | return mPhoneNumber; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /mockito_fundamentals/src/main/java/com/techyourchance/mockitofundamentals/example8/UserMess.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mockitofundamentals.example8; 2 | 3 | import java.util.List; 4 | 5 | public class UserMess { 6 | 7 | private final String mFullName; 8 | private final Address mAddress; 9 | private final PhoneNumber mPhoneNumber; 10 | 11 | public UserMess(String fullName, Address address, PhoneNumber phoneNumber) { 12 | mFullName = fullName; 13 | mAddress = address; 14 | mPhoneNumber = phoneNumber; 15 | } 16 | 17 | public void logOut() { 18 | // real implementation here 19 | } 20 | 21 | public void connectWith(UserMess otherUser) { 22 | // real implementation here 23 | } 24 | 25 | public List getConnectedUsers() { 26 | // real implementation here 27 | return null; 28 | } 29 | 30 | public void disconnectFromAll() { 31 | // real implementation here 32 | } 33 | 34 | public String getFullName() { 35 | return mFullName; 36 | } 37 | 38 | public Address getAddress() { 39 | return mAddress; 40 | } 41 | 42 | public PhoneNumber getPhoneNumber() { 43 | return mPhoneNumber; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /mockito_fundamentals/src/main/java/com/techyourchance/mockitofundamentals/example8/UserObject.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mockitofundamentals.example8; 2 | 3 | import java.util.List; 4 | 5 | public class UserObject { 6 | 7 | public void logOut() { 8 | // real implementation here 9 | } 10 | 11 | public void connectWith(UserObject otherUser) { 12 | // real implementation here 13 | } 14 | 15 | public List getConnectedUsers() { 16 | // real implementation here 17 | return null; 18 | } 19 | 20 | public void disconnectFromAll() { 21 | // real implementation here 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /mockito_fundamentals/src/main/java/com/techyourchance/mockitofundamentals/exercise5/UpdateUsernameUseCaseSync.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mockitofundamentals.exercise5; 2 | 3 | import com.techyourchance.mockitofundamentals.exercise5.eventbus.EventBusPoster; 4 | import com.techyourchance.mockitofundamentals.exercise5.eventbus.UserDetailsChangedEvent; 5 | import com.techyourchance.mockitofundamentals.exercise5.networking.NetworkErrorException; 6 | import com.techyourchance.mockitofundamentals.exercise5.networking.UpdateUsernameHttpEndpointSync; 7 | import com.techyourchance.mockitofundamentals.exercise5.networking.UpdateUsernameHttpEndpointSync.EndpointResult; 8 | import com.techyourchance.mockitofundamentals.exercise5.networking.UpdateUsernameHttpEndpointSync.EndpointResultStatus; 9 | import com.techyourchance.mockitofundamentals.exercise5.users.User; 10 | import com.techyourchance.mockitofundamentals.exercise5.users.UsersCache; 11 | 12 | public class UpdateUsernameUseCaseSync { 13 | 14 | public enum UseCaseResult { 15 | SUCCESS, 16 | FAILURE, 17 | NETWORK_ERROR 18 | } 19 | 20 | private final UpdateUsernameHttpEndpointSync mUpdateUsernameHttpEndpointSync; 21 | private final UsersCache mUsersCache; 22 | private final EventBusPoster mEventBusPoster; 23 | 24 | public UpdateUsernameUseCaseSync(UpdateUsernameHttpEndpointSync updateUsernameHttpEndpointSync, 25 | UsersCache usersCache, 26 | EventBusPoster eventBusPoster) { 27 | mUpdateUsernameHttpEndpointSync = updateUsernameHttpEndpointSync; 28 | mUsersCache = usersCache; 29 | mEventBusPoster = eventBusPoster; 30 | } 31 | 32 | public UseCaseResult updateUsernameSync(String userId, String username) { 33 | EndpointResult endpointResult = null; 34 | try { 35 | endpointResult = mUpdateUsernameHttpEndpointSync.updateUsername(userId, username); 36 | } catch (NetworkErrorException e) { 37 | // the bug here is "swallowed" exception instead of return 38 | } 39 | 40 | if (isSuccessfulEndpointResult(endpointResult)) { 41 | // the bug here is reversed arguments 42 | User user = new User(endpointResult.getUsername(), endpointResult.getUserId()); 43 | mEventBusPoster.postEvent(new UserDetailsChangedEvent(new User(userId, username))); 44 | mUsersCache.cacheUser(user); 45 | return UseCaseResult.SUCCESS; 46 | } else { 47 | return UseCaseResult.FAILURE; 48 | } 49 | } 50 | 51 | private boolean isSuccessfulEndpointResult(EndpointResult endpointResult) { 52 | // the bug here is the wrong definition of successful response 53 | return endpointResult.getStatus() == EndpointResultStatus.SUCCESS 54 | || endpointResult.getStatus() == EndpointResultStatus.GENERAL_ERROR; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /mockito_fundamentals/src/main/java/com/techyourchance/mockitofundamentals/exercise5/description.txt: -------------------------------------------------------------------------------- 1 | Description: 2 | Your goal is to find all bugs in UpdateUsernameUseCaseSync class without looking at its internal 3 | implementation. 4 | The test class UpdateUsernameUseCaseSyncTest already exists. 5 | You should: 6 | 1) Open the test class and set up system under test (SUT). 7 | 2) Implement the required test doubles with Mockito. 8 | 3) Think about the required test cases and list them as comments. 9 | 4) Write actual tests and identify which requirements aren't satisfied. 10 | 5) Read tests output and try to come up with a hypothesis about the production code. 11 | 6) Fix the production code. 12 | 7) Make sure that all tests pass. -------------------------------------------------------------------------------- /mockito_fundamentals/src/main/java/com/techyourchance/mockitofundamentals/exercise5/eventbus/EventBusPoster.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mockitofundamentals.exercise5.eventbus; 2 | 3 | public interface EventBusPoster { 4 | 5 | void postEvent(Object event); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /mockito_fundamentals/src/main/java/com/techyourchance/mockitofundamentals/exercise5/eventbus/UserDetailsChangedEvent.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mockitofundamentals.exercise5.eventbus; 2 | 3 | import com.techyourchance.mockitofundamentals.exercise5.users.User; 4 | 5 | public class UserDetailsChangedEvent { 6 | 7 | private final User mUser; 8 | 9 | public UserDetailsChangedEvent(User user) { 10 | mUser = user; 11 | } 12 | 13 | public User getUser() { 14 | return mUser; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /mockito_fundamentals/src/main/java/com/techyourchance/mockitofundamentals/exercise5/networking/NetworkErrorException.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mockitofundamentals.exercise5.networking; 2 | 3 | public class NetworkErrorException extends Exception { 4 | } 5 | -------------------------------------------------------------------------------- /mockito_fundamentals/src/main/java/com/techyourchance/mockitofundamentals/exercise5/networking/UpdateUsernameHttpEndpointSync.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mockitofundamentals.exercise5.networking; 2 | 3 | public interface UpdateUsernameHttpEndpointSync { 4 | 5 | /** 6 | * Update user's username on the server 7 | * @return the aggregated result 8 | * @throws NetworkErrorException if operation failed due to network error 9 | */ 10 | EndpointResult updateUsername(String userId, String username) throws NetworkErrorException; 11 | 12 | enum EndpointResultStatus { 13 | SUCCESS, 14 | AUTH_ERROR, 15 | SERVER_ERROR, 16 | GENERAL_ERROR 17 | } 18 | 19 | class EndpointResult { 20 | private final EndpointResultStatus mStatus; 21 | private final String mUserId; 22 | private final String mUsername; 23 | 24 | public EndpointResult(EndpointResultStatus status, String userId, String username) { 25 | mStatus = status; 26 | mUserId = userId; 27 | mUsername = username; 28 | } 29 | 30 | public EndpointResultStatus getStatus() { 31 | return mStatus; 32 | } 33 | 34 | public String getUserId() { 35 | return mUserId; 36 | } 37 | 38 | public String getUsername() { 39 | return mUserId; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /mockito_fundamentals/src/main/java/com/techyourchance/mockitofundamentals/exercise5/users/User.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mockitofundamentals.exercise5.users; 2 | 3 | public class User { 4 | private final String mUserId; 5 | private final String mUsername; 6 | 7 | public User(String userId, String username) { 8 | mUserId = userId; 9 | mUsername = username; 10 | } 11 | 12 | public String getUserId() { 13 | return mUserId; 14 | } 15 | 16 | public String getUsername() { 17 | return mUsername; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /mockito_fundamentals/src/main/java/com/techyourchance/mockitofundamentals/exercise5/users/UsersCache.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mockitofundamentals.exercise5.users; 2 | 3 | public interface UsersCache { 4 | 5 | void cacheUser(User user); 6 | 7 | User getUser(String userId); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /mockito_fundamentals/src/test/java/com/techyourchance/mockitofundamentals/exercise5/UpdateUsernameUseCaseSyncTest.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.mockitofundamentals.exercise5; 2 | 3 | public class UpdateUsernameUseCaseSyncTest { 4 | 5 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':unit_testing_fundamentals' 2 | include ':test_doubles_fundamentals' 3 | include ':mockito_fundamentals' 4 | include ':test_driven_development' 5 | include ':unit_testing_in_android' 6 | include ':tutorial_android_application' 7 | -------------------------------------------------------------------------------- /test_doubles_fundamentals/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | 3 | dependencies { 4 | implementation fileTree(dir: 'libs', include: ['*.jar']) 5 | testImplementation 'junit:junit:4.13.2' 6 | implementation 'org.jetbrains:annotations-java5:15.0' 7 | } 8 | -------------------------------------------------------------------------------- /test_doubles_fundamentals/src/main/java/com/techyourchance/testdoublesfundamentals/example4/LoginUseCaseSync.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdoublesfundamentals.example4; 2 | 3 | import com.techyourchance.testdoublesfundamentals.example4.authtoken.AuthTokenCache; 4 | import com.techyourchance.testdoublesfundamentals.example4.eventbus.EventBusPoster; 5 | import com.techyourchance.testdoublesfundamentals.example4.eventbus.LoggedInEvent; 6 | import com.techyourchance.testdoublesfundamentals.example4.networking.LoginHttpEndpointSync; 7 | import com.techyourchance.testdoublesfundamentals.example4.networking.NetworkErrorException; 8 | 9 | public class LoginUseCaseSync { 10 | 11 | public enum UseCaseResult { 12 | SUCCESS, 13 | FAILURE, 14 | NETWORK_ERROR 15 | } 16 | 17 | private final LoginHttpEndpointSync mLoginHttpEndpointSync; 18 | private final AuthTokenCache mAuthTokenCache; 19 | private final EventBusPoster mEventBusPoster; 20 | 21 | public LoginUseCaseSync(LoginHttpEndpointSync loginHttpEndpointSync, 22 | AuthTokenCache authTokenCache, 23 | EventBusPoster eventBusPoster) { 24 | mLoginHttpEndpointSync = loginHttpEndpointSync; 25 | mAuthTokenCache = authTokenCache; 26 | mEventBusPoster = eventBusPoster; 27 | } 28 | 29 | public UseCaseResult loginSync(String username, String password) { 30 | LoginHttpEndpointSync.EndpointResult endpointEndpointResult; 31 | try { 32 | endpointEndpointResult = mLoginHttpEndpointSync.loginSync(username, password); 33 | } catch (NetworkErrorException e) { 34 | return UseCaseResult.NETWORK_ERROR; 35 | } 36 | 37 | 38 | if (isSuccessfulEndpointResult(endpointEndpointResult)) { 39 | mAuthTokenCache.cacheAuthToken(endpointEndpointResult.getAuthToken()); 40 | mEventBusPoster.postEvent(new LoggedInEvent()); 41 | return UseCaseResult.SUCCESS; 42 | } else { 43 | return UseCaseResult.FAILURE; 44 | } 45 | } 46 | 47 | private boolean isSuccessfulEndpointResult(LoginHttpEndpointSync.EndpointResult endpointResult) { 48 | return endpointResult.getStatus() == LoginHttpEndpointSync.EndpointResultStatus.SUCCESS; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test_doubles_fundamentals/src/main/java/com/techyourchance/testdoublesfundamentals/example4/authtoken/AuthTokenCache.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdoublesfundamentals.example4.authtoken; 2 | 3 | public interface AuthTokenCache { 4 | 5 | void cacheAuthToken(String authToken); 6 | 7 | String getAuthToken(); 8 | } 9 | -------------------------------------------------------------------------------- /test_doubles_fundamentals/src/main/java/com/techyourchance/testdoublesfundamentals/example4/eventbus/EventBusPoster.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdoublesfundamentals.example4.eventbus; 2 | 3 | public interface EventBusPoster { 4 | 5 | void postEvent(Object event); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /test_doubles_fundamentals/src/main/java/com/techyourchance/testdoublesfundamentals/example4/eventbus/LoggedInEvent.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdoublesfundamentals.example4.eventbus; 2 | 3 | public class LoggedInEvent {} 4 | -------------------------------------------------------------------------------- /test_doubles_fundamentals/src/main/java/com/techyourchance/testdoublesfundamentals/example4/networking/LoginHttpEndpointSync.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdoublesfundamentals.example4.networking; 2 | 3 | public interface LoginHttpEndpointSync { 4 | 5 | /** 6 | * Log in using provided credentials 7 | * @return the aggregated result of login operation 8 | * @throws NetworkErrorException if login attempt failed due to network error 9 | */ 10 | EndpointResult loginSync(String username, String password) throws NetworkErrorException; 11 | 12 | enum EndpointResultStatus { 13 | SUCCESS, 14 | AUTH_ERROR, 15 | SERVER_ERROR, 16 | GENERAL_ERROR 17 | } 18 | 19 | class EndpointResult { 20 | private final EndpointResultStatus mStatus; 21 | private final String mAuthToken; 22 | 23 | public EndpointResult(EndpointResultStatus status, String authToken) { 24 | mStatus = status; 25 | mAuthToken = authToken; 26 | } 27 | 28 | public EndpointResultStatus getStatus() { 29 | return mStatus; 30 | } 31 | 32 | public String getAuthToken() { 33 | return mAuthToken; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test_doubles_fundamentals/src/main/java/com/techyourchance/testdoublesfundamentals/example4/networking/NetworkErrorException.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdoublesfundamentals.example4.networking; 2 | 3 | public class NetworkErrorException extends Exception { 4 | } 5 | -------------------------------------------------------------------------------- /test_doubles_fundamentals/src/main/java/com/techyourchance/testdoublesfundamentals/example5/FullNameValidator.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdoublesfundamentals.example5; 2 | 3 | public class FullNameValidator { 4 | 5 | public static boolean isValidFullName(String fullName) { 6 | // trivially simple task 7 | return !fullName.isEmpty(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test_doubles_fundamentals/src/main/java/com/techyourchance/testdoublesfundamentals/example5/ServerUsernameValidator.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdoublesfundamentals.example5; 2 | 3 | import com.techyourchance.testdoublesfundamentals.example4.networking.NetworkErrorException; 4 | 5 | public class ServerUsernameValidator { 6 | 7 | public static boolean isValidUsername(String username) { 8 | // this sleep mimics network request that checks whether username is free, but fails due to 9 | // absence of network connection 10 | try { 11 | Thread.sleep(1000); 12 | throw new RuntimeException("no network connection"); 13 | } catch (InterruptedException e) { 14 | e.printStackTrace(); 15 | return false; 16 | } 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /test_doubles_fundamentals/src/main/java/com/techyourchance/testdoublesfundamentals/example5/UserInputValidator.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdoublesfundamentals.example5; 2 | 3 | public class UserInputValidator { 4 | 5 | public boolean isValidFullName(String fullName) { 6 | return FullNameValidator.isValidFullName(fullName); 7 | } 8 | 9 | public boolean isValidUsername(String username) { 10 | return ServerUsernameValidator.isValidUsername(username); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test_doubles_fundamentals/src/main/java/com/techyourchance/testdoublesfundamentals/example6/Counter.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdoublesfundamentals.example6; 2 | 3 | public class Counter { 4 | 5 | private static Counter sInstance; 6 | 7 | private int mTotalCount; 8 | 9 | private Counter() {} 10 | 11 | /** 12 | * @return reference to Counter Singleton 13 | */ 14 | public static Counter getInstance() { 15 | if (sInstance == null) { 16 | sInstance = new Counter(); 17 | } 18 | return sInstance; 19 | } 20 | 21 | public void add() { 22 | mTotalCount++; 23 | } 24 | 25 | public void add(int count) { 26 | mTotalCount += count; 27 | } 28 | 29 | public int getTotal() { 30 | return mTotalCount; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test_doubles_fundamentals/src/main/java/com/techyourchance/testdoublesfundamentals/example6/FitnessTracker.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdoublesfundamentals.example6; 2 | 3 | public class FitnessTracker { 4 | 5 | public static final int RUN_STEPS_FACTOR = 2; 6 | 7 | public void step() { 8 | Counter.getInstance().add(); 9 | } 10 | 11 | public void runStep() { 12 | Counter.getInstance().add(RUN_STEPS_FACTOR); 13 | } 14 | 15 | public int getTotalSteps() { 16 | return Counter.getInstance().getTotal(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test_doubles_fundamentals/src/main/java/com/techyourchance/testdoublesfundamentals/exercise4/FetchUserProfileUseCaseSync.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdoublesfundamentals.exercise4; 2 | 3 | import com.techyourchance.testdoublesfundamentals.example4.networking.NetworkErrorException; 4 | import com.techyourchance.testdoublesfundamentals.exercise4.networking.UserProfileHttpEndpointSync; 5 | import com.techyourchance.testdoublesfundamentals.exercise4.networking.UserProfileHttpEndpointSync.EndpointResult; 6 | import com.techyourchance.testdoublesfundamentals.exercise4.users.User; 7 | import com.techyourchance.testdoublesfundamentals.exercise4.users.UsersCache; 8 | 9 | public class FetchUserProfileUseCaseSync { 10 | 11 | public enum UseCaseResult { 12 | SUCCESS, 13 | FAILURE, 14 | NETWORK_ERROR 15 | } 16 | 17 | private final UserProfileHttpEndpointSync mUserProfileHttpEndpointSync; 18 | private final UsersCache mUsersCache; 19 | 20 | public FetchUserProfileUseCaseSync(UserProfileHttpEndpointSync userProfileHttpEndpointSync, 21 | UsersCache usersCache) { 22 | mUserProfileHttpEndpointSync = userProfileHttpEndpointSync; 23 | mUsersCache = usersCache; 24 | } 25 | 26 | public UseCaseResult fetchUserProfileSync(String userId) { 27 | EndpointResult endpointResult; 28 | try { 29 | // the bug here is that userId is not passed to endpoint 30 | endpointResult = mUserProfileHttpEndpointSync.getUserProfile(""); 31 | // the bug here is that I don't check for successful result and it's also a duplication 32 | // of the call later in this method 33 | mUsersCache.cacheUser( 34 | new User(userId, endpointResult.getFullName(), endpointResult.getImageUrl())); 35 | } catch (NetworkErrorException e) { 36 | return UseCaseResult.NETWORK_ERROR; 37 | } 38 | 39 | if (isSuccessfulEndpointResult(endpointResult)) { 40 | mUsersCache.cacheUser( 41 | new User(userId, endpointResult.getFullName(), endpointResult.getImageUrl())); 42 | } 43 | 44 | // the bug here is that I return wrong result in case of an unsuccessful server response 45 | return UseCaseResult.SUCCESS; 46 | } 47 | 48 | private boolean isSuccessfulEndpointResult(EndpointResult endpointResult) { 49 | return endpointResult.getStatus() == UserProfileHttpEndpointSync.EndpointResultStatus.SUCCESS; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test_doubles_fundamentals/src/main/java/com/techyourchance/testdoublesfundamentals/exercise4/description.txt: -------------------------------------------------------------------------------- 1 | Description: 2 | Your goal is to find all bugs in FetchUserProfileUseCaseSync class without looking at its internal 3 | implementation. 4 | The test class FetchUserProfileUseCaseSyncTest already exists. 5 | You should: 6 | 1) Open the test class and set up system under test (SUT). 7 | 2) Implement the required test doubles. 8 | 3) Think about the required test cases and list them as comments. 9 | 4) Write actual tests and identify which requirements aren't satisfied. 10 | 5) Read tests output and try to come up with a hypothesis about the production code. 11 | 6) Fix the production code. 12 | 7) Make sure that all tests pass. 13 | -------------------------------------------------------------------------------- /test_doubles_fundamentals/src/main/java/com/techyourchance/testdoublesfundamentals/exercise4/networking/UserProfileHttpEndpointSync.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdoublesfundamentals.exercise4.networking; 2 | 3 | import com.techyourchance.testdoublesfundamentals.example4.networking.NetworkErrorException; 4 | 5 | public interface UserProfileHttpEndpointSync { 6 | 7 | /** 8 | * Get user's profile from the server 9 | * @return the aggregated result 10 | * @throws NetworkErrorException if operation failed due to network error 11 | */ 12 | EndpointResult getUserProfile(String userId) throws NetworkErrorException; 13 | 14 | enum EndpointResultStatus { 15 | SUCCESS, 16 | AUTH_ERROR, 17 | SERVER_ERROR, 18 | GENERAL_ERROR 19 | } 20 | 21 | class EndpointResult { 22 | private final EndpointResultStatus mStatus; 23 | private final String mUserId; 24 | private final String mFullName; 25 | private final String mImageUrl; 26 | 27 | public EndpointResult(EndpointResultStatus status, String userId, String fullName, String imageUrl) { 28 | mStatus = status; 29 | mUserId = userId; 30 | mFullName = fullName; 31 | mImageUrl = imageUrl; 32 | } 33 | 34 | public EndpointResultStatus getStatus() { 35 | return mStatus; 36 | } 37 | 38 | public String getUserId() { 39 | return mUserId; 40 | } 41 | 42 | public String getFullName() { 43 | return mFullName; 44 | } 45 | 46 | public String getImageUrl() { 47 | return mImageUrl; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test_doubles_fundamentals/src/main/java/com/techyourchance/testdoublesfundamentals/exercise4/users/User.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdoublesfundamentals.exercise4.users; 2 | 3 | public class User { 4 | private final String mUserId; 5 | private final String mFullName; 6 | private final String mImageUrl; 7 | 8 | public User(String userId, String fullName, String imageUrl) { 9 | mUserId = userId; 10 | mFullName = fullName; 11 | mImageUrl = imageUrl; 12 | } 13 | 14 | public String getUserId() { 15 | return mUserId; 16 | } 17 | 18 | public String getFullName() { 19 | return mFullName; 20 | } 21 | 22 | public String getImageUrl() { 23 | return mImageUrl; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test_doubles_fundamentals/src/main/java/com/techyourchance/testdoublesfundamentals/exercise4/users/UsersCache.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdoublesfundamentals.exercise4.users; 2 | 3 | import org.jetbrains.annotations.Nullable; 4 | 5 | public interface UsersCache { 6 | 7 | void cacheUser(User user); 8 | 9 | @Nullable User getUser(String userId); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /test_doubles_fundamentals/src/test/java/com/techyourchance/testdoublesfundamentals/example5/UserInputValidatorTest.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdoublesfundamentals.example5; 2 | 3 | import org.hamcrest.CoreMatchers; 4 | import org.junit.Assert; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | 8 | import static org.hamcrest.CoreMatchers.is; 9 | import static org.junit.Assert.assertThat; 10 | 11 | public class UserInputValidatorTest { 12 | 13 | UserInputValidator SUT; 14 | 15 | @Before 16 | public void setup() throws Exception { 17 | SUT = new UserInputValidator(); 18 | } 19 | 20 | @Test 21 | public void isValidFullName_validFullName_trueReturned() throws Exception { 22 | boolean result = SUT.isValidFullName("validFullName"); 23 | assertThat(result, is(true)); 24 | } 25 | 26 | @Test 27 | public void isValidFullName_invalidFullName_falseReturned() throws Exception { 28 | boolean result = SUT.isValidFullName(""); 29 | assertThat(result, is(false)); 30 | } 31 | 32 | @Test 33 | public void isValidUsername_validUsername_trueReturned() throws Exception { 34 | boolean result = SUT.isValidUsername("validUsername"); 35 | assertThat(result, is(true)); 36 | } 37 | 38 | @Test 39 | public void isValidUsername_invalidUsername_falseReturned() throws Exception { 40 | boolean result = SUT.isValidUsername(""); 41 | assertThat(result, is(false)); 42 | } 43 | } -------------------------------------------------------------------------------- /test_doubles_fundamentals/src/test/java/com/techyourchance/testdoublesfundamentals/example6/FitnessTrackerTest.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdoublesfundamentals.example6; 2 | 3 | import org.hamcrest.CoreMatchers; 4 | import org.junit.Assert; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | 8 | import static org.hamcrest.CoreMatchers.is; 9 | import static org.junit.Assert.*; 10 | 11 | public class FitnessTrackerTest { 12 | 13 | FitnessTracker SUT; 14 | 15 | @Before 16 | public void setup() throws Exception { 17 | SUT = new FitnessTracker(); 18 | } 19 | 20 | @Test 21 | public void step_totalIncremented() throws Exception { 22 | SUT.step(); 23 | assertThat(SUT.getTotalSteps(), is(1)); 24 | } 25 | 26 | @Test 27 | public void runStep_totalIncrementedByCorrectRatio() throws Exception { 28 | SUT.runStep(); 29 | assertThat(SUT.getTotalSteps(), is(2)); 30 | } 31 | } -------------------------------------------------------------------------------- /test_doubles_fundamentals/src/test/java/com/techyourchance/testdoublesfundamentals/exercise4/FetchUserProfileUseCaseSyncTest.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdoublesfundamentals.exercise4; 2 | 3 | public class FetchUserProfileUseCaseSyncTest { 4 | 5 | } -------------------------------------------------------------------------------- /test_driven_development/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /test_driven_development/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | 3 | dependencies { 4 | implementation fileTree(dir: 'libs', include: ['*.jar']) 5 | testImplementation 'junit:junit:4.13.2' 6 | testImplementation 'org.mockito:mockito-core:2.18.3' 7 | implementation 'org.jetbrains:annotations-java5:15.0' 8 | } -------------------------------------------------------------------------------- /test_driven_development/src/main/java/com/techyourchance/testdrivendevelopment/example10/PingServerSyncUseCase.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdrivendevelopment.example10; 2 | 3 | import com.techyourchance.testdrivendevelopment.example10.networking.PingServerHttpEndpointSync; 4 | 5 | public class PingServerSyncUseCase { 6 | 7 | public enum UseCaseResult { 8 | FAILURE, 9 | SUCCESS 10 | } 11 | 12 | private final PingServerHttpEndpointSync mPingServerHttpEndpointSync; 13 | 14 | public PingServerSyncUseCase(PingServerHttpEndpointSync pingServerHttpEndpointSync) { 15 | mPingServerHttpEndpointSync = pingServerHttpEndpointSync; 16 | } 17 | 18 | public UseCaseResult pingServerSync() { 19 | PingServerHttpEndpointSync.EndpointResult result = mPingServerHttpEndpointSync.pingServerSync(); 20 | switch (result) { 21 | case GENERAL_ERROR: 22 | case NETWORK_ERROR: 23 | return UseCaseResult.FAILURE; 24 | case SUCCESS: 25 | return UseCaseResult.SUCCESS; 26 | default: 27 | throw new RuntimeException("invalid result: " + result); 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /test_driven_development/src/main/java/com/techyourchance/testdrivendevelopment/example10/networking/PingServerHttpEndpointSync.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdrivendevelopment.example10.networking; 2 | 3 | 4 | public interface PingServerHttpEndpointSync { 5 | 6 | enum EndpointResult { 7 | SUCCESS, 8 | GENERAL_ERROR, 9 | NETWORK_ERROR 10 | } 11 | 12 | EndpointResult pingServerSync(); 13 | 14 | 15 | } 16 | -------------------------------------------------------------------------------- /test_driven_development/src/main/java/com/techyourchance/testdrivendevelopment/example11/FetchCartItemsUseCase.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdrivendevelopment.example11; 2 | 3 | import com.techyourchance.testdrivendevelopment.example11.cart.CartItem; 4 | import com.techyourchance.testdrivendevelopment.example11.networking.CartItemSchema; 5 | import com.techyourchance.testdrivendevelopment.example11.networking.GetCartItemsHttpEndpoint; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class FetchCartItemsUseCase { 11 | 12 | public interface Listener { 13 | void onCartItemsFetched(List capture); 14 | void onFetchCartItemsFailed(); 15 | } 16 | 17 | private final List mListeners = new ArrayList<>(); 18 | private final GetCartItemsHttpEndpoint mGetCartItemsHttpEndpoint; 19 | 20 | public FetchCartItemsUseCase(GetCartItemsHttpEndpoint getCartItemsHttpEndpoint) { 21 | mGetCartItemsHttpEndpoint = getCartItemsHttpEndpoint; 22 | } 23 | 24 | public void fetchCartItemsAndNotify(int limit) { 25 | mGetCartItemsHttpEndpoint.getCartItems(limit, new GetCartItemsHttpEndpoint.Callback() { 26 | 27 | @Override 28 | public void onGetCartItemsSucceeded(List cartItems) { 29 | notifySucceeded(cartItems); 30 | } 31 | 32 | @Override 33 | public void onGetCartItemsFailed(GetCartItemsHttpEndpoint.FailReason failReason) { 34 | switch (failReason) { 35 | case GENERAL_ERROR: 36 | case NETWORK_ERROR: 37 | notifyFailed(); 38 | break; 39 | } 40 | } 41 | }); 42 | } 43 | 44 | private void notifySucceeded(List cartItems) { 45 | for (Listener listener : mListeners) { 46 | listener.onCartItemsFetched(cartItemsFromSchemas(cartItems)); 47 | } 48 | } 49 | 50 | private void notifyFailed() { 51 | for (Listener listener : mListeners) { 52 | listener.onFetchCartItemsFailed(); 53 | } 54 | } 55 | 56 | private List cartItemsFromSchemas(List cartItemSchemas) { 57 | List cartItems = new ArrayList<>(); 58 | for (CartItemSchema schema : cartItemSchemas) { 59 | cartItems.add(new CartItem( 60 | schema.getId(), 61 | schema.getTitle(), 62 | schema.getDescription(), 63 | schema.getPrice() 64 | )); 65 | } 66 | return cartItems; 67 | } 68 | 69 | public void registerListener(Listener listener) { 70 | mListeners.add(listener); 71 | } 72 | 73 | public void unregisterListener(Listener listener) { 74 | mListeners.remove(listener); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /test_driven_development/src/main/java/com/techyourchance/testdrivendevelopment/example11/cart/CartItem.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdrivendevelopment.example11.cart; 2 | 3 | public class CartItem { 4 | private final String mId; 5 | private final String mTitle; 6 | private final String mDescription; 7 | private final int mPrice; 8 | 9 | public CartItem(String id, String title, String description, int price) { 10 | mId = id; 11 | mTitle = title; 12 | mDescription = description; 13 | mPrice = price; 14 | } 15 | 16 | public String getId() { 17 | return mId; 18 | } 19 | 20 | public String getTitle() { 21 | return mTitle; 22 | } 23 | 24 | public String getDescription() { 25 | return mDescription; 26 | } 27 | 28 | public int getPrice() { 29 | return mPrice; 30 | } 31 | 32 | @Override 33 | public boolean equals(Object o) { 34 | if (this == o) return true; 35 | if (o == null || getClass() != o.getClass()) return false; 36 | 37 | CartItem cartItem = (CartItem) o; 38 | 39 | if (mPrice != cartItem.mPrice) return false; 40 | if (!mId.equals(cartItem.mId)) return false; 41 | if (!mTitle.equals(cartItem.mTitle)) return false; 42 | return mDescription.equals(cartItem.mDescription); 43 | } 44 | 45 | @Override 46 | public int hashCode() { 47 | int result = mId.hashCode(); 48 | result = 31 * result + mTitle.hashCode(); 49 | result = 31 * result + mDescription.hashCode(); 50 | result = 31 * result + mPrice; 51 | return result; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test_driven_development/src/main/java/com/techyourchance/testdrivendevelopment/example11/networking/CartItemSchema.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdrivendevelopment.example11.networking; 2 | 3 | public class CartItemSchema { 4 | private final String mId; 5 | private final String mTitle; 6 | private final String mDescription; 7 | private final int mPrice; 8 | 9 | public CartItemSchema(String id, String title, String description, int price) { 10 | mId = id; 11 | mTitle = title; 12 | mDescription = description; 13 | mPrice = price; 14 | } 15 | 16 | public String getId() { 17 | return mId; 18 | } 19 | 20 | public String getTitle() { 21 | return mTitle; 22 | } 23 | 24 | public String getDescription() { 25 | return mDescription; 26 | } 27 | 28 | public int getPrice() { 29 | return mPrice; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test_driven_development/src/main/java/com/techyourchance/testdrivendevelopment/example11/networking/GetCartItemsHttpEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdrivendevelopment.example11.networking; 2 | 3 | import java.util.List; 4 | 5 | public interface GetCartItemsHttpEndpoint { 6 | 7 | enum FailReason { 8 | GENERAL_ERROR, 9 | NETWORK_ERROR 10 | } 11 | 12 | interface Callback { 13 | void onGetCartItemsSucceeded(List cartItems); 14 | void onGetCartItemsFailed(FailReason failReason); 15 | } 16 | 17 | /** 18 | * @param limit max amount of cart items to fetch 19 | * @param callback object to be notified when the request completes 20 | */ 21 | public void getCartItems(int limit, Callback callback); 22 | } 23 | -------------------------------------------------------------------------------- /test_driven_development/src/main/java/com/techyourchance/testdrivendevelopment/example9/AddToCartUseCaseSync.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdrivendevelopment.example9; 2 | 3 | 4 | import com.techyourchance.testdrivendevelopment.example9.networking.AddToCartHttpEndpointSync; 5 | import com.techyourchance.testdrivendevelopment.example9.networking.CartItemScheme; 6 | import com.techyourchance.testdrivendevelopment.example9.networking.NetworkErrorException; 7 | 8 | public class AddToCartUseCaseSync { 9 | 10 | public enum UseCaseResult { 11 | SUCCESS, 12 | FAILURE, 13 | NETWORK_ERROR; 14 | } 15 | 16 | private final AddToCartHttpEndpointSync mAddToCartHttpEndpointSync; 17 | 18 | public AddToCartUseCaseSync(AddToCartHttpEndpointSync addToCartHttpEndpointSync) { 19 | mAddToCartHttpEndpointSync = addToCartHttpEndpointSync; 20 | } 21 | 22 | public UseCaseResult addToCartSync(String offerId, int amount) { 23 | AddToCartHttpEndpointSync.EndpointResult result; 24 | 25 | try { 26 | result = mAddToCartHttpEndpointSync.addToCartSync(new CartItemScheme(offerId, amount)); 27 | } catch (NetworkErrorException e) { 28 | return UseCaseResult.NETWORK_ERROR; 29 | } 30 | 31 | switch (result) { 32 | case SUCCESS: 33 | return UseCaseResult.SUCCESS; 34 | case AUTH_ERROR: 35 | case GENERAL_ERROR: 36 | return UseCaseResult.FAILURE; 37 | default: 38 | throw new RuntimeException("invalid endpoint result: " + result); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test_driven_development/src/main/java/com/techyourchance/testdrivendevelopment/example9/networking/AddToCartHttpEndpointSync.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdrivendevelopment.example9.networking; 2 | 3 | public interface AddToCartHttpEndpointSync { 4 | 5 | EndpointResult addToCartSync(CartItemScheme cartItemScheme) throws NetworkErrorException; 6 | 7 | enum EndpointResult { 8 | SUCCESS, 9 | AUTH_ERROR, 10 | GENERAL_ERROR 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /test_driven_development/src/main/java/com/techyourchance/testdrivendevelopment/example9/networking/CartItemScheme.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdrivendevelopment.example9.networking; 2 | 3 | public class CartItemScheme { 4 | 5 | private final String mOfferId; 6 | private final int mAmount; 7 | 8 | public CartItemScheme(String offedId, int amount) { 9 | mOfferId = offedId; 10 | mAmount = amount; 11 | } 12 | 13 | public String getOfferId() { 14 | return mOfferId; 15 | } 16 | 17 | public int getAmount() { 18 | return mAmount; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test_driven_development/src/main/java/com/techyourchance/testdrivendevelopment/example9/networking/NetworkErrorException.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdrivendevelopment.example9.networking; 2 | 3 | public class NetworkErrorException extends Exception { 4 | } 5 | -------------------------------------------------------------------------------- /test_driven_development/src/main/java/com/techyourchance/testdrivendevelopment/exercise6/FetchUserUseCaseSync.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdrivendevelopment.exercise6; 2 | 3 | import org.jetbrains.annotations.Nullable; 4 | import com.techyourchance.testdrivendevelopment.exercise6.users.User; 5 | 6 | interface FetchUserUseCaseSync { 7 | 8 | enum Status { 9 | SUCCESS, 10 | FAILURE, 11 | NETWORK_ERROR 12 | } 13 | 14 | class UseCaseResult { 15 | private final Status mStatus; 16 | 17 | @Nullable 18 | private final User mUser; 19 | 20 | public UseCaseResult(Status status, @Nullable User user) { 21 | mStatus = status; 22 | mUser = user; 23 | } 24 | 25 | public Status getStatus() { 26 | return mStatus; 27 | } 28 | 29 | @Nullable 30 | public User getUser() { 31 | return mUser; 32 | } 33 | } 34 | 35 | UseCaseResult fetchUserSync(String userId); 36 | 37 | } 38 | -------------------------------------------------------------------------------- /test_driven_development/src/main/java/com/techyourchance/testdrivendevelopment/exercise6/description.txt: -------------------------------------------------------------------------------- 1 | Description: 2 | Your goal is to implement FetchUserUseCaseSync interface using TDD. 3 | 4 | The requirements: 5 | 1) If the user with given user ID is not in the cache then it should be fetched from the server. 6 | 2) If the user fetched from the server then it should be stored in the cache before returning to the caller. 7 | 3) If the user is in the cache then cached record should be returned without polling the server. 8 | 9 | You should: 10 | 1) Create a new implementation of the interface WITHOUT writing any actual functionality. 11 | 2) Create a new test class for your implementation. 12 | 3) Write all the required tests. 13 | 4) Run the tests and ensure that all of them fail. 14 | 5) Write the required production code. 15 | 6) Run the tests and ensure that all of them pass. Debug if necessary. 16 | 7) Refactor the production code for readability and maintainability. 17 | 8) Refactor the test code for readability and maintainability. 18 | 9) Assign your implementation to SUT in the class FetchUserUseCaseSyncTestRef. 19 | 10) Ensure that all reference tests pass. Debug if necessary. 20 | -------------------------------------------------------------------------------- /test_driven_development/src/main/java/com/techyourchance/testdrivendevelopment/exercise6/networking/FetchUserHttpEndpointSync.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdrivendevelopment.exercise6.networking; 2 | 3 | 4 | public interface FetchUserHttpEndpointSync { 5 | 6 | enum EndpointStatus { 7 | SUCCESS, 8 | AUTH_ERROR, 9 | GENERAL_ERROR 10 | } 11 | 12 | class EndpointResult { 13 | private final EndpointStatus mStatus; 14 | private final String mUserId; 15 | private final String mUsername; 16 | 17 | public EndpointResult(EndpointStatus status, String userId, String username) { 18 | mStatus = status; 19 | mUserId = userId; 20 | mUsername = username; 21 | } 22 | 23 | public EndpointStatus getStatus() { 24 | return mStatus; 25 | } 26 | 27 | public String getUserId() { 28 | return mUserId; 29 | } 30 | 31 | public String getUsername() { 32 | return mUsername; 33 | } 34 | } 35 | 36 | /** 37 | * Get user details 38 | * @return the aggregated result 39 | * @throws NetworkErrorException if operation failed due to network error 40 | */ 41 | EndpointResult fetchUserSync(String userId) throws NetworkErrorException; 42 | 43 | } 44 | -------------------------------------------------------------------------------- /test_driven_development/src/main/java/com/techyourchance/testdrivendevelopment/exercise6/networking/NetworkErrorException.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdrivendevelopment.exercise6.networking; 2 | 3 | public class NetworkErrorException extends Exception { 4 | } 5 | -------------------------------------------------------------------------------- /test_driven_development/src/main/java/com/techyourchance/testdrivendevelopment/exercise6/users/User.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdrivendevelopment.exercise6.users; 2 | 3 | public class User { 4 | private final String mUserId; 5 | private final String mUsername; 6 | 7 | public User(String userId, String username) { 8 | mUserId = userId; 9 | mUsername = username; 10 | } 11 | 12 | public String getUserId() { 13 | return mUserId; 14 | } 15 | 16 | public String getUsername() { 17 | return mUsername; 18 | } 19 | 20 | @Override 21 | public boolean equals(Object o) { 22 | if (this == o) return true; 23 | if (o == null || getClass() != o.getClass()) return false; 24 | 25 | User user = (User) o; 26 | 27 | if (!mUserId.equals(user.mUserId)) return false; 28 | return mUsername.equals(user.mUsername); 29 | } 30 | 31 | @Override 32 | public int hashCode() { 33 | int result = mUserId.hashCode(); 34 | result = 31 * result + mUsername.hashCode(); 35 | return result; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test_driven_development/src/main/java/com/techyourchance/testdrivendevelopment/exercise6/users/UsersCache.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdrivendevelopment.exercise6.users; 2 | 3 | import org.jetbrains.annotations.Nullable; 4 | 5 | public interface UsersCache { 6 | 7 | void cacheUser(User user); 8 | 9 | @Nullable User getUser(String userId); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /test_driven_development/src/main/java/com/techyourchance/testdrivendevelopment/exercise7/description.txt: -------------------------------------------------------------------------------- 1 | Description: 2 | Your goal is to implement FetchReputationUseCaseSync class using Uncle Bob's TDD technique. 3 | 4 | The three rules: 5 | 1) You are not allowed to write any production code unless it is to make a failing unit test pass 6 | 2) You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures 7 | 3) You are not allowed to write any more production code than is sufficient to pass the one failing unit test 8 | 9 | The requirements: 10 | 1) If the server request completes successfully, then use case should indicate successful completion of the flow. 11 | 2) If the server request completes successfully, then the fetched reputation should be returned 12 | 3) If the server request fails for any reason, the use case should indicate that the flow failed. 13 | 4) If the server request fails for any reason, the returned reputation should be 0. 14 | 15 | You should: 16 | 1) Create a new class FetchReputationUseCaseSync WITHOUT writing any actual functionality. 17 | 2) Create a new test class. 18 | 3) Test drive the implementation of FetchReputationUseCaseSync according to Uncle Bob's three rules of TDD. 19 | Always run all the tests to make sure that further changes don't break the existing functionality. 20 | 4) Refactor the production code. 21 | 5) Run the tests to make sure that all of them pass after the refactoring. 22 | 6) Refactor the tests code. 23 | 7) Run the tests to make sure that all of them pass after the refactoring. 24 | 25 | 26 | -------------------------------------------------------------------------------- /test_driven_development/src/main/java/com/techyourchance/testdrivendevelopment/exercise7/networking/GetReputationHttpEndpointSync.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdrivendevelopment.exercise7.networking; 2 | 3 | 4 | public interface GetReputationHttpEndpointSync { 5 | 6 | enum EndpointStatus { 7 | SUCCESS, 8 | GENERAL_ERROR, 9 | NETWORK_ERROR 10 | } 11 | 12 | class EndpointResult { 13 | private final EndpointStatus mEndpointStatus; 14 | private final int mReputation; 15 | 16 | public EndpointResult(EndpointStatus endpointStatus, int reputation) { 17 | mEndpointStatus = endpointStatus; 18 | mReputation = reputation; 19 | } 20 | 21 | public EndpointStatus getStatus() { 22 | return mEndpointStatus; 23 | } 24 | 25 | public int getReputation() { 26 | return mReputation; 27 | } 28 | } 29 | 30 | EndpointResult getReputationSync(); 31 | 32 | } 33 | -------------------------------------------------------------------------------- /test_driven_development/src/main/java/com/techyourchance/testdrivendevelopment/exercise8/contacts/Contact.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdrivendevelopment.exercise8.contacts; 2 | 3 | public class Contact { 4 | 5 | private final String mId; 6 | private final String mFullName; 7 | private final String mImageUrl; 8 | 9 | public Contact(String id, String fullName, String imageUrl) { 10 | mId = id; 11 | mFullName = fullName; 12 | mImageUrl = imageUrl; 13 | } 14 | 15 | public String getId() { 16 | return mId; 17 | } 18 | 19 | public String getFullName() { 20 | return mFullName; 21 | } 22 | 23 | public String getImageUrl() { 24 | return mImageUrl; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test_driven_development/src/main/java/com/techyourchance/testdrivendevelopment/exercise8/description.txt: -------------------------------------------------------------------------------- 1 | Description: 2 | Your goal is to implement FetchContactsUseCase class using Uncle Bob's TDD technique. This class 3 | must be Observable and notify it's observers (listeners) about completion of an async flow 4 | through method calls. 5 | 6 | The three rules of TDD are: 7 | 1) You are not allowed to write any production code unless it is to make a failing unit test pass 8 | 2) You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures 9 | 3) You are not allowed to write any more production code than is sufficient to pass the one failing unit test 10 | 11 | The requirements: 12 | 1) If the server request completes successfully, then registered listeners should be notified with correct data. 13 | 2) If the server request fails for any reason except network error, then registered listeners should be notified about a failure. 14 | 3) If the server request fails due to network error, then registered listeners should be notified about a network error specifically. 15 | 16 | You should: 17 | 1) Create a new class FetchContactsUseCase WITHOUT writing any actual functionality. 18 | 2) Create a new test class. 19 | 3) Test drive the implementation of FetchContactsUseCase according to Uncle Bob's three rules of TDD. 20 | Always run all the tests to make sure that further changes don't break the existing functionality. 21 | 4) Refactor the production code. 22 | 5) Run the tests to make sure that all of them pass after the refactoring. 23 | 6) Refactor the tests code. 24 | 7) Run the tests to make sure that all of them pass after the refactoring. 25 | 26 | 27 | -------------------------------------------------------------------------------- /test_driven_development/src/main/java/com/techyourchance/testdrivendevelopment/exercise8/networking/ContactSchema.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdrivendevelopment.exercise8.networking; 2 | 3 | public class ContactSchema { 4 | private final String mId; 5 | private final String mFullName; 6 | private final String mFullPhoneNumber; 7 | private final String mImageUrl; 8 | private final double mAge; 9 | 10 | public ContactSchema(String id, String fullName, String fullPhoneNumber, String imageUrl, double age) { 11 | mId = id; 12 | mFullName = fullName; 13 | mFullPhoneNumber = fullPhoneNumber; 14 | mImageUrl = imageUrl; 15 | mAge = age; 16 | } 17 | 18 | public String getId() { 19 | return mId; 20 | } 21 | 22 | public String getFullName() { 23 | return mFullName; 24 | } 25 | 26 | public String getFullPhoneNumber() { 27 | return mFullPhoneNumber; 28 | } 29 | 30 | public double getAge() { 31 | return mAge; 32 | } 33 | 34 | public String getImageUrl() { 35 | return mImageUrl; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test_driven_development/src/main/java/com/techyourchance/testdrivendevelopment/exercise8/networking/GetContactsHttpEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdrivendevelopment.exercise8.networking; 2 | 3 | import java.util.List; 4 | 5 | public interface GetContactsHttpEndpoint { 6 | 7 | enum FailReason { 8 | GENERAL_ERROR, 9 | NETWORK_ERROR 10 | } 11 | 12 | interface Callback { 13 | void onGetContactsSucceeded(List cartItems); 14 | void onGetContactsFailed(FailReason failReason); 15 | } 16 | 17 | /** 18 | * @param filterTerm filter term to match in any of the contact fields 19 | * @param callback object to be notified when the request completes 20 | */ 21 | public void getContacts(String filterTerm, Callback callback); 22 | } 23 | -------------------------------------------------------------------------------- /test_driven_development/src/test/java/com/techyourchance/testdrivendevelopment/example10/PingServerSyncUseCaseTest.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.testdrivendevelopment.example10; 2 | 3 | import com.techyourchance.testdrivendevelopment.example10.networking.PingServerHttpEndpointSync; 4 | 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.mockito.Mock; 9 | import org.mockito.junit.MockitoJUnitRunner; 10 | 11 | 12 | import static org.hamcrest.CoreMatchers.is; 13 | import static org.hamcrest.CoreMatchers.notNullValue; 14 | import static org.hamcrest.CoreMatchers.nullValue; 15 | import static org.junit.Assert.*; 16 | import static org.mockito.Mockito.*; 17 | 18 | @RunWith(MockitoJUnitRunner.class) 19 | public class PingServerSyncUseCaseTest { 20 | 21 | // region constants ---------------------------------------------------------------------------- 22 | // endregion constants ------------------------------------------------------------------------- 23 | 24 | // region helper fields ------------------------------------------------------------------------ 25 | @Mock PingServerHttpEndpointSync mPingServerHttpEndpointSyncMock; 26 | // endregion helper fields --------------------------------------------------------------------- 27 | 28 | PingServerSyncUseCase SUT; 29 | 30 | @Before 31 | public void setup() throws Exception { 32 | SUT = new PingServerSyncUseCase(mPingServerHttpEndpointSyncMock); 33 | success(); 34 | } 35 | 36 | @Test 37 | public void pingServerSync_success_successReturned() throws Exception { 38 | // Arrange 39 | // Act 40 | PingServerSyncUseCase.UseCaseResult result = SUT.pingServerSync(); 41 | // Assert 42 | assertThat(result, is(PingServerSyncUseCase.UseCaseResult.SUCCESS)); 43 | } 44 | 45 | @Test 46 | public void pingServerSync_generalError_failureReturned() throws Exception { 47 | // Arrange 48 | generalError(); 49 | // Act 50 | PingServerSyncUseCase.UseCaseResult result = SUT.pingServerSync(); 51 | // Assert 52 | assertThat(result, is(PingServerSyncUseCase.UseCaseResult.FAILURE)); 53 | } 54 | 55 | @Test 56 | public void pingServerSync_networkError_failureReturned() throws Exception { 57 | // Arrange 58 | networkError(); 59 | // Act 60 | PingServerSyncUseCase.UseCaseResult result = SUT.pingServerSync(); 61 | // Assert 62 | assertThat(result, is(PingServerSyncUseCase.UseCaseResult.FAILURE)); 63 | } 64 | 65 | // region helper methods ----------------------------------------------------------------------- 66 | 67 | private void success() { 68 | when(mPingServerHttpEndpointSyncMock.pingServerSync()).thenReturn(PingServerHttpEndpointSync.EndpointResult.SUCCESS); 69 | } 70 | 71 | private void networkError() { 72 | when(mPingServerHttpEndpointSyncMock.pingServerSync()).thenReturn(PingServerHttpEndpointSync.EndpointResult.NETWORK_ERROR); 73 | } 74 | 75 | private void generalError() { 76 | when(mPingServerHttpEndpointSyncMock.pingServerSync()).thenReturn(PingServerHttpEndpointSync.EndpointResult.GENERAL_ERROR); 77 | } 78 | 79 | // endregion helper methods -------------------------------------------------------------------- 80 | 81 | // region helper classes ----------------------------------------------------------------------- 82 | // endregion helper classes -------------------------------------------------------------------- 83 | 84 | } -------------------------------------------------------------------------------- /tutorial_android_application/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /tutorial_android_application/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | namespace "com.techyourchance.unittesting" 5 | compileSdk = 34 6 | 7 | defaultConfig { 8 | applicationId = "com.techyourchance.unittesting" 9 | minSdk = 19 10 | targetSdk = 34 11 | versionCode = 1 12 | versionName = "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | implementation fileTree(dir: 'libs', include: ['*.jar']) 24 | 25 | implementation 'androidx.appcompat:appcompat:1.4.1' 26 | implementation 'com.google.android.material:material:1.4.0' 27 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 28 | 29 | implementation 'com.squareup.retrofit2:retrofit:2.3.0' 30 | implementation 'com.squareup.retrofit2:converter-gson:2.3.0' 31 | 32 | testImplementation 'junit:junit:4.13.2' 33 | testImplementation 'org.mockito:mockito-core:2.18.3' 34 | } 35 | -------------------------------------------------------------------------------- /tutorial_android_application/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 22 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 15 | 16 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/common/BaseObservable.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.common; 2 | 3 | import java.util.Collections; 4 | import java.util.Set; 5 | import java.util.concurrent.ConcurrentHashMap; 6 | 7 | public abstract class BaseObservable { 8 | 9 | // thread-safe set of listeners 10 | private final Set mListeners = Collections.newSetFromMap( 11 | new ConcurrentHashMap(1)); 12 | 13 | 14 | public final void registerListener(LISTENER_CLASS listener) { 15 | mListeners.add(listener); 16 | } 17 | 18 | public final void unregisterListener(LISTENER_CLASS listener) { 19 | mListeners.remove(listener); 20 | } 21 | 22 | protected final Set getListeners() { 23 | return Collections.unmodifiableSet(mListeners); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/common/Constants.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.common; 2 | 3 | public final class Constants { 4 | private Constants() {} 5 | 6 | public static final int QUESTIONS_LIST_PAGE_SIZE = 20; 7 | 8 | public static final String BASE_URL = "https://api.stackexchange.com/2.2/"; 9 | 10 | public static final String STACKOVERFLOW_API_KEY = "f)yov8mEGrYZa1dJDb2gpg(("; 11 | } 12 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/common/CustomApplication.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.common; 2 | 3 | import android.app.Application; 4 | 5 | import com.techyourchance.unittesting.common.dependencyinjection.CompositionRoot; 6 | 7 | public class CustomApplication extends Application { 8 | 9 | private CompositionRoot mCompositionRoot; 10 | 11 | @Override 12 | public void onCreate() { 13 | super.onCreate(); 14 | mCompositionRoot = new CompositionRoot(); 15 | } 16 | 17 | public CompositionRoot getCompositionRoot() { 18 | return mCompositionRoot; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/common/dependencyinjection/CompositionRoot.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.common.dependencyinjection; 2 | 3 | import com.techyourchance.unittesting.common.Constants; 4 | import com.techyourchance.unittesting.common.time.TimeProvider; 5 | import com.techyourchance.unittesting.networking.StackoverflowApi; 6 | import com.techyourchance.unittesting.networking.questions.FetchQuestionDetailsEndpoint; 7 | import com.techyourchance.unittesting.questions.FetchQuestionDetailsUseCase; 8 | 9 | import retrofit2.Retrofit; 10 | import retrofit2.converter.gson.GsonConverterFactory; 11 | 12 | public class CompositionRoot { 13 | 14 | private Retrofit mRetrofit; 15 | private FetchQuestionDetailsUseCase mFetchQuestionDetailsUseCase; 16 | 17 | private Retrofit getRetrofit() { 18 | if (mRetrofit == null) { 19 | mRetrofit = new Retrofit.Builder() 20 | .baseUrl(Constants.BASE_URL) 21 | .addConverterFactory(GsonConverterFactory.create()) 22 | .build(); 23 | } 24 | return mRetrofit; 25 | } 26 | 27 | public StackoverflowApi getStackoverflowApi() { 28 | return getRetrofit().create(StackoverflowApi.class); 29 | } 30 | 31 | public TimeProvider getTimeProvider() { 32 | return new TimeProvider(); 33 | } 34 | 35 | private FetchQuestionDetailsEndpoint getFetchQuestionDetailsEndpoint() { 36 | return new FetchQuestionDetailsEndpoint(getStackoverflowApi()); 37 | } 38 | 39 | public FetchQuestionDetailsUseCase getFetchQuestionDetailsUseCase() { 40 | if (mFetchQuestionDetailsUseCase == null) { 41 | mFetchQuestionDetailsUseCase = new FetchQuestionDetailsUseCase(getFetchQuestionDetailsEndpoint(), getTimeProvider()); 42 | } 43 | return mFetchQuestionDetailsUseCase; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/common/dependencyinjection/ControllerCompositionRoot.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.common.dependencyinjection; 2 | 3 | import android.content.Context; 4 | import androidx.fragment.app.FragmentActivity; 5 | import androidx.fragment.app.FragmentManager; 6 | import android.view.LayoutInflater; 7 | 8 | import com.techyourchance.unittesting.common.time.TimeProvider; 9 | import com.techyourchance.unittesting.networking.StackoverflowApi; 10 | import com.techyourchance.unittesting.networking.questions.FetchLastActiveQuestionsEndpoint; 11 | import com.techyourchance.unittesting.questions.FetchLastActiveQuestionsUseCase; 12 | import com.techyourchance.unittesting.questions.FetchQuestionDetailsUseCase; 13 | import com.techyourchance.unittesting.screens.common.ViewMvcFactory; 14 | import com.techyourchance.unittesting.screens.common.controllers.BackPressDispatcher; 15 | import com.techyourchance.unittesting.screens.common.fragmentframehelper.FragmentFrameHelper; 16 | import com.techyourchance.unittesting.screens.common.fragmentframehelper.FragmentFrameWrapper; 17 | import com.techyourchance.unittesting.screens.common.navdrawer.NavDrawerHelper; 18 | import com.techyourchance.unittesting.screens.common.screensnavigator.ScreensNavigator; 19 | import com.techyourchance.unittesting.screens.common.toastshelper.ToastsHelper; 20 | import com.techyourchance.unittesting.screens.questiondetails.QuestionDetailsController; 21 | import com.techyourchance.unittesting.screens.questionslist.QuestionsListController; 22 | 23 | public class ControllerCompositionRoot { 24 | 25 | private final CompositionRoot mCompositionRoot; 26 | private final FragmentActivity mActivity; 27 | 28 | public ControllerCompositionRoot(CompositionRoot compositionRoot, FragmentActivity activity) { 29 | mCompositionRoot = compositionRoot; 30 | mActivity = activity; 31 | } 32 | 33 | private FragmentActivity getActivity() { 34 | return mActivity; 35 | } 36 | 37 | private Context getContext() { 38 | return mActivity; 39 | } 40 | 41 | private FragmentManager getFragmentManager() { 42 | return getActivity().getSupportFragmentManager(); 43 | } 44 | 45 | private StackoverflowApi getStackoverflowApi() { 46 | return mCompositionRoot.getStackoverflowApi(); 47 | } 48 | 49 | private FetchLastActiveQuestionsEndpoint getFetchLastActiveQuestionsEndpoint() { 50 | return new FetchLastActiveQuestionsEndpoint(getStackoverflowApi()); 51 | } 52 | 53 | private LayoutInflater getLayoutInflater() { 54 | return LayoutInflater.from(getContext()); 55 | } 56 | 57 | public ViewMvcFactory getViewMvcFactory() { 58 | return new ViewMvcFactory(getLayoutInflater(), getNavDrawerHelper()); 59 | } 60 | 61 | private NavDrawerHelper getNavDrawerHelper() { 62 | return (NavDrawerHelper) getActivity(); 63 | } 64 | 65 | public FetchQuestionDetailsUseCase getFetchQuestionDetailsUseCase() { 66 | return mCompositionRoot.getFetchQuestionDetailsUseCase(); 67 | } 68 | 69 | public FetchLastActiveQuestionsUseCase getFetchLastActiveQuestionsUseCase() { 70 | return new FetchLastActiveQuestionsUseCase(getFetchLastActiveQuestionsEndpoint()); 71 | } 72 | 73 | public TimeProvider getTimeProvider() { 74 | return mCompositionRoot.getTimeProvider(); 75 | } 76 | 77 | public QuestionsListController getQuestionsListController() { 78 | return new QuestionsListController( 79 | getFetchLastActiveQuestionsUseCase(), 80 | getScreensNavigator(), 81 | getToastsHelper(), 82 | getTimeProvider()); 83 | } 84 | 85 | public ToastsHelper getToastsHelper() { 86 | return new ToastsHelper(getContext()); 87 | } 88 | 89 | public ScreensNavigator getScreensNavigator() { 90 | return new ScreensNavigator(getFragmentFrameHelper()); 91 | } 92 | 93 | private FragmentFrameHelper getFragmentFrameHelper() { 94 | return new FragmentFrameHelper(getActivity(), getFragmentFrameWrapper(), getFragmentManager()); 95 | } 96 | 97 | private FragmentFrameWrapper getFragmentFrameWrapper() { 98 | return (FragmentFrameWrapper) getActivity(); 99 | } 100 | 101 | public BackPressDispatcher getBackPressDispatcher() { 102 | return (BackPressDispatcher) getActivity(); 103 | } 104 | 105 | public QuestionDetailsController getQuestionDetailsController() { 106 | return new QuestionDetailsController(getFetchQuestionDetailsUseCase(), getScreensNavigator(), getToastsHelper()); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/common/time/TimeProvider.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.common.time; 2 | 3 | public class TimeProvider { 4 | 5 | public long getCurrentTimestamp() { 6 | return System.currentTimeMillis(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/description_exercise10.txt: -------------------------------------------------------------------------------- 1 | Description: 2 | Your goal is to extract application layer logic from QuestionDetailsFragment into standalone 3 | controller using Uncle Bob's TDD technique. 4 | I already created empty QuestionDetailsController class for you and set it up in 5 | QuestionDetailsFragment. 6 | 7 | The three rules of TDD are: 8 | 1) You are not allowed to write any production code unless it is to make a failing unit test pass 9 | 2) You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures 10 | 3) You are not allowed to write any more production code than is sufficient to pass the one failing unit test 11 | 12 | You should: 13 | 1) Create a new test class. 14 | 2) Think about the required test cases. In this case, it's totally alright to base test 15 | cases on QuestionDetailsFragment's implementation. 16 | 3) Test drive the implementation of QuestionDetailsController according to Uncle Bob's three rules of TDD. 17 | Always run all the tests to make sure that further changes don't break the existing functionality. 18 | 4) Refactor the production code. 19 | 5) Run the tests to make sure that all of them pass after the refactoring. 20 | 6) Refactor the tests code. 21 | 7) Run the tests to make sure that all of them pass after the refactoring. 22 | 8) Remove the implemented functionality from QuestionDetailsFragment. 23 | 9) Install the app and perform exploratory manual testing. 24 | 25 | * Note that you don't need to ensure line coverage and perform mutation testing because 26 | you're using Uncle Bob's technique. -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/description_exercise11.txt: -------------------------------------------------------------------------------- 1 | Description: 2 | Your goal is to add in-memory caching functionality with timeout to FetchQuestionDetailsUseCase 3 | using Uncle Bob's TDD technique. Set the timeout to 60 seconds. 4 | FetchQuestionDetailsUseCase is a "global" object, meaning that the same instance will be reused 5 | each time the user enters QuestionDetails screen. 6 | To make your life easier, I already injected TimeProvider into FetchQuestionDetailsUseCase's 7 | constructor. Therefore, you don't need to change any production code outside of this class in this 8 | exercise. 9 | 10 | The three rules of TDD are: 11 | 1) You are not allowed to write any production code unless it is to make a failing unit test pass 12 | 2) You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures 13 | 3) You are not allowed to write any more production code than is sufficient to pass the one failing unit test 14 | 15 | You should: 16 | 1) Think about all the required test cases for this functionality. 17 | 2) Test drive the implementation of the feature according to Uncle Bob's three rules of TDD. 18 | Always run all the tests to make sure that further changes don't break the existing functionality. 19 | 3) Refactor the production code. 20 | 4) Run the tests to make sure that all of them pass after the refactoring. 21 | 5) Refactor the tests code. 22 | 6) Run the tests to make sure that all of them pass after the refactoring. 23 | 7) Install the app and perform exploratory manual testing. 24 | 8) After you complete all the above steps, and before you review my solution, please read 25 | hint_exercise11.txt file. It contains a hint about specific test cases required for this 26 | functionality. If you already implemented them - great! If not, add them at this point. -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/description_exercise9.txt: -------------------------------------------------------------------------------- 1 | Description: 2 | Your goal is to unit test existing class FetchQuestionDetailsUseCase. 3 | 4 | You should: 5 | 1) Create a new test class. 6 | 2) Think about the required test cases without reviewing SUT's internal implementation. 7 | 3) Implement test cases. 8 | 4) Run tests and make sure that all of them pass. 9 | 5) Refactor the tests code. 10 | 6) Run the tests to make sure that all of them pass after the refactoring. 11 | 7) Perform mutation testing to make sure that SUT's functionality is actually covered by tests. 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/hint_exercise11.txt: -------------------------------------------------------------------------------- 1 | Since FetchQuestionDetailsUseCase is a "global" object, meaning that the same instance will be 2 | reused, it's reasonable to assume that over the lifetime of the application it will need to 3 | fetch information for different questions. 4 | To implement the caching functionality properly, you need to make sure that this use case can cache 5 | multiple questions in parallel, and that each cached question has its own timeout. 6 | Map data structure that stores question IDs as keys and some data as value can come in very handy. -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/networking/StackoverflowApi.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.networking; 2 | 3 | import com.techyourchance.unittesting.common.Constants; 4 | import com.techyourchance.unittesting.networking.questions.QuestionDetailsResponseSchema; 5 | import com.techyourchance.unittesting.networking.questions.QuestionsListResponseSchema; 6 | 7 | import retrofit2.Call; 8 | import retrofit2.http.GET; 9 | import retrofit2.http.Path; 10 | import retrofit2.http.Query; 11 | 12 | public interface StackoverflowApi { 13 | 14 | @GET("/questions?key=" + Constants.STACKOVERFLOW_API_KEY + "&sort=activity&order=desc&site=stackoverflow&filter=withbody") 15 | Call fetchLastActiveQuestions(@Query("pagesize") Integer pageSize); 16 | 17 | @GET("/questions/{questionId}?key=" + Constants.STACKOVERFLOW_API_KEY + "&site=stackoverflow&filter=withbody") 18 | Call fetchQuestionDetails(@Path("questionId") String questionId); 19 | } 20 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/networking/questions/FetchLastActiveQuestionsEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.networking.questions; 2 | 3 | import com.techyourchance.unittesting.common.Constants; 4 | import com.techyourchance.unittesting.networking.StackoverflowApi; 5 | 6 | import java.util.List; 7 | 8 | import retrofit2.Call; 9 | import retrofit2.Callback; 10 | import retrofit2.Response; 11 | 12 | public class FetchLastActiveQuestionsEndpoint { 13 | 14 | public interface Listener { 15 | void onQuestionsFetched(List questions); 16 | void onQuestionsFetchFailed(); 17 | } 18 | 19 | private final StackoverflowApi mStackoverflowApi; 20 | 21 | public FetchLastActiveQuestionsEndpoint(StackoverflowApi stackoverflowApi) { 22 | mStackoverflowApi = stackoverflowApi; 23 | } 24 | 25 | public void fetchLastActiveQuestions(final Listener listener) { 26 | mStackoverflowApi.fetchLastActiveQuestions(Constants.QUESTIONS_LIST_PAGE_SIZE) 27 | .enqueue(new Callback() { 28 | @Override 29 | public void onResponse(Call call, Response response) { 30 | if (response.isSuccessful()) { 31 | listener.onQuestionsFetched(response.body().getQuestions()); 32 | } else { 33 | listener.onQuestionsFetchFailed(); 34 | } 35 | } 36 | 37 | @Override 38 | public void onFailure(Call call, Throwable t) { 39 | listener.onQuestionsFetchFailed(); 40 | } 41 | } 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/networking/questions/FetchQuestionDetailsEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.networking.questions; 2 | 3 | import com.techyourchance.unittesting.networking.StackoverflowApi; 4 | 5 | import retrofit2.Call; 6 | import retrofit2.Callback; 7 | import retrofit2.Response; 8 | 9 | public class FetchQuestionDetailsEndpoint { 10 | 11 | public interface Listener { 12 | void onQuestionDetailsFetched(QuestionSchema question); 13 | void onQuestionDetailsFetchFailed(); 14 | } 15 | 16 | private final StackoverflowApi mStackoverflowApi; 17 | 18 | public FetchQuestionDetailsEndpoint(StackoverflowApi stackoverflowApi) { 19 | mStackoverflowApi = stackoverflowApi; 20 | } 21 | 22 | public void fetchQuestionDetails(String questionId, final Listener listener) { 23 | mStackoverflowApi.fetchQuestionDetails(questionId) 24 | .enqueue(new Callback() { 25 | @Override 26 | public void onResponse(Call call, Response response) { 27 | if (response.isSuccessful()) { 28 | listener.onQuestionDetailsFetched(response.body().getQuestion()); 29 | } else { 30 | listener.onQuestionDetailsFetchFailed(); 31 | } 32 | } 33 | 34 | @Override 35 | public void onFailure(Call call, Throwable t) { 36 | listener.onQuestionDetailsFetchFailed(); 37 | } 38 | } 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/networking/questions/QuestionDetailsResponseSchema.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.networking.questions; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | public class QuestionDetailsResponseSchema { 9 | 10 | @SerializedName("items") 11 | private final List mQuestions; 12 | 13 | public QuestionDetailsResponseSchema(QuestionSchema question) { 14 | mQuestions = Collections.singletonList(question); 15 | } 16 | 17 | public QuestionSchema getQuestion() { 18 | return mQuestions.get(0); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/networking/questions/QuestionSchema.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.networking.questions; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | public class QuestionSchema { 6 | 7 | @SerializedName("title") 8 | private final String mTitle; 9 | 10 | @SerializedName("question_id") 11 | private final String mId; 12 | 13 | @SerializedName("body") 14 | private final String mBody; 15 | 16 | public QuestionSchema(String title, String id, String body) { 17 | mTitle = title; 18 | mId = id; 19 | mBody = body; 20 | } 21 | 22 | public String getTitle() { 23 | return mTitle; 24 | } 25 | 26 | public String getId() { 27 | return mId; 28 | } 29 | 30 | public String getBody() { 31 | return mBody; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/networking/questions/QuestionsListResponseSchema.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.networking.questions; 2 | 3 | import com.google.gson.annotations.SerializedName; 4 | 5 | import java.util.List; 6 | 7 | public class QuestionsListResponseSchema { 8 | 9 | @SerializedName("items") 10 | private final List mQuestions; 11 | 12 | public QuestionsListResponseSchema(List questions) { 13 | mQuestions = questions; 14 | } 15 | 16 | public List getQuestions() { 17 | return mQuestions; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/questions/FetchLastActiveQuestionsUseCase.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.questions; 2 | 3 | import com.techyourchance.unittesting.common.BaseObservable; 4 | import com.techyourchance.unittesting.networking.questions.FetchLastActiveQuestionsEndpoint; 5 | import com.techyourchance.unittesting.networking.questions.QuestionSchema; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class FetchLastActiveQuestionsUseCase extends BaseObservable { 11 | 12 | public interface Listener { 13 | void onLastActiveQuestionsFetched(List questions); 14 | void onLastActiveQuestionsFetchFailed(); 15 | } 16 | 17 | private final FetchLastActiveQuestionsEndpoint mFetchLastActiveQuestionsEndpoint; 18 | 19 | public FetchLastActiveQuestionsUseCase(FetchLastActiveQuestionsEndpoint fetchLastActiveQuestionsEndpoint) { 20 | mFetchLastActiveQuestionsEndpoint = fetchLastActiveQuestionsEndpoint; 21 | } 22 | 23 | public void fetchLastActiveQuestionsAndNotify() { 24 | mFetchLastActiveQuestionsEndpoint.fetchLastActiveQuestions(new FetchLastActiveQuestionsEndpoint.Listener() { 25 | @Override 26 | public void onQuestionsFetched(List questions) { 27 | notifySuccess(questions); 28 | } 29 | 30 | @Override 31 | public void onQuestionsFetchFailed() { 32 | notifyFailure(); 33 | } 34 | }); 35 | } 36 | 37 | private void notifyFailure() { 38 | for (Listener listener : getListeners()) { 39 | listener.onLastActiveQuestionsFetchFailed(); 40 | } 41 | } 42 | 43 | private void notifySuccess(List questionSchemas) { 44 | List questions = new ArrayList<>(questionSchemas.size()); 45 | for (QuestionSchema questionSchema : questionSchemas) { 46 | questions.add(new Question(questionSchema.getId(), questionSchema.getTitle())); 47 | } 48 | for (Listener listener : getListeners()) { 49 | listener.onLastActiveQuestionsFetched(questions); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/questions/FetchQuestionDetailsUseCase.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.questions; 2 | 3 | import com.techyourchance.unittesting.common.BaseObservable; 4 | import com.techyourchance.unittesting.common.time.TimeProvider; 5 | import com.techyourchance.unittesting.networking.questions.FetchQuestionDetailsEndpoint; 6 | import com.techyourchance.unittesting.networking.questions.QuestionSchema; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | public class FetchQuestionDetailsUseCase extends BaseObservable { 12 | 13 | public interface Listener { 14 | void onQuestionDetailsFetched(QuestionDetails questionDetails); 15 | void onQuestionDetailsFetchFailed(); 16 | } 17 | 18 | private static final long CACHE_TIMEOUT_MS = 60000; 19 | 20 | private final FetchQuestionDetailsEndpoint mFetchQuestionDetailsEndpoint; 21 | private final TimeProvider mTimeProvider; 22 | 23 | private final Map mQuestionDetailsCache = new HashMap<>(); 24 | 25 | public FetchQuestionDetailsUseCase(FetchQuestionDetailsEndpoint fetchQuestionDetailsEndpoint, 26 | TimeProvider timeProvider) { 27 | mFetchQuestionDetailsEndpoint = fetchQuestionDetailsEndpoint; 28 | mTimeProvider = timeProvider; 29 | } 30 | 31 | public void fetchQuestionDetailsAndNotify(final String questionId) { 32 | if (serveQuestionDetailsFromCacheIfValid(questionId)) { 33 | return; 34 | } 35 | mFetchQuestionDetailsEndpoint.fetchQuestionDetails(questionId, new FetchQuestionDetailsEndpoint.Listener() { 36 | @Override 37 | public void onQuestionDetailsFetched(QuestionSchema question) { 38 | QuestionDetailsCacheEntry cacheEntry = new QuestionDetailsCacheEntry( 39 | schemaToQuestionDetails(question), 40 | mTimeProvider.getCurrentTimestamp() 41 | ); 42 | mQuestionDetailsCache.put(questionId, cacheEntry); 43 | notifySuccess(cacheEntry.mQuestionDetails); 44 | } 45 | 46 | @Override 47 | public void onQuestionDetailsFetchFailed() { 48 | notifyFailure(); 49 | } 50 | }); 51 | } 52 | 53 | private boolean serveQuestionDetailsFromCacheIfValid(String questionId) { 54 | final QuestionDetailsCacheEntry cachedQuestionDetailsEntry = mQuestionDetailsCache.get(questionId); 55 | if (cachedQuestionDetailsEntry != null 56 | && mTimeProvider.getCurrentTimestamp() < cachedQuestionDetailsEntry.mCachedTimestamp + CACHE_TIMEOUT_MS) { 57 | notifySuccess(cachedQuestionDetailsEntry.mQuestionDetails); 58 | return true; 59 | } else { 60 | return false; 61 | } 62 | } 63 | 64 | private QuestionDetails schemaToQuestionDetails(QuestionSchema questionSchema) { 65 | return new QuestionDetails( 66 | questionSchema.getId(), 67 | questionSchema.getTitle(), 68 | questionSchema.getBody() 69 | ); 70 | } 71 | 72 | private void notifyFailure() { 73 | for (Listener listener : getListeners()) { 74 | listener.onQuestionDetailsFetchFailed(); 75 | } 76 | } 77 | 78 | private void notifySuccess(QuestionDetails questionDetails) { 79 | for (Listener listener : getListeners()) { 80 | listener.onQuestionDetailsFetched(questionDetails); 81 | } 82 | } 83 | 84 | private static class QuestionDetailsCacheEntry { 85 | private final QuestionDetails mQuestionDetails; 86 | private final long mCachedTimestamp; 87 | 88 | private QuestionDetailsCacheEntry(QuestionDetails questionDetails, long cachedTimestamp) { 89 | mQuestionDetails = questionDetails; 90 | mCachedTimestamp = cachedTimestamp; 91 | } 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/questions/Question.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.questions; 2 | 3 | import java.util.Objects; 4 | 5 | public class Question { 6 | 7 | private final String mId; 8 | 9 | private final String mTitle; 10 | 11 | public Question(String id, String title) { 12 | mId = id; 13 | mTitle = title; 14 | } 15 | 16 | public String getId() { 17 | return mId; 18 | } 19 | 20 | public String getTitle() { 21 | return mTitle; 22 | } 23 | 24 | @Override 25 | public boolean equals(Object o) { 26 | if (this == o) return true; 27 | if (o == null || getClass() != o.getClass()) return false; 28 | Question question = (Question) o; 29 | return Objects.equals(mId, question.mId) && 30 | Objects.equals(mTitle, question.mTitle); 31 | } 32 | 33 | @Override 34 | public int hashCode() { 35 | return Objects.hash(mId, mTitle); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/questions/QuestionDetails.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.questions; 2 | 3 | import java.util.Objects; 4 | 5 | public class QuestionDetails { 6 | 7 | private final String mId; 8 | 9 | private final String mTitle; 10 | 11 | private final String mBody; 12 | 13 | public QuestionDetails(String id, String title, String body) { 14 | mId = id; 15 | mTitle = title; 16 | mBody = body; 17 | } 18 | 19 | public String getId() { 20 | return mId; 21 | } 22 | 23 | public String getTitle() { 24 | return mTitle; 25 | } 26 | 27 | public String getBody() { 28 | return mBody; 29 | } 30 | 31 | @Override 32 | public boolean equals(Object o) { 33 | if (this == o) return true; 34 | if (o == null || getClass() != o.getClass()) return false; 35 | QuestionDetails that = (QuestionDetails) o; 36 | return Objects.equals(mId, that.mId) && 37 | Objects.equals(mTitle, that.mTitle) && 38 | Objects.equals(mBody, that.mBody); 39 | } 40 | 41 | @Override 42 | public int hashCode() { 43 | return Objects.hash(mId, mTitle, mBody); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/common/ViewMvcFactory.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.common; 2 | 3 | import androidx.annotation.Nullable; 4 | import android.view.LayoutInflater; 5 | import android.view.ViewGroup; 6 | 7 | import com.techyourchance.unittesting.screens.common.navdrawer.NavDrawerHelper; 8 | import com.techyourchance.unittesting.screens.common.navdrawer.NavDrawerViewMvc; 9 | import com.techyourchance.unittesting.screens.common.navdrawer.NavDrawerViewMvcImpl; 10 | import com.techyourchance.unittesting.screens.common.toolbar.ToolbarViewMvc; 11 | import com.techyourchance.unittesting.screens.questiondetails.QuestionDetailsViewMvc; 12 | import com.techyourchance.unittesting.screens.questiondetails.QuestionDetailsViewMvcImpl; 13 | import com.techyourchance.unittesting.screens.questionslist.QuestionsListViewMvc; 14 | import com.techyourchance.unittesting.screens.questionslist.QuestionsListViewMvcImpl; 15 | import com.techyourchance.unittesting.screens.questionslist.questionslistitem.QuestionsListItemViewMvc; 16 | import com.techyourchance.unittesting.screens.questionslist.questionslistitem.QuestionsListItemViewMvcImpl; 17 | 18 | public class ViewMvcFactory { 19 | 20 | private final LayoutInflater mLayoutInflater; 21 | private final NavDrawerHelper mNavDrawerHelper; 22 | 23 | public ViewMvcFactory(LayoutInflater layoutInflater, NavDrawerHelper navDrawerHelper) { 24 | mLayoutInflater = layoutInflater; 25 | mNavDrawerHelper = navDrawerHelper; 26 | } 27 | 28 | public QuestionsListViewMvc getQuestionsListViewMvc(@Nullable ViewGroup parent) { 29 | return new QuestionsListViewMvcImpl(mLayoutInflater, parent, mNavDrawerHelper, this); 30 | } 31 | 32 | public QuestionsListItemViewMvc getQuestionsListItemViewMvc(@Nullable ViewGroup parent) { 33 | return new QuestionsListItemViewMvcImpl(mLayoutInflater, parent); 34 | } 35 | 36 | public QuestionDetailsViewMvc getQuestionDetailsViewMvc(@Nullable ViewGroup parent) { 37 | return new QuestionDetailsViewMvcImpl(mLayoutInflater, parent, this); 38 | } 39 | 40 | public ToolbarViewMvc getToolbarViewMvc(@Nullable ViewGroup parent) { 41 | return new ToolbarViewMvc(mLayoutInflater, parent); 42 | } 43 | 44 | public NavDrawerViewMvc getNavDrawerViewMvc(@Nullable ViewGroup parent) { 45 | return new NavDrawerViewMvcImpl(mLayoutInflater, parent); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/common/controllers/BackPressDispatcher.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.common.controllers; 2 | 3 | public interface BackPressDispatcher { 4 | void registerListener(BackPressedListener listener); 5 | void unregisterListener(BackPressedListener listener); 6 | } 7 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/common/controllers/BackPressedListener.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.common.controllers; 2 | 3 | public interface BackPressedListener { 4 | /** 5 | * 6 | * @return true if the listener handled the back press; false otherwise 7 | */ 8 | boolean onBackPressed(); 9 | } 10 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/common/controllers/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.common.controllers; 2 | 3 | import androidx.appcompat.app.AppCompatActivity; 4 | 5 | import com.techyourchance.unittesting.common.CustomApplication; 6 | import com.techyourchance.unittesting.common.dependencyinjection.ControllerCompositionRoot; 7 | 8 | public class BaseActivity extends AppCompatActivity { 9 | 10 | private ControllerCompositionRoot mControllerCompositionRoot; 11 | 12 | protected ControllerCompositionRoot getCompositionRoot() { 13 | if (mControllerCompositionRoot == null) { 14 | mControllerCompositionRoot = new ControllerCompositionRoot( 15 | ((CustomApplication) getApplication()).getCompositionRoot(), 16 | this 17 | ); 18 | } 19 | return mControllerCompositionRoot; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/common/controllers/BaseFragment.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.common.controllers; 2 | 3 | import androidx.fragment.app.Fragment; 4 | 5 | import com.techyourchance.unittesting.common.CustomApplication; 6 | import com.techyourchance.unittesting.common.dependencyinjection.ControllerCompositionRoot; 7 | 8 | public class BaseFragment extends Fragment { 9 | 10 | private ControllerCompositionRoot mControllerCompositionRoot; 11 | 12 | protected ControllerCompositionRoot getCompositionRoot() { 13 | if (mControllerCompositionRoot == null) { 14 | mControllerCompositionRoot = new ControllerCompositionRoot( 15 | ((CustomApplication) requireActivity().getApplication()).getCompositionRoot(), 16 | requireActivity() 17 | ); 18 | } 19 | return mControllerCompositionRoot; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/common/fragmentframehelper/FragmentFrameHelper.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.common.fragmentframehelper; 2 | 3 | import android.app.Activity; 4 | import androidx.fragment.app.Fragment; 5 | import androidx.fragment.app.FragmentManager; 6 | import androidx.fragment.app.FragmentTransaction; 7 | 8 | 9 | public class FragmentFrameHelper { 10 | 11 | private final Activity mActivity; 12 | private final FragmentFrameWrapper mFragmentFrameWrapper; 13 | private final FragmentManager mFragmentManager; 14 | 15 | public FragmentFrameHelper(Activity activity, FragmentFrameWrapper fragmentFrameWrapper, FragmentManager fragmentManager) { 16 | mActivity = activity; 17 | mFragmentFrameWrapper = fragmentFrameWrapper; 18 | mFragmentManager = fragmentManager; 19 | } 20 | 21 | public void replaceFragment(Fragment newFragment) { 22 | replaceFragment(newFragment, true, false); 23 | } 24 | 25 | public void replaceFragmentDontAddToBackstack(Fragment newFragment) { 26 | replaceFragment(newFragment, false, false); 27 | } 28 | 29 | public void replaceFragmentAndClearBackstack(Fragment newFragment) { 30 | replaceFragment(newFragment, false, true); 31 | } 32 | 33 | public void navigateUp() { 34 | 35 | // Some navigateUp calls can be "lost" if they happen after the state has been saved 36 | if (mFragmentManager.isStateSaved()) { 37 | return; 38 | } 39 | 40 | Fragment currentFragment = getCurrentFragment(); 41 | 42 | if (mFragmentManager.getBackStackEntryCount() > 0) { 43 | 44 | // In a normal world, just popping back stack would be sufficient, but since android 45 | // is not normal, a call to popBackStack can leave the popped fragment on screen. 46 | // Therefore, we start with manual removal of the current fragment. 47 | // Description of the issue can be found here: https://stackoverflow.com/q/45278497/2463035 48 | removeCurrentFragment(); 49 | 50 | if (mFragmentManager.popBackStackImmediate()) { 51 | return; // navigated "up" in fragments back-stack 52 | } 53 | } 54 | 55 | if (HierarchicalFragment.class.isInstance(currentFragment)) { 56 | Fragment parentFragment = 57 | ((HierarchicalFragment)currentFragment).getHierarchicalParentFragment(); 58 | if (parentFragment != null) { 59 | replaceFragment(parentFragment, false, true); 60 | return; // navigate "up" to hierarchical parent fragment 61 | } 62 | } 63 | 64 | if (mActivity.onNavigateUp()) { 65 | return; // navigated "up" to hierarchical parent activity 66 | } 67 | 68 | mActivity.onBackPressed(); // no "up" navigation targets - just treat UP as back press 69 | } 70 | 71 | private Fragment getCurrentFragment() { 72 | return mFragmentManager.findFragmentById(getFragmentFrameId()); 73 | } 74 | 75 | private void replaceFragment(Fragment newFragment, boolean addToBackStack, boolean clearBackStack) { 76 | if (clearBackStack) { 77 | if (mFragmentManager.isStateSaved()) { 78 | // If the state is saved we can't clear the back stack. Simply not doing this, but 79 | // still replacing fragment is a bad idea. Therefore we abort the entire operation. 80 | return; 81 | } 82 | // Remove all entries from back stack 83 | mFragmentManager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); 84 | } 85 | 86 | FragmentTransaction ft = mFragmentManager.beginTransaction(); 87 | 88 | if (addToBackStack) { 89 | ft.addToBackStack(null); 90 | } 91 | 92 | // Change to a new fragment 93 | ft.replace(getFragmentFrameId(), newFragment, null); 94 | 95 | if (mFragmentManager.isStateSaved()) { 96 | // We acknowledge the possibility of losing this transaction if the app undergoes 97 | // save&restore flow after it is committed. 98 | ft.commitAllowingStateLoss(); 99 | } else { 100 | ft.commit(); 101 | } 102 | } 103 | 104 | private void removeCurrentFragment() { 105 | FragmentTransaction ft = mFragmentManager.beginTransaction(); 106 | ft.remove(getCurrentFragment()); 107 | ft.commit(); 108 | 109 | // not sure it is needed; will keep it as a reminder to myself if there will be problems 110 | // mFragmentManager.executePendingTransactions(); 111 | } 112 | 113 | private int getFragmentFrameId() { 114 | return mFragmentFrameWrapper.getFragmentFrame().getId(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/common/fragmentframehelper/FragmentFrameWrapper.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.common.fragmentframehelper; 2 | 3 | import android.widget.FrameLayout; 4 | 5 | public interface FragmentFrameWrapper { 6 | 7 | FrameLayout getFragmentFrame(); 8 | } 9 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/common/fragmentframehelper/HierarchicalFragment.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.common.fragmentframehelper; 2 | 3 | import androidx.annotation.Nullable; 4 | import androidx.fragment.app.Fragment; 5 | 6 | public interface HierarchicalFragment { 7 | /** 8 | * In case of UP navigation when Fragments back-stack is empty, the Fragment returned by this 9 | * method will be navigated to. If this method returns null, then UP navigation will be 10 | * delegated to enclosing Activity. 11 | * @return hierarchical parent Fragment of this Fragment; null this Fragment has no hierarchical 12 | * parent 13 | */ 14 | @Nullable 15 | Fragment getHierarchicalParentFragment(); 16 | } 17 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/common/main/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.common.main; 2 | 3 | import android.os.Bundle; 4 | import androidx.annotation.Nullable; 5 | import android.widget.FrameLayout; 6 | 7 | import com.techyourchance.unittesting.screens.common.controllers.BackPressDispatcher; 8 | import com.techyourchance.unittesting.screens.common.controllers.BackPressedListener; 9 | import com.techyourchance.unittesting.screens.common.controllers.BaseActivity; 10 | import com.techyourchance.unittesting.screens.common.fragmentframehelper.FragmentFrameWrapper; 11 | import com.techyourchance.unittesting.screens.common.navdrawer.NavDrawerHelper; 12 | import com.techyourchance.unittesting.screens.common.navdrawer.NavDrawerViewMvc; 13 | import com.techyourchance.unittesting.screens.common.screensnavigator.ScreensNavigator; 14 | 15 | import java.util.HashSet; 16 | import java.util.Set; 17 | 18 | public class MainActivity extends BaseActivity implements 19 | BackPressDispatcher, 20 | FragmentFrameWrapper, 21 | NavDrawerViewMvc.Listener, 22 | NavDrawerHelper { 23 | 24 | private final Set mBackPressedListeners = new HashSet<>(); 25 | private ScreensNavigator mScreensNavigator; 26 | 27 | private NavDrawerViewMvc mViewMvc; 28 | 29 | @Override 30 | protected void onCreate(@Nullable Bundle savedInstanceState) { 31 | super.onCreate(savedInstanceState); 32 | mScreensNavigator = getCompositionRoot().getScreensNavigator(); 33 | mViewMvc = getCompositionRoot().getViewMvcFactory().getNavDrawerViewMvc(null); 34 | setContentView(mViewMvc.getRootView()); 35 | 36 | if (savedInstanceState == null) { 37 | mScreensNavigator.toQuestionsList(); 38 | } 39 | } 40 | 41 | @Override 42 | protected void onStart() { 43 | super.onStart(); 44 | mViewMvc.registerListener(this); 45 | } 46 | 47 | @Override 48 | protected void onStop() { 49 | super.onStop(); 50 | mViewMvc.unregisterListener(this); 51 | } 52 | 53 | @Override 54 | public void onQuestionsListClicked() { 55 | mScreensNavigator.toQuestionsList(); 56 | } 57 | 58 | @Override 59 | public void registerListener(BackPressedListener listener) { 60 | mBackPressedListeners.add(listener); 61 | } 62 | 63 | @Override 64 | public void unregisterListener(BackPressedListener listener) { 65 | mBackPressedListeners.remove(listener); 66 | } 67 | 68 | @Override 69 | public void onBackPressed() { 70 | boolean isBackPressConsumedByAnyListener = false; 71 | for (BackPressedListener listener : mBackPressedListeners) { 72 | if (listener.onBackPressed()) { 73 | isBackPressConsumedByAnyListener = true; 74 | } 75 | } 76 | if (!isBackPressConsumedByAnyListener) { 77 | super.onBackPressed(); 78 | } 79 | } 80 | 81 | @Override 82 | public FrameLayout getFragmentFrame() { 83 | return mViewMvc.getFragmentFrame(); 84 | } 85 | 86 | @Override 87 | public void openDrawer() { 88 | mViewMvc.openDrawer(); 89 | } 90 | 91 | @Override 92 | public void closeDrawer() { 93 | mViewMvc.closeDrawer(); 94 | } 95 | 96 | @Override 97 | public boolean isDrawerOpen() { 98 | return mViewMvc.isDrawerOpen(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/common/navdrawer/DrawerItems.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.common.navdrawer; 2 | 3 | public enum DrawerItems { 4 | QUESTIONS_LIST 5 | } 6 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/common/navdrawer/NavDrawerHelper.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.common.navdrawer; 2 | 3 | public interface NavDrawerHelper { 4 | 5 | void openDrawer(); 6 | void closeDrawer(); 7 | boolean isDrawerOpen(); 8 | } 9 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/common/navdrawer/NavDrawerViewMvc.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.common.navdrawer; 2 | 3 | import android.widget.FrameLayout; 4 | 5 | import com.techyourchance.unittesting.screens.common.views.ObservableViewMvc; 6 | 7 | public interface NavDrawerViewMvc extends ObservableViewMvc { 8 | 9 | interface Listener { 10 | 11 | void onQuestionsListClicked(); 12 | } 13 | 14 | FrameLayout getFragmentFrame(); 15 | 16 | boolean isDrawerOpen(); 17 | void openDrawer(); 18 | void closeDrawer(); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/common/navdrawer/NavDrawerViewMvcImpl.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.common.navdrawer; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | import com.google.android.material.navigation.NavigationView; 6 | import androidx.drawerlayout.widget.DrawerLayout; 7 | import android.view.Gravity; 8 | import android.view.LayoutInflater; 9 | import android.view.MenuItem; 10 | import android.view.ViewGroup; 11 | import android.widget.FrameLayout; 12 | 13 | import com.techyourchance.unittesting.R; 14 | import com.techyourchance.unittesting.screens.common.views.BaseObservableViewMvc; 15 | 16 | public class NavDrawerViewMvcImpl extends BaseObservableViewMvc 17 | implements NavDrawerViewMvc { 18 | 19 | private final DrawerLayout mDrawerLayout; 20 | private final FrameLayout mFrameLayout; 21 | private final NavigationView mNavigationView; 22 | 23 | public NavDrawerViewMvcImpl(LayoutInflater inflater, @Nullable ViewGroup parent) { 24 | setRootView(inflater.inflate(R.layout.layout_drawer, parent, false)); 25 | mDrawerLayout = findViewById(R.id.drawer_layout); 26 | mFrameLayout = findViewById(R.id.frame_content); 27 | mNavigationView = findViewById(R.id.nav_view); 28 | 29 | mNavigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() { 30 | @Override 31 | public boolean onNavigationItemSelected(@NonNull MenuItem item) { 32 | mDrawerLayout.closeDrawers(); 33 | if (item.getItemId() == R.id.drawer_menu_questions_list) { 34 | for (Listener listener : getListeners()) { 35 | listener.onQuestionsListClicked(); 36 | } 37 | } 38 | return false; 39 | } 40 | }); 41 | } 42 | 43 | @Override 44 | public void openDrawer() { 45 | mDrawerLayout.openDrawer(Gravity.START); 46 | } 47 | 48 | @Override 49 | public boolean isDrawerOpen() { 50 | return mDrawerLayout.isDrawerOpen(Gravity.START); 51 | } 52 | 53 | @Override 54 | public void closeDrawer() { 55 | mDrawerLayout.closeDrawers(); 56 | } 57 | 58 | @Override 59 | public FrameLayout getFragmentFrame() { 60 | return mFrameLayout; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/common/screensnavigator/ScreensNavigator.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.common.screensnavigator; 2 | 3 | import com.techyourchance.unittesting.screens.common.fragmentframehelper.FragmentFrameHelper; 4 | import com.techyourchance.unittesting.screens.questiondetails.QuestionDetailsFragment; 5 | import com.techyourchance.unittesting.screens.questionslist.QuestionsListFragment; 6 | 7 | public class ScreensNavigator { 8 | 9 | private FragmentFrameHelper mFragmentFrameHelper; 10 | 11 | public ScreensNavigator(FragmentFrameHelper fragmentFrameHelper) { 12 | mFragmentFrameHelper = fragmentFrameHelper; 13 | } 14 | 15 | public void toQuestionDetails(String questionId) { 16 | mFragmentFrameHelper.replaceFragment(QuestionDetailsFragment.newInstance(questionId)); 17 | } 18 | 19 | public void toQuestionsList() { 20 | mFragmentFrameHelper.replaceFragmentAndClearBackstack(QuestionsListFragment.newInstance()); 21 | } 22 | 23 | public void navigateUp() { 24 | mFragmentFrameHelper.navigateUp(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/common/toastshelper/ToastsHelper.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.common.toastshelper; 2 | 3 | import android.content.Context; 4 | import android.widget.Toast; 5 | 6 | import com.techyourchance.unittesting.R; 7 | 8 | public class ToastsHelper { 9 | 10 | private final Context mContext; 11 | 12 | public ToastsHelper(Context context) { 13 | mContext = context; 14 | } 15 | 16 | public void showUseCaseError() { 17 | Toast.makeText(mContext, R.string.error_network_call_failed, Toast.LENGTH_SHORT).show(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/common/toolbar/ToolbarViewMvc.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.common.toolbar; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.ImageButton; 7 | import android.widget.TextView; 8 | 9 | import com.techyourchance.unittesting.R; 10 | import com.techyourchance.unittesting.screens.common.views.BaseViewMvc; 11 | 12 | public class ToolbarViewMvc extends BaseViewMvc { 13 | 14 | public interface NavigateUpClickListener { 15 | void onNavigateUpClicked(); 16 | } 17 | 18 | public interface HamburgerClickListener { 19 | void onHamburgerClicked(); 20 | } 21 | 22 | private final TextView mTxtTitle; 23 | private final ImageButton mBtnBack; 24 | private final ImageButton mBtnHamburger; 25 | 26 | private NavigateUpClickListener mNavigateUpClickListener; 27 | private HamburgerClickListener mHamburgerClickListener; 28 | 29 | public ToolbarViewMvc(LayoutInflater inflater, ViewGroup parent) { 30 | setRootView(inflater.inflate(R.layout.layout_toolbar, parent, false)); 31 | mTxtTitle = findViewById(R.id.txt_toolbar_title); 32 | mBtnHamburger = findViewById(R.id.btn_hamburger); 33 | mBtnHamburger.setOnClickListener(new View.OnClickListener() { 34 | @Override 35 | public void onClick(View view) { 36 | mHamburgerClickListener.onHamburgerClicked(); 37 | } 38 | }); 39 | mBtnBack = findViewById(R.id.btn_back); 40 | mBtnBack.setOnClickListener(new View.OnClickListener() { 41 | @Override 42 | public void onClick(View view) { 43 | mNavigateUpClickListener.onNavigateUpClicked(); 44 | } 45 | }); 46 | } 47 | 48 | public void setTitle(String title) { 49 | mTxtTitle.setText(title); 50 | } 51 | 52 | public void enableHamburgerButtonAndListen(HamburgerClickListener hamburgerClickListener) { 53 | if (mNavigateUpClickListener != null) { 54 | throw new RuntimeException("hamburger and up shouldn't be shown together"); 55 | } 56 | mHamburgerClickListener = hamburgerClickListener; 57 | mBtnHamburger.setVisibility(View.VISIBLE); 58 | } 59 | 60 | public void enableUpButtonAndListen(NavigateUpClickListener navigateUpClickListener) { 61 | if (mHamburgerClickListener != null) { 62 | throw new RuntimeException("hamburger and up shouldn't be shown together"); 63 | } 64 | mNavigateUpClickListener = navigateUpClickListener; 65 | mBtnBack.setVisibility(View.VISIBLE); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/common/views/BaseObservableViewMvc.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.common.views; 2 | 3 | import java.util.Collections; 4 | import java.util.HashSet; 5 | import java.util.Set; 6 | 7 | public abstract class BaseObservableViewMvc extends BaseViewMvc 8 | implements ObservableViewMvc { 9 | 10 | private Set mListeners = new HashSet<>(); 11 | 12 | @Override 13 | public final void registerListener(ListenerType listener) { 14 | mListeners.add(listener); 15 | } 16 | 17 | @Override 18 | public final void unregisterListener(ListenerType listener) { 19 | mListeners.remove(listener); 20 | } 21 | 22 | protected final Set getListeners() { 23 | return Collections.unmodifiableSet(mListeners); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/common/views/BaseViewMvc.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.common.views; 2 | 3 | import android.content.Context; 4 | import androidx.annotation.StringRes; 5 | import android.view.View; 6 | 7 | public abstract class BaseViewMvc implements ViewMvc { 8 | 9 | private View mRootView; 10 | 11 | @Override 12 | public View getRootView() { 13 | return mRootView; 14 | } 15 | 16 | protected void setRootView(View rootView) { 17 | mRootView = rootView; 18 | } 19 | 20 | protected T findViewById(int id) { 21 | return getRootView().findViewById(id); 22 | } 23 | 24 | protected Context getContext() { 25 | return getRootView().getContext(); 26 | } 27 | 28 | protected String getString(@StringRes int id) { 29 | return getContext().getString(id); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/common/views/ObservableViewMvc.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.common.views; 2 | 3 | public interface ObservableViewMvc extends ViewMvc { 4 | 5 | void registerListener(ListenerType listener); 6 | 7 | void unregisterListener(ListenerType listener); 8 | } 9 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/common/views/ViewMvc.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.common.views; 2 | 3 | import android.view.View; 4 | 5 | public interface ViewMvc { 6 | View getRootView(); 7 | } 8 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/questiondetails/QuestionDetailsController.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.questiondetails; 2 | 3 | import com.techyourchance.unittesting.questions.FetchQuestionDetailsUseCase; 4 | import com.techyourchance.unittesting.questions.QuestionDetails; 5 | import com.techyourchance.unittesting.screens.common.screensnavigator.ScreensNavigator; 6 | import com.techyourchance.unittesting.screens.common.toastshelper.ToastsHelper; 7 | 8 | public class QuestionDetailsController implements QuestionDetailsViewMvc.Listener, FetchQuestionDetailsUseCase.Listener { 9 | 10 | private final FetchQuestionDetailsUseCase mFetchQuestionDetailsUseCase; 11 | private final ScreensNavigator mScreensNavigator; 12 | private final ToastsHelper mToastsHelper; 13 | 14 | private String mQuestionId; 15 | private QuestionDetailsViewMvc mViewMvc; 16 | 17 | public QuestionDetailsController(FetchQuestionDetailsUseCase fetchQuestionDetailsUseCase, 18 | ScreensNavigator screensNavigator, 19 | ToastsHelper toastsHelper) { 20 | mFetchQuestionDetailsUseCase = fetchQuestionDetailsUseCase; 21 | mScreensNavigator = screensNavigator; 22 | mToastsHelper = toastsHelper; 23 | } 24 | 25 | public void bindQuestionId(String questionId) { 26 | mQuestionId = questionId; 27 | } 28 | 29 | public void bindView(QuestionDetailsViewMvc viewMvc) { 30 | mViewMvc = viewMvc; 31 | } 32 | 33 | public void onStart() { 34 | mViewMvc.registerListener(this); 35 | mFetchQuestionDetailsUseCase.registerListener(this); 36 | 37 | mViewMvc.showProgressIndication(); 38 | mFetchQuestionDetailsUseCase.fetchQuestionDetailsAndNotify(mQuestionId); 39 | } 40 | 41 | public void onStop() { 42 | mViewMvc.unregisterListener(this); 43 | mFetchQuestionDetailsUseCase.unregisterListener(this); 44 | } 45 | 46 | @Override 47 | public void onQuestionDetailsFetched(QuestionDetails questionDetails) { 48 | mViewMvc.bindQuestion(questionDetails); 49 | mViewMvc.hideProgressIndication(); 50 | } 51 | 52 | @Override 53 | public void onQuestionDetailsFetchFailed() { 54 | mViewMvc.hideProgressIndication(); 55 | mToastsHelper.showUseCaseError(); 56 | } 57 | 58 | @Override 59 | public void onNavigateUpClicked() { 60 | mScreensNavigator.navigateUp(); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/questiondetails/QuestionDetailsFragment.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.questiondetails; 2 | 3 | import android.os.Bundle; 4 | import androidx.annotation.NonNull; 5 | import androidx.annotation.Nullable; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | 10 | import com.techyourchance.unittesting.screens.common.controllers.BaseFragment; 11 | 12 | public class QuestionDetailsFragment extends BaseFragment { 13 | 14 | private static final String ARG_QUESTION_ID = "ARG_QUESTION_ID"; 15 | 16 | public static QuestionDetailsFragment newInstance(String questionId) { 17 | Bundle args = new Bundle(); 18 | args.putString(ARG_QUESTION_ID, questionId); 19 | QuestionDetailsFragment fragment = new QuestionDetailsFragment(); 20 | fragment.setArguments(args); 21 | return fragment; 22 | } 23 | 24 | private QuestionDetailsController mQuestionDetailsController; 25 | 26 | @Override 27 | public void onCreate(@Nullable Bundle savedInstanceState) { 28 | super.onCreate(savedInstanceState); 29 | mQuestionDetailsController = getCompositionRoot().getQuestionDetailsController(); 30 | } 31 | 32 | @Nullable 33 | @Override 34 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 35 | QuestionDetailsViewMvc mViewMvc = getCompositionRoot().getViewMvcFactory().getQuestionDetailsViewMvc(container); 36 | 37 | mQuestionDetailsController.bindView(mViewMvc); 38 | mQuestionDetailsController.bindQuestionId(getArguments().getString(ARG_QUESTION_ID)); 39 | 40 | return mViewMvc.getRootView(); 41 | } 42 | 43 | @Override 44 | public void onStart() { 45 | super.onStart(); 46 | mQuestionDetailsController.onStart(); 47 | } 48 | 49 | @Override 50 | public void onStop() { 51 | super.onStop(); 52 | mQuestionDetailsController.onStop(); 53 | } 54 | 55 | private String getQuestionId() { 56 | return getArguments().getString(ARG_QUESTION_ID); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/questiondetails/QuestionDetailsViewMvc.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.questiondetails; 2 | 3 | import com.techyourchance.unittesting.questions.QuestionDetails; 4 | import com.techyourchance.unittesting.screens.common.views.ObservableViewMvc; 5 | 6 | public interface QuestionDetailsViewMvc extends ObservableViewMvc { 7 | 8 | public interface Listener { 9 | void onNavigateUpClicked(); 10 | } 11 | 12 | void bindQuestion(QuestionDetails question); 13 | 14 | void showProgressIndication(); 15 | 16 | void hideProgressIndication(); 17 | } 18 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/questiondetails/QuestionDetailsViewMvcImpl.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.questiondetails; 2 | 3 | import androidx.appcompat.widget.Toolbar; 4 | import android.text.Html; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.ProgressBar; 9 | import android.widget.TextView; 10 | 11 | import com.techyourchance.unittesting.R; 12 | import com.techyourchance.unittesting.questions.QuestionDetails; 13 | import com.techyourchance.unittesting.screens.common.ViewMvcFactory; 14 | import com.techyourchance.unittesting.screens.common.toolbar.ToolbarViewMvc; 15 | import com.techyourchance.unittesting.screens.common.views.BaseObservableViewMvc; 16 | 17 | 18 | public class QuestionDetailsViewMvcImpl extends BaseObservableViewMvc 19 | implements QuestionDetailsViewMvc { 20 | 21 | 22 | private final ToolbarViewMvc mToolbarViewMvc; 23 | private final Toolbar mToolbar; 24 | 25 | private final TextView mTxtQuestionTitle; 26 | private final TextView mTxtQuestionBody; 27 | private final ProgressBar mProgressBar; 28 | 29 | public QuestionDetailsViewMvcImpl(LayoutInflater inflater, ViewGroup parent, ViewMvcFactory viewMvcFactory) { 30 | 31 | setRootView(inflater.inflate(R.layout.layout_question_details, parent, false)); 32 | 33 | mTxtQuestionTitle = findViewById(R.id.txt_question_title); 34 | mTxtQuestionBody = findViewById(R.id.txt_question_body); 35 | mProgressBar = findViewById(R.id.progress); 36 | mToolbar = findViewById(R.id.toolbar); 37 | 38 | mToolbarViewMvc = viewMvcFactory.getToolbarViewMvc(mToolbar); 39 | 40 | initToolbar(); 41 | } 42 | 43 | private void initToolbar() { 44 | mToolbar.addView(mToolbarViewMvc.getRootView()); 45 | 46 | mToolbarViewMvc.setTitle(getString(R.string.question_details_screen_title)); 47 | 48 | mToolbarViewMvc.enableUpButtonAndListen(new ToolbarViewMvc.NavigateUpClickListener() { 49 | @Override 50 | public void onNavigateUpClicked() { 51 | for (Listener listener : getListeners()) { 52 | listener.onNavigateUpClicked(); 53 | } 54 | } 55 | }); 56 | } 57 | 58 | @Override 59 | public void bindQuestion(QuestionDetails question) { 60 | String questionTitle = question.getTitle(); 61 | String questionBody = question.getBody(); 62 | 63 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { 64 | mTxtQuestionTitle.setText(Html.fromHtml(questionTitle, Html.FROM_HTML_MODE_LEGACY)); 65 | mTxtQuestionBody.setText(Html.fromHtml(questionBody, Html.FROM_HTML_MODE_LEGACY)); 66 | } else { 67 | mTxtQuestionTitle.setText(Html.fromHtml(questionTitle)); 68 | mTxtQuestionBody.setText(Html.fromHtml(questionBody)); 69 | } 70 | } 71 | 72 | 73 | @Override 74 | public void showProgressIndication() { 75 | mProgressBar.setVisibility(View.VISIBLE); 76 | } 77 | 78 | @Override 79 | public void hideProgressIndication() { 80 | mProgressBar.setVisibility(View.GONE); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/questionslist/QuestionsListController.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.questionslist; 2 | 3 | import com.techyourchance.unittesting.common.time.TimeProvider; 4 | import com.techyourchance.unittesting.questions.FetchLastActiveQuestionsUseCase; 5 | import com.techyourchance.unittesting.questions.Question; 6 | import com.techyourchance.unittesting.screens.common.screensnavigator.ScreensNavigator; 7 | import com.techyourchance.unittesting.screens.common.toastshelper.ToastsHelper; 8 | 9 | import java.util.List; 10 | 11 | public class QuestionsListController implements 12 | QuestionsListViewMvc.Listener, 13 | FetchLastActiveQuestionsUseCase.Listener { 14 | 15 | private static final int CACHE_TIMEOUT_MS = 10000; 16 | 17 | private final FetchLastActiveQuestionsUseCase mFetchLastActiveQuestionsUseCase; 18 | private final ScreensNavigator mScreensNavigator; 19 | private final ToastsHelper mToastsHelper; 20 | private final TimeProvider mTimeProvider; 21 | 22 | private QuestionsListViewMvc mViewMvc; 23 | private List mQuestions; 24 | private long mLastCachedTimestamp; 25 | 26 | public QuestionsListController(FetchLastActiveQuestionsUseCase fetchLastActiveQuestionsUseCase, 27 | ScreensNavigator screensNavigator, 28 | ToastsHelper toastsHelper, 29 | TimeProvider timeProvider) { 30 | mFetchLastActiveQuestionsUseCase = fetchLastActiveQuestionsUseCase; 31 | mScreensNavigator = screensNavigator; 32 | mToastsHelper = toastsHelper; 33 | mTimeProvider = timeProvider; 34 | } 35 | 36 | public void bindView(QuestionsListViewMvc viewMvc) { 37 | mViewMvc = viewMvc; 38 | } 39 | 40 | public void onStart() { 41 | mViewMvc.registerListener(this); 42 | mFetchLastActiveQuestionsUseCase.registerListener(this); 43 | 44 | if (isCachedDataValid()) { 45 | mViewMvc.bindQuestions(mQuestions); 46 | } else { 47 | mViewMvc.showProgressIndication(); 48 | mFetchLastActiveQuestionsUseCase.fetchLastActiveQuestionsAndNotify(); 49 | } 50 | } 51 | 52 | private boolean isCachedDataValid() { 53 | return mQuestions != null 54 | && mTimeProvider.getCurrentTimestamp() < mLastCachedTimestamp + CACHE_TIMEOUT_MS; 55 | } 56 | 57 | public void onStop() { 58 | mViewMvc.unregisterListener(this); 59 | mFetchLastActiveQuestionsUseCase.unregisterListener(this); 60 | } 61 | 62 | @Override 63 | public void onQuestionClicked(Question question) { 64 | mScreensNavigator.toQuestionDetails(question.getId()); 65 | } 66 | 67 | @Override 68 | public void onLastActiveQuestionsFetched(List questions) { 69 | mQuestions = questions; 70 | mLastCachedTimestamp = mTimeProvider.getCurrentTimestamp(); 71 | mViewMvc.hideProgressIndication(); 72 | mViewMvc.bindQuestions(questions); 73 | } 74 | 75 | @Override 76 | public void onLastActiveQuestionsFetchFailed() { 77 | mViewMvc.hideProgressIndication(); 78 | mToastsHelper.showUseCaseError(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/questionslist/QuestionsListFragment.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.questionslist; 2 | 3 | import android.os.Bundle; 4 | import androidx.annotation.NonNull; 5 | import androidx.annotation.Nullable; 6 | import androidx.fragment.app.Fragment; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | 11 | import com.techyourchance.unittesting.screens.common.controllers.BaseFragment; 12 | 13 | public class QuestionsListFragment extends BaseFragment { 14 | 15 | public static Fragment newInstance() { 16 | return new QuestionsListFragment(); 17 | } 18 | 19 | private QuestionsListController mQuestionsListController; 20 | 21 | @Override 22 | public void onCreate(@Nullable Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | mQuestionsListController = getCompositionRoot().getQuestionsListController(); 25 | } 26 | 27 | @Nullable 28 | @Override 29 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 30 | QuestionsListViewMvc viewMvc = getCompositionRoot().getViewMvcFactory().getQuestionsListViewMvc(container); 31 | 32 | mQuestionsListController.bindView(viewMvc); 33 | 34 | return viewMvc.getRootView(); 35 | } 36 | 37 | @Override 38 | public void onStart() { 39 | super.onStart(); 40 | mQuestionsListController.onStart(); 41 | } 42 | 43 | @Override 44 | public void onStop() { 45 | super.onStop(); 46 | mQuestionsListController.onStop(); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/questionslist/QuestionsListViewMvc.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.questionslist; 2 | 3 | import com.techyourchance.unittesting.questions.Question; 4 | import com.techyourchance.unittesting.screens.common.views.ObservableViewMvc; 5 | 6 | import java.util.List; 7 | 8 | public interface QuestionsListViewMvc extends ObservableViewMvc { 9 | 10 | public interface Listener { 11 | void onQuestionClicked(Question question); 12 | } 13 | 14 | void bindQuestions(List questions); 15 | 16 | void showProgressIndication(); 17 | 18 | void hideProgressIndication(); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/questionslist/QuestionsListViewMvcImpl.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.questionslist; 2 | 3 | import androidx.annotation.Nullable; 4 | import androidx.recyclerview.widget.LinearLayoutManager; 5 | import androidx.recyclerview.widget.RecyclerView; 6 | import androidx.appcompat.widget.Toolbar; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.ProgressBar; 11 | 12 | import com.techyourchance.unittesting.R; 13 | import com.techyourchance.unittesting.questions.Question; 14 | import com.techyourchance.unittesting.screens.common.ViewMvcFactory; 15 | import com.techyourchance.unittesting.screens.common.navdrawer.NavDrawerHelper; 16 | import com.techyourchance.unittesting.screens.common.toolbar.ToolbarViewMvc; 17 | import com.techyourchance.unittesting.screens.common.views.BaseObservableViewMvc; 18 | 19 | import java.util.List; 20 | 21 | public class QuestionsListViewMvcImpl extends BaseObservableViewMvc 22 | implements QuestionsListViewMvc, QuestionsRecyclerAdapter.Listener { 23 | 24 | private final ToolbarViewMvc mToolbarViewMvc; 25 | private final NavDrawerHelper mNavDrawerHelper; 26 | 27 | private final Toolbar mToolbar; 28 | private final RecyclerView mRecyclerQuestions; 29 | private final QuestionsRecyclerAdapter mAdapter; 30 | private final ProgressBar mProgressBar; 31 | 32 | public QuestionsListViewMvcImpl(LayoutInflater inflater, 33 | @Nullable ViewGroup parent, 34 | NavDrawerHelper navDrawerHelper, 35 | ViewMvcFactory viewMvcFactory) { 36 | mNavDrawerHelper = navDrawerHelper; 37 | setRootView(inflater.inflate(R.layout.layout_questions_list, parent, false)); 38 | 39 | mRecyclerQuestions = findViewById(R.id.recycler_questions); 40 | mRecyclerQuestions.setLayoutManager(new LinearLayoutManager(getContext())); 41 | mAdapter = new QuestionsRecyclerAdapter(this, viewMvcFactory); 42 | mRecyclerQuestions.setAdapter(mAdapter); 43 | 44 | mProgressBar = findViewById(R.id.progress); 45 | 46 | mToolbar = findViewById(R.id.toolbar); 47 | mToolbarViewMvc = viewMvcFactory.getToolbarViewMvc(mToolbar); 48 | initToolbar(); 49 | } 50 | 51 | private void initToolbar() { 52 | mToolbarViewMvc.setTitle(getString(R.string.questions_list_screen_title)); 53 | mToolbar.addView(mToolbarViewMvc.getRootView()); 54 | mToolbarViewMvc.enableHamburgerButtonAndListen(new ToolbarViewMvc.HamburgerClickListener() { 55 | @Override 56 | public void onHamburgerClicked() { 57 | mNavDrawerHelper.openDrawer(); 58 | } 59 | }); 60 | } 61 | 62 | @Override 63 | public void onQuestionClicked(Question question) { 64 | for (Listener listener : getListeners()) { 65 | listener.onQuestionClicked(question); 66 | } 67 | } 68 | 69 | @Override 70 | public void bindQuestions(List questions) { 71 | mAdapter.bindQuestions(questions); 72 | } 73 | 74 | @Override 75 | public void showProgressIndication() { 76 | mProgressBar.setVisibility(View.VISIBLE); 77 | } 78 | 79 | @Override 80 | public void hideProgressIndication() { 81 | mProgressBar.setVisibility(View.GONE); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/questionslist/QuestionsRecyclerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.questionslist; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.recyclerview.widget.RecyclerView; 5 | import android.view.ViewGroup; 6 | 7 | import com.techyourchance.unittesting.questions.Question; 8 | import com.techyourchance.unittesting.screens.common.ViewMvcFactory; 9 | import com.techyourchance.unittesting.screens.questionslist.questionslistitem.QuestionsListItemViewMvc; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | public class QuestionsRecyclerAdapter extends RecyclerView.Adapter 15 | implements QuestionsListItemViewMvc.Listener { 16 | 17 | public interface Listener { 18 | void onQuestionClicked(Question question); 19 | } 20 | 21 | static class MyViewHolder extends RecyclerView.ViewHolder { 22 | 23 | private final QuestionsListItemViewMvc mViewMvc; 24 | 25 | public MyViewHolder(QuestionsListItemViewMvc viewMvc) { 26 | super(viewMvc.getRootView()); 27 | mViewMvc = viewMvc; 28 | } 29 | 30 | } 31 | 32 | private final Listener mListener; 33 | private final ViewMvcFactory mViewMvcFactory; 34 | 35 | private List mQuestions = new ArrayList<>(); 36 | 37 | public QuestionsRecyclerAdapter(Listener listener, ViewMvcFactory viewMvcFactory) { 38 | mListener = listener; 39 | mViewMvcFactory = viewMvcFactory; 40 | } 41 | 42 | public void bindQuestions(List questions) { 43 | mQuestions = new ArrayList<>(questions); 44 | notifyDataSetChanged(); 45 | } 46 | 47 | @NonNull 48 | @Override 49 | public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 50 | QuestionsListItemViewMvc viewMvc = mViewMvcFactory.getQuestionsListItemViewMvc(parent); 51 | viewMvc.registerListener(this); 52 | return new MyViewHolder(viewMvc); 53 | } 54 | 55 | @Override 56 | public void onBindViewHolder(@NonNull MyViewHolder holder, int position) { 57 | holder.mViewMvc.bindQuestion(mQuestions.get(position)); 58 | } 59 | 60 | @Override 61 | public int getItemCount() { 62 | return mQuestions.size(); 63 | } 64 | 65 | @Override 66 | public void onQuestionClicked(Question question) { 67 | mListener.onQuestionClicked(question); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/questionslist/questionslistitem/QuestionsListItemViewMvc.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.questionslist.questionslistitem; 2 | 3 | import com.techyourchance.unittesting.questions.Question; 4 | import com.techyourchance.unittesting.screens.common.views.ObservableViewMvc; 5 | 6 | public interface QuestionsListItemViewMvc extends ObservableViewMvc { 7 | 8 | public interface Listener { 9 | void onQuestionClicked(Question question); 10 | } 11 | 12 | void bindQuestion(Question question); 13 | } 14 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/java/com/techyourchance/unittesting/screens/questionslist/questionslistitem/QuestionsListItemViewMvcImpl.java: -------------------------------------------------------------------------------- 1 | package com.techyourchance.unittesting.screens.questionslist.questionslistitem; 2 | 3 | import androidx.annotation.Nullable; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.TextView; 8 | 9 | import com.techyourchance.unittesting.R; 10 | import com.techyourchance.unittesting.questions.Question; 11 | import com.techyourchance.unittesting.screens.common.views.BaseObservableViewMvc; 12 | 13 | public class QuestionsListItemViewMvcImpl extends BaseObservableViewMvc 14 | implements QuestionsListItemViewMvc { 15 | 16 | private final TextView mTxtTitle; 17 | 18 | private Question mQuestion; 19 | 20 | public QuestionsListItemViewMvcImpl(LayoutInflater inflater, @Nullable ViewGroup parent) { 21 | setRootView(inflater.inflate(R.layout.layout_question_list_item, parent, false)); 22 | 23 | mTxtTitle = findViewById(R.id.txt_title); 24 | getRootView().setOnClickListener(new View.OnClickListener() { 25 | @Override 26 | public void onClick(View view) { 27 | for (Listener listener : getListeners()) { 28 | listener.onQuestionClicked(mQuestion); 29 | } 30 | } 31 | }); 32 | } 33 | 34 | @Override 35 | public void bindQuestion(Question question) { 36 | mQuestion = question; 37 | mTxtTitle.setText(question.getTitle()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/drawable-hdpi/ic_arrow_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/drawable-hdpi/ic_arrow_back.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/drawable-hdpi/ic_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/drawable-hdpi/ic_menu.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/drawable-hdpi/ic_view_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/drawable-hdpi/ic_view_list.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/drawable-mdpi/ic_arrow_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/drawable-mdpi/ic_arrow_back.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/drawable-mdpi/ic_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/drawable-mdpi/ic_menu.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/drawable-mdpi/ic_view_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/drawable-mdpi/ic_view_list.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/drawable-xhdpi/ic_arrow_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/drawable-xhdpi/ic_arrow_back.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/drawable-xhdpi/ic_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/drawable-xhdpi/ic_menu.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/drawable-xhdpi/ic_view_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/drawable-xhdpi/ic_view_list.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/drawable-xxhdpi/ic_arrow_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/drawable-xxhdpi/ic_arrow_back.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/drawable-xxhdpi/ic_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/drawable-xxhdpi/ic_menu.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/drawable-xxhdpi/ic_view_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/drawable-xxhdpi/ic_view_list.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/drawable-xxxhdpi/ic_arrow_back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/drawable-xxxhdpi/ic_arrow_back.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/drawable-xxxhdpi/ic_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/drawable-xxxhdpi/ic_menu.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/drawable-xxxhdpi/ic_view_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/drawable-xxxhdpi/ic_view_list.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/layout/element_toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/layout/layout_content_frame.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/layout/layout_drawer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 15 | 16 | 23 | 24 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/layout/layout_question_details.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 13 | 14 | 20 | 21 | 24 | 25 | 32 | 33 | 40 | 41 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/layout/layout_question_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 20 | 21 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/layout/layout_questions_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | 14 | 15 | 21 | 22 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/layout/layout_toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 22 | 23 | 31 | 32 | 33 | 34 | 35 | 42 | 43 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/menu/menu_drawer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techyourchance/unit-testing-in-android-course/59f3c32b9e75573bb08dabafb10a726ab53fe1ae/tutorial_android_application/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | #FFFFFF 7 | #000000 8 | #00000000 9 | 10 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | UNIT TESTING 3 | network call failed 4 | LATEST QUESTIONS 5 | QUESTION DETAILS 6 | Questions list 7 | 8 | -------------------------------------------------------------------------------- /tutorial_android_application/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 |