diff --git a/.gitignore b/.gitignore index 39fb081..5d7f7d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.iml .gradle +/.gradle /local.properties /.idea/workspace.xml /.idea/libraries @@ -7,3 +8,4 @@ /build /captures .externalNativeBuild +/.idea/ diff --git a/app/build.gradle b/app/build.gradle index 9513af3..725d015 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,5 @@ apply plugin: 'com.android.application' -//Dagger -apply plugin: 'com.neenbedankt.android-apt' + //ButterKnife apply plugin: 'com.jakewharton.butterknife' //lamda @@ -59,10 +58,10 @@ dependencies { compile "com.android.support:design:${supportLibraryVersion}" //Dagger compile "com.google.dagger:dagger:${daggerVersion}" - apt "com.google.dagger:dagger-compiler:${daggerVersion}" + annotationProcessor "com.google.dagger:dagger-compiler:${daggerVersion}" //Butter knife compile "com.jakewharton:butterknife:${butterKnifeVersion}" - apt "com.jakewharton:butterknife-compiler:${butterKnifeVersion}" + annotationProcessor "com.jakewharton:butterknife-compiler:${butterKnifeVersion}" //event bus compile 'com.squareup:otto:1.3.8' //Logging @@ -81,9 +80,9 @@ dependencies { compile "com.hannesdorfmann.parcelableplease:annotation:${parcelablepleaseVersion}" compile "com.hannesdorfmann.parcelableplease:processor:${parcelablepleaseVersion}" //Android RX - compile 'io.reactivex:rxandroid:1.2.1' - compile 'io.reactivex:rxjava:1.1.6' - compile "com.squareup.retrofit2:adapter-rxjava:${retrofitVersion}" + compile 'io.reactivex.rxjava2:rxjava:2.0.7' + compile 'io.reactivex.rxjava2:rxandroid:2.0.1' + compile 'com.squareup.retrofit2:adapter-rxjava2:2.2.0' // espresso , junit for testing testCompile 'junit:junit:4.12' compile('com.android.support.test:rules:0.5') { @@ -115,4 +114,7 @@ dependencies { exclude group: "javax.inject", module: "javax.inject" exclude group: 'com.android.support', module: 'support-annotations' } + //Mockito + testCompile "org.mockito:mockito-core:${mockitoVersion}" + androidTestCompile "org.mockito:mockito-core:${mockitoVersion}" } \ No newline at end of file diff --git a/app/configuration.gradle b/app/configuration.gradle index 69c5b8c..8501ff4 100644 --- a/app/configuration.gradle +++ b/app/configuration.gradle @@ -9,12 +9,13 @@ ext { targetSdkVersion = 24 //Libraries - buildToolsVersion = '24.0.1' - supportLibraryVersion = '24.2.1' + buildToolsVersion = '25.0.2' + supportLibraryVersion = '25.1.1' daggerVersion = '2.0.2' parcelablepleaseVersion = '1.0.2' espressoVersion = '2.2.2' retrofitVersion = '2.2.0' okhttpVersion = '3.3.0' butterKnifeVersion = '8.5.1' + mockitoVersion = '2.7.1' } \ No newline at end of file diff --git a/app/src/main/java/com/task/data/DataRepository.java b/app/src/main/java/com/task/data/DataRepository.java index 30ce0ad..3f95d86 100644 --- a/app/src/main/java/com/task/data/DataRepository.java +++ b/app/src/main/java/com/task/data/DataRepository.java @@ -6,8 +6,9 @@ import com.task.data.remote.dto.NewsModel; import javax.inject.Inject; -import rx.Observable; -import rx.schedulers.Schedulers; +import io.reactivex.Observable; +import io.reactivex.schedulers.Schedulers; + /** * Created by AhmedEltaher on 5/12/2016 diff --git a/app/src/main/java/com/task/data/remote/ApiRepository.java b/app/src/main/java/com/task/data/remote/ApiRepository.java index 70e2338..b05f559 100644 --- a/app/src/main/java/com/task/data/remote/ApiRepository.java +++ b/app/src/main/java/com/task/data/remote/ApiRepository.java @@ -10,13 +10,18 @@ import com.task.data.remote.service.NewsService; import com.task.utils.Constants; import com.task.utils.L; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; + import java.io.IOException; import javax.inject.Inject; +import io.reactivex.Observable; +import io.reactivex.ObservableEmitter; +import io.reactivex.ObservableOnSubscribe; +import io.reactivex.Observer; import retrofit2.Call; -import rx.Observable; -import rx.Subscriber; import static com.task.data.remote.ServiceError.NETWORK_ERROR; import static com.task.utils.Constants.ERROR_UNDEFINED; @@ -37,19 +42,31 @@ public class ApiRepository { public Observable getNews() { - Observable newsObservable = Observable.create(new Observable.OnSubscribe() { - @Override - public void call(Subscriber subscriber) { - if (!isConnected(App.getContext())) { - Exception e = new NetworkErrorException(); - subscriber.onError(e); - } else { - NewsService newsService = serviceGenerator.createService(NewsService.class, Constants.BASE_URL); - ServiceResponse serviceResponse = processCall(newsService.fetchNews(), false); - NewsModel newsModel = (NewsModel) serviceResponse.getData(); - subscriber.onNext(newsModel); - subscriber.onCompleted(); - } +// Observable newsObservable = Observable.create(new Observable.OnSubscribe() { +// @Override +// public void call(Subscriber subscriber) { +// if (!isConnected(App.getContext())) { +// Exception e = new NetworkErrorException(); +// subscriber.onError(e); +// } else { +// NewsService newsService = serviceGenerator.createService(NewsService.class, Constants.BASE_URL); +// ServiceResponse serviceResponse = processCall(newsService.fetchNews(), false); +// NewsModel newsModel = (NewsModel) serviceResponse.getData(); +// subscriber.onNext(newsModel); +// subscriber.onCompleted(); +// } +// } +// }); + Observable newsObservable = Observable.create(newsModelObservableEmitter -> { + if (!isConnected(App.getContext())) { + Exception e = new NetworkErrorException(); + newsModelObservableEmitter.onError(e); + } else { + NewsService newsService = serviceGenerator.createService(NewsService.class, Constants.BASE_URL); + ServiceResponse serviceResponse = processCall(newsService.fetchNews(), false); + NewsModel newsModel = (NewsModel) serviceResponse.getData(); + newsModelObservableEmitter.onNext(newsModel); + newsModelObservableEmitter.onComplete(); } }); return newsObservable; diff --git a/app/src/main/java/com/task/data/remote/ServiceError.java b/app/src/main/java/com/task/data/remote/ServiceError.java index 5c3f6c2..146725d 100644 --- a/app/src/main/java/com/task/data/remote/ServiceError.java +++ b/app/src/main/java/com/task/data/remote/ServiceError.java @@ -10,6 +10,8 @@ public class ServiceError { private static final int GROUP_400 = 4; private static final int GROUP_500 = 5; private static final int VALUE_100 = 100; + public static final int SUCCESS_CODE = 200; + public static final int ERROR_CODE = 400; private String description; private int code; diff --git a/app/src/main/java/com/task/data/remote/ServiceGenerator.java b/app/src/main/java/com/task/data/remote/ServiceGenerator.java index 3c1b78f..7c9e4e5 100644 --- a/app/src/main/java/com/task/data/remote/ServiceGenerator.java +++ b/app/src/main/java/com/task/data/remote/ServiceGenerator.java @@ -13,7 +13,7 @@ import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.logging.HttpLoggingInterceptor; import retrofit2.Retrofit; -import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory; +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; import retrofit2.converter.gson.GsonConverterFactory; /** @@ -49,7 +49,7 @@ public class ServiceGenerator { retrofit = new Retrofit.Builder() .baseUrl(baseUrl).client(client) .addConverterFactory(GsonConverterFactory.create(gson)) - .addCallAdapterFactory(RxJavaCallAdapterFactory.create()).build(); + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()).build(); return retrofit.create(serviceClass); } diff --git a/app/src/main/java/com/task/di/MainModule.java b/app/src/main/java/com/task/di/MainModule.java index 2b8e1ae..10058ab 100644 --- a/app/src/main/java/com/task/di/MainModule.java +++ b/app/src/main/java/com/task/di/MainModule.java @@ -1,14 +1,15 @@ package com.task.di; -import com.task.data.local.LocalRepository; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.task.data.local.LocalRepository; import javax.inject.Singleton; import dagger.Module; import dagger.Provides; +import io.reactivex.disposables.CompositeDisposable; /** * Created by AhmedEltaher on 5/12/2016 @@ -28,4 +29,11 @@ public class MainModule { Gson gson = new GsonBuilder().create(); return gson; } + + @Provides + @Singleton + public CompositeDisposable provideCompositeSubscription() { + CompositeDisposable compositeDisposable = new CompositeDisposable(); + return compositeDisposable; + } } diff --git a/app/src/main/java/com/task/ui/component/news/HomePresenter.java b/app/src/main/java/com/task/ui/component/news/HomePresenter.java index ca60a5a..2bf98ea 100644 --- a/app/src/main/java/com/task/ui/component/news/HomePresenter.java +++ b/app/src/main/java/com/task/ui/component/news/HomePresenter.java @@ -1,6 +1,7 @@ package com.task.ui.component.news; import android.os.Bundle; +import android.support.annotation.VisibleForTesting; import com.task.data.remote.dto.NewsItem; import com.task.data.remote.dto.NewsModel; @@ -13,7 +14,7 @@ import java.util.List; import javax.inject.Inject; -import static android.text.TextUtils.isEmpty; +import static com.task.utils.ObjectUtil.isEmpty; import static com.task.utils.ObjectUtil.isNull; /** @@ -75,6 +76,11 @@ public class HomePresenter extends Presenter { getView().navigateToDetailsScreen(newsModel.getNewsItems().get(position)); }; + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public NewsModel getNewsModel() { + return newsModel; + } + private final Callback callback = new Callback() { @Override public void onSuccess(NewsModel newsModel) { diff --git a/app/src/main/java/com/task/usecase/NewsUseCase.java b/app/src/main/java/com/task/usecase/NewsUseCase.java index 33b15bf..d9e63c0 100644 --- a/app/src/main/java/com/task/usecase/NewsUseCase.java +++ b/app/src/main/java/com/task/usecase/NewsUseCase.java @@ -1,7 +1,5 @@ package com.task.usecase; -import android.support.annotation.NonNull; - import com.task.data.DataRepository; import com.task.data.remote.dto.NewsItem; import com.task.data.remote.dto.NewsModel; @@ -10,32 +8,28 @@ import java.util.List; import javax.inject.Inject; -import rx.android.schedulers.AndroidSchedulers; -import rx.schedulers.Schedulers; -import rx.subscriptions.CompositeSubscription; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; + /** * Created by AhmedEltaher on 5/12/2016 */ public class NewsUseCase { - DataRepository dataRepository; - @NonNull - private CompositeSubscription mSubscriptions; + private DataRepository dataRepository; + private CompositeDisposable mSubscriptions; @Inject - public NewsUseCase(DataRepository dataRepository) { + public NewsUseCase(DataRepository dataRepository,CompositeDisposable mSubscriptions) { this.dataRepository = dataRepository; - this.mSubscriptions = new CompositeSubscription(); + this.mSubscriptions = mSubscriptions; } public void getNews(final Callback callback) { - mSubscriptions.add(dataRepository.requestNews().subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) + mSubscriptions.add(dataRepository.requestNews().observeOn(AndroidSchedulers.mainThread()) .subscribe(newsModel -> callback.onSuccess(newsModel), - exception -> { - callback.onFail(); - })); + exception -> callback.onFail())); } public NewsItem searchByTitle(List news, String keyWord) { @@ -48,7 +42,7 @@ public class NewsUseCase { } public void unSubscribe() { - mSubscriptions.unsubscribe(); + mSubscriptions.clear(); } public interface Callback { diff --git a/app/src/test/java/com/task/ui/component/news/HomePresenterTest.java b/app/src/test/java/com/task/ui/component/news/HomePresenterTest.java new file mode 100644 index 0000000..1a7384e --- /dev/null +++ b/app/src/test/java/com/task/ui/component/news/HomePresenterTest.java @@ -0,0 +1,106 @@ + +package com.task.ui.component.news; + +import com.task.data.remote.dto.NewsItem; +import com.task.data.remote.dto.NewsModel; +import com.task.usecase.NewsUseCase; +import com.task.usecase.NewsUseCase.Callback; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +@RunWith(MockitoJUnitRunner.class) +public class HomePresenterTest { + + @Mock + private NewsUseCase newsUseCase; + @Mock + private NewsModel newsModelMock; + @Mock + private HomeView homeView; + @Mock + private Callback callback; + @Mock + private List newsItems; + @Mock + private NewsItem newsItem; + + private HomePresenter homePresenter; + private String newsTitle = "this is test"; + private NewsModel newsModel; + private TestModelsGenerator testModelsGenerator; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + testModelsGenerator = new TestModelsGenerator(); + newsModel = testModelsGenerator.generateNewsModel(newsTitle); + doAnswer(invocation -> { + ((Callback) invocation.getArguments()[0]).onSuccess(newsModel); + return null; + }).when(newsUseCase).getNews(any(Callback.class)); + homePresenter = new HomePresenter(newsUseCase); + homePresenter.setView(homeView); + } + + @Test + public void getNewsList() { + // Let's do a synchronous answer for the callback + doAnswer(invocation -> { + ((Callback) invocation.getArguments()[0]).onSuccess(newsModel); + return null; + }).when(newsUseCase).getNews(any(Callback.class)); + + homePresenter.getNews(); + verify(homeView, times(1)).setLoaderVisibility(true); + verify(homeView, times(2)).setNoDataVisibility(false); + verify(homeView, times(1)).setListVisibility(false); + verify(newsUseCase, times(1)).getNews(any(Callback.class)); + assertThat(homePresenter.getNewsModel(), is(equalTo(newsModel))); + } + + @Test + public void testSearchSuccess() { + when(newsUseCase.searchByTitle(newsModel.getNewsItems(), newsTitle)).thenReturn(newsItem); + homePresenter.getNews(); + homePresenter.onSearchClick(newsTitle); + verify(homeView, times(1)).navigateToDetailsScreen(any(NewsItem.class)); + } + + @Test + public void testSearchFailedWhileEmptyList() { + homePresenter.getNews(); + homePresenter.onSearchClick(newsTitle); + assertThat(newsModelMock.getNewsItems().size(), equalTo(0)); + verify(homeView, times(1)).showSearchError(); + } + + @Test + public void testSearchFailedWhenNothingMatches() { + when(newsUseCase.searchByTitle(any(), any())).thenReturn(null); + homePresenter.getNews(); + homePresenter.onSearchClick(newsTitle); + verify(homeView, times(1)).showSearchError(); + } + + @After + public void tearDown() throws Exception { + } +} \ No newline at end of file diff --git a/app/src/test/java/com/task/ui/component/news/TestModelsGenerator.java b/app/src/test/java/com/task/ui/component/news/TestModelsGenerator.java new file mode 100644 index 0000000..020bbd8 --- /dev/null +++ b/app/src/test/java/com/task/ui/component/news/TestModelsGenerator.java @@ -0,0 +1,51 @@ +package com.task.ui.component.news; + +import com.task.data.remote.ServiceResponse; +import com.task.data.remote.dto.NewsItem; +import com.task.data.remote.dto.NewsModel; + +import java.util.ArrayList; +import java.util.List; + +import static com.task.data.remote.ServiceError.ERROR_CODE; +import static com.task.data.remote.ServiceError.SUCCESS_CODE; + +/** + * Created by ahmedeltaher on 3/8/17. + */ + +public class TestModelsGenerator { + + public NewsModel generateNewsModel(String stup) { + NewsModel newsModel = new NewsModel(); + newsModel.setCopyright(stup); + newsModel.setLastUpdated(stup); + newsModel.setSection(stup); + newsModel.setStatus(stup); + newsModel.setNumResults(25L); + List newsItems = new ArrayList<>(); + for (int i = 0; i < 25; i++) { + newsItems.add(generateNewsItemModel(stup)); + } + newsModel.setNewsItems(newsItems); + return newsModel; + } + + public NewsItem generateNewsItemModel(String stup) { + NewsItem newsItem = new NewsItem(); + newsItem.setTitle(stup); + newsItem.setAbstract(stup); + newsItem.setUrl(stup); + return newsItem; + } + + public ServiceResponse getNewsSuccessfulModel() { + String stupString = "this is temp string"; + NewsModel newsModel = generateNewsModel(stupString); + return new ServiceResponse(SUCCESS_CODE, newsModel); + } + + public ServiceResponse getNewsErrorModel() { + return new ServiceResponse(ERROR_CODE, null); + } +} diff --git a/build.gradle b/build.gradle index 33431be..8cbc3de 100644 --- a/build.gradle +++ b/build.gradle @@ -6,9 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.3' - //DI - classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4' + classpath 'com.android.tools.build:gradle:2.3.0' //lamda classpath 'me.tatarka:gradle-retrolambda:3.2.5' classpath 'com.jakewharton.hugo:hugo-plugin:1.2.1' diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 13372ae..e69de29 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 04e285f..6b88c7a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Dec 28 10:00:20 PST 2015 +#Fri Mar 10 17:20:13 CET 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip