diff --git a/app/build.gradle b/app/build.gradle index d3900e8..f6d0806 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,9 +12,9 @@ buildscript { apply plugin: 'com.android.application' apply plugin: 'com.neenbedankt.android-apt' -apply plugin: 'me.tatarka.retrolambda' apply plugin: 'com.ofg.uptodate' apply from: '../dependencies.gradle' +apply plugin: 'me.tatarka.retrolambda' repositories { mavenCentral() @@ -49,8 +49,9 @@ android { buildConfigField 'String', 'GIT_SHA', "\"${gitSha}\"" buildConfigField 'long', 'GIT_TIMESTAMP', "${gitTimestamp}" + testApplicationId "${versions.packagename}.tests" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "ru.ltst.u2020mvp.U2020InstrumentationRunner" } signingConfigs { @@ -83,16 +84,6 @@ android { } } -// productFlavors { -// internal { -// applicationId "${packagename}.internal" -// -// } -// production { -// applicationId "${packagename}" -// } -// } - lintOptions { textReport true textOutput 'stdout' @@ -116,6 +107,9 @@ configurations { configurations.all { resolutionStrategy { + force "com.android.support:support-annotations:${SUPPORT_V7_VERSION}" + force "com.android.support:recyclerview-v7:${SUPPORT_V7_VERSION}" + force "com.android.support:support-v4:${SUPPORT_V4_VERSION}" force libraries.supportAnnotation } } @@ -170,14 +164,14 @@ dependencies { //-----tests----- // Espresso 2 Dependencies - androidTestCompile libraries.testingSupportLib androidTestCompile libraries.junit androidTestCompile libraries.testRunner androidTestCompile libraries.testRules androidTestCompile libraries.espressoCore -// androidTestCompile('com.android.support.test.espresso:espresso-contrib:2.0') { -// exclude module: 'support-annotations' -// } + androidTestApt libraries.daggerCompiler + + testCompile libraries.mockito + testCompile libraries.junit } // change apk name diff --git a/app/src/androidTest/java/ru/ltst/u2020mvp/TestU2020Application.java b/app/src/androidTest/java/ru/ltst/u2020mvp/TestU2020Application.java new file mode 100644 index 0000000..4edb084 --- /dev/null +++ b/app/src/androidTest/java/ru/ltst/u2020mvp/TestU2020Application.java @@ -0,0 +1,23 @@ +package ru.ltst.u2020mvp; + + +public class TestU2020Application extends U2020App { + private TestU2020Component component; + + @Override + public void buildComponentAndInject() { + component = DaggerTestU2020Component.builder() + .u2020AppModule(new U2020AppModule(this)) + .build(); + component.inject(this); + } + + @Override + public U2020Component component() { + return component; + } + + public TestU2020Component getTestComponent() { + return component; + } +} diff --git a/app/src/androidTest/java/ru/ltst/u2020mvp/TestU2020Component.java b/app/src/androidTest/java/ru/ltst/u2020mvp/TestU2020Component.java new file mode 100644 index 0000000..e1b97e1 --- /dev/null +++ b/app/src/androidTest/java/ru/ltst/u2020mvp/TestU2020Component.java @@ -0,0 +1,18 @@ +package ru.ltst.u2020mvp; + +import dagger.Component; +import ru.ltst.u2020mvp.data.DebugDataModule; +import ru.ltst.u2020mvp.ui.DebugUiModule; +import ru.ltst.u2020mvp.ui.ExternalIntentActivityTest; +import ru.ltst.u2020mvp.ui.screen.main.MainActivityTest; +import ru.ltst.u2020mvp.ui.screen.main.view.MainViewTest; + +@ApplicationScope +@Component(modules = {U2020AppModule.class, DebugUiModule.class, DebugDataModule.class, TestU2020Module.class}) +public interface TestU2020Component extends U2020Component { + void inject(MainActivityTest mainActivityTest); + + void inject(ExternalIntentActivityTest externalIntentActivityTest); + + void inject(MainViewTest mainViewTest); +} diff --git a/app/src/androidTest/java/ru/ltst/u2020mvp/TestU2020Module.java b/app/src/androidTest/java/ru/ltst/u2020mvp/TestU2020Module.java new file mode 100644 index 0000000..a1fed5f --- /dev/null +++ b/app/src/androidTest/java/ru/ltst/u2020mvp/TestU2020Module.java @@ -0,0 +1,20 @@ +package ru.ltst.u2020mvp; + +import dagger.Module; +import dagger.Provides; +import ru.ltst.u2020mvp.ApplicationScope; +import ru.ltst.u2020mvp.IsInstrumentationTest; + +@Module +public class TestU2020Module { + // Low-tech flag to force certain debug build behaviors when running in an instrumentation test. + // This value is used in the creation of singletons so it must be set before the graph is created. + static boolean instrumentationTest = true; + + @Provides + @ApplicationScope + @IsInstrumentationTest + boolean provideIsInstrumentationTest() { + return instrumentationTest; + } +} diff --git a/app/src/androidTest/java/ru/ltst/u2020mvp/U2020InstrumentationRunner.java b/app/src/androidTest/java/ru/ltst/u2020mvp/U2020InstrumentationRunner.java new file mode 100644 index 0000000..d6854f6 --- /dev/null +++ b/app/src/androidTest/java/ru/ltst/u2020mvp/U2020InstrumentationRunner.java @@ -0,0 +1,13 @@ +package ru.ltst.u2020mvp; + +import android.app.Application; +import android.content.Context; +import android.support.test.runner.AndroidJUnitRunner; + +public class U2020InstrumentationRunner extends AndroidJUnitRunner { + @Override + public Application newApplication(ClassLoader cl, String className, Context context) + throws InstantiationException, IllegalAccessException, ClassNotFoundException { + return super.newApplication(cl, TestU2020Application.class.getName(), context); + } +} diff --git a/app/src/androidTest/java/ru/ltst/u2020mvp/base/BaseTest.java b/app/src/androidTest/java/ru/ltst/u2020mvp/base/BaseTest.java new file mode 100644 index 0000000..21bedf5 --- /dev/null +++ b/app/src/androidTest/java/ru/ltst/u2020mvp/base/BaseTest.java @@ -0,0 +1,17 @@ +package ru.ltst.u2020mvp.base; + + +import android.support.test.InstrumentationRegistry; + +import ru.ltst.u2020mvp.TestU2020Application; +import ru.ltst.u2020mvp.TestU2020Component; + +public abstract class BaseTest { + protected TestU2020Application getApp() { + return (TestU2020Application) InstrumentationRegistry.getTargetContext().getApplicationContext(); + } + + protected TestU2020Component getTestComponent() { + return getApp().getTestComponent(); + } +} diff --git a/app/src/androidTest/java/ru/ltst/u2020mvp/base/ComponentFinderTest.java b/app/src/androidTest/java/ru/ltst/u2020mvp/base/ComponentFinderTest.java new file mode 100644 index 0000000..17cee3f --- /dev/null +++ b/app/src/androidTest/java/ru/ltst/u2020mvp/base/ComponentFinderTest.java @@ -0,0 +1,27 @@ +package ru.ltst.u2020mvp.base; + +import android.content.Context; +import android.support.test.espresso.matcher.ViewMatchers; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; + +import org.hamcrest.CoreMatchers; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import ru.ltst.u2020mvp.ui.screen.main.MainActivity; +import ru.ltst.u2020mvp.ui.screen.main.MainScope; + +@RunWith(AndroidJUnit4.class) +public class ComponentFinderTest { + @Rule + public ActivityTestRule activityTestRule = new ActivityTestRule<>(MainActivity.class); + + @Test + public void findActivityComponentTest() { + Context context = activityTestRule.getActivity(); + ViewMatchers.assertThat(ComponentFinder.findActivityComponent(context), + CoreMatchers.instanceOf(MainScope.MainComponent.class)); + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/ru/ltst/u2020mvp/base/mvp/BasePresenterTest2.java b/app/src/androidTest/java/ru/ltst/u2020mvp/base/mvp/BasePresenterTest2.java new file mode 100644 index 0000000..8835325 --- /dev/null +++ b/app/src/androidTest/java/ru/ltst/u2020mvp/base/mvp/BasePresenterTest2.java @@ -0,0 +1,88 @@ +package ru.ltst.u2020mvp.base.mvp; + + +import android.support.test.espresso.UiController; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; + +import ru.ltst.u2020mvp.R; +import ru.ltst.u2020mvp.ui.screen.main.MainActivity; +import ru.ltst.u2020mvp.ui.screen.main.MainPresenter; +import ru.ltst.u2020mvp.ui.screen.main.view.MainView; +import ru.ltst.u2020mvp.util.SimpleViewAction; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +@RunWith(AndroidJUnit4.class) +public class BasePresenterTest2 { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public ActivityTestRule activityTestRule = + new ActivityTestRule<>(MainActivity.class); + + MainPresenter mainPresenter; + + @Before + public void setup() { + mainPresenter = activityTestRule.getActivity().getComponent().presenter(); + } + + @Test + public void takeView() throws Exception { + assertEquals(true, mainPresenter.hasView()); + mainPresenter.getView(); //assert no errors + } + + @Test + public void dropView() throws Exception { + expectedException.expect(NullPointerException.class); + mainPresenter.dropView(null); + onView(withId(R.id.main_drawer_layout)) + .perform(new SimpleViewAction() { + @Override + protected void call(UiController uiController, MainView view) { + mainPresenter.dropView(view); + } + }); + assertEquals(false, mainPresenter.hasView()); + expectedException.expect(NullPointerException.class); + mainPresenter.getView(); + } + + @Test + public void hasView() throws Exception { + assertEquals(true, mainPresenter.hasView()); + onView(withId(R.id.main_drawer_layout)) + .perform(new SimpleViewAction() { + @Override + protected void call(UiController uiController, MainView view) { + mainPresenter.dropView(view); + } + }); + assertEquals(false, mainPresenter.hasView()); + } + + @Test + public void getView() throws Exception { + assertNotNull(mainPresenter.getView()); + onView(withId(R.id.main_drawer_layout)) + .perform(new SimpleViewAction() { + @Override + protected void call(UiController uiController, MainView view) { + mainPresenter.dropView(view); + } + }); + expectedException.expect(NullPointerException.class); + mainPresenter.getView(); + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/ru/ltst/u2020mvp/tests/util/ActivityRule.java b/app/src/androidTest/java/ru/ltst/u2020mvp/tests/util/ActivityRule.java deleted file mode 100644 index 3c7e406..0000000 --- a/app/src/androidTest/java/ru/ltst/u2020mvp/tests/util/ActivityRule.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (C) 2015 Jake Wharton - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package ru.ltst.u2020mvp.tests.util; - -import android.app.Activity; -import android.app.Instrumentation; -import android.content.Intent; -import android.support.test.InstrumentationRegistry; -import org.junit.Before; -import org.junit.Rule; -import org.junit.rules.TestRule; -import org.junit.runner.Description; -import org.junit.runners.model.Statement; - -/** - * A JUnit {@link Rule @Rule} which launches an activity when your test starts. Stop extending - * gross {@code ActivityInstrumentationBarfCase2}! - *

- * Usage: - *

{@code
- * @Rule
- * public final ActivityRule example =
- *     new ActivityRule<>(ExampleActivity.class);
- * }
- * - * This will automatically launch the activity for each test method. The instance will also be - * created sooner should you need to use it in a {@link Before @Before} method. - *

- * You can also customize the way in which the activity is launched by overriding - * {@link #getLaunchIntent(String, Class)} and customizing or replacing the {@link Intent}. - *

{@code
- * @Rule
- * public final ActivityRule example =
- *     new ActivityRule(ExampleActivity.class) {
- *       @Override
- *       protected Intent getLaunchIntent(String packageName, Class activityClass) {
- *         Intent intent = super.getLaunchIntent(packageName, activityClass);
- *         intent.putExtra("Hello", "World!");
- *         return intent;
- *       }
- *     };
- * }
- */ -public class ActivityRule implements TestRule { - private final Class activityClass; - - private T activity; - private Instrumentation instrumentation; - - public ActivityRule(Class activityClass) { - this.activityClass = activityClass; - } - - protected Intent getLaunchIntent(String targetPackage, Class activityClass) { - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.setClassName(targetPackage, activityClass.getName()); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - return intent; - } - - /** - * Get the running instance of the specified activity. This will launch it if it is not already - * running. - */ - public final T get() { - launchActivity(); - return activity; - } - - /** Get the {@link Instrumentation} instance for this test. */ - public final Instrumentation instrumentation() { - launchActivity(); - return instrumentation; - } - - @Override public final Statement apply(final Statement base, Description description) { - return new Statement() { - @Override public void evaluate() throws Throwable { - launchActivity(); - - base.evaluate(); - - if (!activity.isFinishing()) { - activity.finish(); - } - activity = null; // Eager reference kill in case someone leaked our reference. - } - }; - } - - private Instrumentation fetchInstrumentation() { - Instrumentation result = instrumentation; - return result != null ? result - : (instrumentation = InstrumentationRegistry.getInstrumentation()); - } - - @SuppressWarnings("unchecked") // Guarded by generics at the constructor. - private void launchActivity() { - if (activity != null) return; - - Instrumentation instrumentation = fetchInstrumentation(); - - String targetPackage = instrumentation.getTargetContext().getPackageName(); - Intent intent = getLaunchIntent(targetPackage, activityClass); - - activity = (T) instrumentation.startActivitySync(intent); - instrumentation.waitForIdleSync(); - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/ru/ltst/u2020mvp/tests/util/Constants.java b/app/src/androidTest/java/ru/ltst/u2020mvp/tests/util/Constants.java deleted file mode 100644 index e73378c..0000000 --- a/app/src/androidTest/java/ru/ltst/u2020mvp/tests/util/Constants.java +++ /dev/null @@ -1,8 +0,0 @@ -package ru.ltst.u2020mvp.tests.util; - -public final class Constants { - public static final int WAIT_DELAY = 5000; - - private Constants() { - } -} diff --git a/app/src/androidTest/java/ru/ltst/u2020mvp/ui/ExternalIntentActivityTest.java b/app/src/androidTest/java/ru/ltst/u2020mvp/ui/ExternalIntentActivityTest.java new file mode 100644 index 0000000..db180ba --- /dev/null +++ b/app/src/androidTest/java/ru/ltst/u2020mvp/ui/ExternalIntentActivityTest.java @@ -0,0 +1,46 @@ +package ru.ltst.u2020mvp.ui; + +import android.content.Intent; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.inject.Inject; + +import ru.ltst.u2020mvp.R; +import ru.ltst.u2020mvp.base.BaseTest; +import ru.ltst.u2020mvp.data.IntentFactory; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withText; + +@RunWith(AndroidJUnit4.class) +public class ExternalIntentActivityTest extends BaseTest { + @Inject + IntentFactory factory; + + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(ExternalIntentActivity.class, false, false); + private Intent launchIntent; + + @Before + public void setup() { + getTestComponent().inject(this); + launchIntent = factory.createUrlIntent("http://google.com"); + rule.launchActivity(launchIntent); + } + + @Test + public void generalTest() { + onView(withId(R.id.action)) + .check(matches(withText("android.intent.action.VIEW"))); + onView(withId(R.id.data)) + .check(matches(withText("http://google.com"))); + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/ru/ltst/u2020mvp/ui/screen/main/MainActivityTest.java b/app/src/androidTest/java/ru/ltst/u2020mvp/ui/screen/main/MainActivityTest.java new file mode 100644 index 0000000..021d0f2 --- /dev/null +++ b/app/src/androidTest/java/ru/ltst/u2020mvp/ui/screen/main/MainActivityTest.java @@ -0,0 +1,136 @@ +package ru.ltst.u2020mvp.ui.screen.main; + +import android.support.test.espresso.UiController; +import android.support.test.espresso.ViewAction; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.support.v4.widget.SwipeRefreshLayout; +import android.view.View; +import android.widget.ImageView; + +import com.f2prateek.rx.preferences.Preference; + +import org.hamcrest.Matcher; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.inject.Inject; + +import ru.ltst.u2020mvp.R; +import ru.ltst.u2020mvp.base.BaseTest; +import ru.ltst.u2020mvp.data.NetworkDelay; +import ru.ltst.u2020mvp.util.RecyclerViewMatcher; +import ru.ltst.u2020mvp.util.TimerTestRule; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.action.ViewActions.swipeDown; +import static android.support.test.espresso.action.ViewActions.swipeUp; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withParent; +import static android.support.test.espresso.matcher.ViewMatchers.withText; +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.not; + +@RunWith(AndroidJUnit4.class) +public class MainActivityTest extends BaseTest { + @Inject + @NetworkDelay + Preference networkDelay; + + @Rule + public ActivityTestRule activityTestRule = new ActivityTestRule<>(MainActivity.class); + @Rule + public TimerTestRule timerTestRule = new TimerTestRule(); + + @Before + public void setup() { + getTestComponent().inject(this); + networkDelay.set(500L); + } + + @Test + public void swipeTest() { + //check loading displayed + onView(withId(R.id.trending_loading)) + .check(matches(isCompletelyDisplayed())); + timerTestRule.scheduleTimeout(2000); + + //check content displayed + onView(withId(R.id.trending_swipe_refresh)) + .check(matches(isCompletelyDisplayed())); + onView(RecyclerViewMatcher.withRecyclerView(R.id.trending_list) + .atPosition(0)) + .check(matches(isCompletelyDisplayed())); + + //check swipe working + onView(withId(R.id.trending_list)) + .perform(swipeUp()); + onView(withId(R.id.trending_list)) + .perform(swipeDown()); + } + + @Test + public void hamburgerMenuTest() { + //click on navigation icon + onView(allOf(instanceOf(ImageView.class), withParent(withId(R.id.trending_toolbar)))) + .perform(click()); + onView(withId(R.id.main_navigation)) + .check(matches(isCompletelyDisplayed())); + } + + @Test + public void swipeToRefreshTest() { + timerTestRule.scheduleTimeout(1000); + onView(withId(R.id.trending_swipe_refresh)) + .perform(withCustomConstraints(swipeDown(), isDisplayingAtLeast(85))) + .check((view, noViewFoundException) -> ((SwipeRefreshLayout) view).isRefreshing()); + } + + @Test + public void spinnerTest() { + onView(withId(R.id.trending_timespan)) + .perform(click()); + onView(withText("today")) + .check(matches(isCompletelyDisplayed())) + .perform(click()); + onView(withId(R.id.trending_swipe_refresh)) + .check((view, noViewFoundException) -> ((SwipeRefreshLayout) view).isRefreshing()); + } + + @Test + public void navigationMenuTest() { + //shortcut to open navigation menu + hamburgerMenuTest(); + onView(withText("Search")) + .perform(click()); + onView(withId(R.id.main_navigation)) + .check(matches(not(isDisplayed()))); + } + + public static ViewAction withCustomConstraints(final ViewAction action, final Matcher constraints) { + return new ViewAction() { + @Override + public Matcher getConstraints() { + return constraints; + } + + @Override + public String getDescription() { + return action.getDescription(); + } + + @Override + public void perform(UiController uiController, View view) { + action.perform(uiController, view); + } + }; + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/ru/ltst/u2020mvp/ui/screen/main/view/MainViewTest.java b/app/src/androidTest/java/ru/ltst/u2020mvp/ui/screen/main/view/MainViewTest.java new file mode 100644 index 0000000..3d56b50 --- /dev/null +++ b/app/src/androidTest/java/ru/ltst/u2020mvp/ui/screen/main/view/MainViewTest.java @@ -0,0 +1,142 @@ +package ru.ltst.u2020mvp.ui.screen.main.view; + +import android.accounts.NetworkErrorException; +import android.support.test.espresso.UiController; +import android.support.test.rule.ActivityTestRule; +import android.support.v4.widget.SwipeRefreshLayout; +import android.view.View; +import android.widget.Spinner; + +import com.f2prateek.rx.preferences.Preference; + +import org.hamcrest.CustomTypeSafeMatcher; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import javax.inject.Inject; + +import ru.ltst.u2020mvp.base.BaseTest; +import ru.ltst.u2020mvp.data.NetworkDelay; +import ru.ltst.u2020mvp.ui.screen.main.MainActivity; +import ru.ltst.u2020mvp.util.SimpleViewAction; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static org.junit.Assert.assertEquals; +import static ru.ltst.u2020mvp.R.id.main_drawer_layout; +import static ru.ltst.u2020mvp.R.id.trending_empty; +import static ru.ltst.u2020mvp.R.id.trending_error; +import static ru.ltst.u2020mvp.R.id.trending_loading; +import static ru.ltst.u2020mvp.R.id.trending_network_error; +import static ru.ltst.u2020mvp.R.id.trending_swipe_refresh; +import static ru.ltst.u2020mvp.R.id.trending_timespan; + +public class MainViewTest extends BaseTest { + @Inject + @NetworkDelay + Preference networkDelay; + + @Rule + public ActivityTestRule activityTestRule = new ActivityTestRule<>(MainActivity.class); + + @Before + public void setup() { + getTestComponent().inject(this); + networkDelay.set(1000L); + } + + @Test + public void setTimespanPosition() throws Exception { + onView(withId(main_drawer_layout)) + .perform(new SimpleViewAction() { + @Override + protected void call(UiController uiController, MainView view) { + view.setTimespanPosition(1); + } + }); + onView(withId(trending_timespan)) + .check((view, noViewFoundException) -> + assertEquals(1, ((Spinner) view).getSelectedItemPosition())); + } + + @Test + public void showLoading() throws Exception { + onView(withId(main_drawer_layout)) + .perform(new SimpleViewAction() { + @Override + protected void call(UiController uiController, MainView view) { + view.showLoading(); + } + }); + onView(withId(trending_loading)) + .check(matches(isCompletelyDisplayed())); + } + + @Test + public void showLoading2() throws Exception { + onView(withId(main_drawer_layout)) + .perform(new SimpleViewAction() { + @Override + protected void call(UiController uiController, MainView view) { + view.showContent(); + view.showLoading(); + } + }); + } + + @Test + public void showContent() throws Exception { + onView(withId(main_drawer_layout)) + .perform(new SimpleViewAction() { + @Override + protected void call(UiController uiController, MainView view) { + view.showContent(); + } + }); + onView(withId(trending_swipe_refresh)) + .check(matches(isCompletelyDisplayed())); + } + + @Test + public void showEmpty() throws Exception { + onView(withId(main_drawer_layout)) + .perform(new SimpleViewAction() { + @Override + protected void call(UiController uiController, MainView view) { + view.showEmpty(); + } + }); + onView(withId(trending_empty)) + .check(matches(isCompletelyDisplayed())); + } + + @Test + public void showError() throws Exception { + onView(withId(main_drawer_layout)) + .perform(new SimpleViewAction() { + @Override + protected void call(UiController uiController, MainView view) { + view.showError(new NetworkErrorException()); + } + }); + onView(withId(trending_error)) + .check(matches(isCompletelyDisplayed())); + } + + @Test + public void onNetworkError() throws Exception { + onView(withId(main_drawer_layout)) + .perform(new SimpleViewAction() { + @Override + protected void call(UiController uiController, MainView view) { + view.onNetworkError(); + } + }); + onView(withId(trending_network_error)) + .check(matches(isCompletelyDisplayed())); + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/ru/ltst/u2020mvp/util/ObservableIdlingResource.java b/app/src/androidTest/java/ru/ltst/u2020mvp/util/ObservableIdlingResource.java new file mode 100644 index 0000000..7b4c45c --- /dev/null +++ b/app/src/androidTest/java/ru/ltst/u2020mvp/util/ObservableIdlingResource.java @@ -0,0 +1,54 @@ +package ru.ltst.u2020mvp.util; + + +import android.support.annotation.Nullable; +import android.support.test.espresso.IdlingResource; + +import rx.Observable; +import rx.functions.Action0; + +public final class ObservableIdlingResource implements IdlingResource { + + private final Observable observable; + @Nullable + private ResourceCallback callback; + private boolean isIdle; + + public ObservableIdlingResource(Observable observable) { + this.observable = observable; + } + + public Observable observe() { + isIdle = false; + return observable.doAfterTerminate(new IdlingAction()); + } + + @Override + public String getName() { + return this.getClass().getName() + hashCode(); + } + + @Override + public boolean isIdleNow() { + return isIdle; + } + + @Override + public void registerIdleTransitionCallback(ResourceCallback resourceCallback) { + callback = resourceCallback; + } + + private void notifyIdle() { + if (callback != null) { + callback.onTransitionToIdle(); + } + } + + private class IdlingAction implements Action0 { + @Override + public void call() { + isIdle = true; + notifyIdle(); + } + } +} diff --git a/app/src/androidTest/java/ru/ltst/u2020mvp/util/RecyclerViewMatcher.java b/app/src/androidTest/java/ru/ltst/u2020mvp/util/RecyclerViewMatcher.java new file mode 100644 index 0000000..55e4568 --- /dev/null +++ b/app/src/androidTest/java/ru/ltst/u2020mvp/util/RecyclerViewMatcher.java @@ -0,0 +1,73 @@ +package ru.ltst.u2020mvp.util; + + +import android.content.res.Resources; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +public class RecyclerViewMatcher { + private final int recyclerViewId; + + public RecyclerViewMatcher(int recyclerViewId) { + this.recyclerViewId = recyclerViewId; + } + + public static RecyclerViewMatcher withRecyclerView(int recyclerViewId) { + return new RecyclerViewMatcher(recyclerViewId); + } + + public Matcher atPosition(final int position) { + return atPositionOnView(position, -1); + } + + public Matcher atPositionOnView(final int position, final int targetViewId) { + + return new TypeSafeMatcher() { + Resources resources = null; + View childView; + + public void describeTo(Description description) { + String idDescription = Integer.toString(recyclerViewId); + if (this.resources != null) { + try { + idDescription = this.resources.getResourceName(recyclerViewId); + } catch (Resources.NotFoundException var4) { + idDescription = String.format("%s (resource name not found)", + new Object[] { Integer.valueOf + (recyclerViewId) }); + } + } + + description.appendText("with id: " + idDescription); + } + + public boolean matchesSafely(View view) { + + this.resources = view.getResources(); + + if (childView == null) { + RecyclerView recyclerView = + (RecyclerView) view.getRootView().findViewById(recyclerViewId); + if (recyclerView != null && recyclerView.getId() == recyclerViewId) { + childView = recyclerView.findViewHolderForAdapterPosition(position).itemView; + } + else { + return false; + } + } + + if (targetViewId == -1) { + return view == childView; + } else { + View targetView = childView.findViewById(targetViewId); + return view == targetView; + } + + } + }; + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/ru/ltst/u2020mvp/util/SimpleViewAction.java b/app/src/androidTest/java/ru/ltst/u2020mvp/util/SimpleViewAction.java new file mode 100644 index 0000000..d3f7721 --- /dev/null +++ b/app/src/androidTest/java/ru/ltst/u2020mvp/util/SimpleViewAction.java @@ -0,0 +1,28 @@ +package ru.ltst.u2020mvp.util; + +import android.support.test.espresso.UiController; +import android.support.test.espresso.ViewAction; +import android.view.View; + +import org.hamcrest.CoreMatchers; +import org.hamcrest.Matcher; + +public abstract class SimpleViewAction implements ViewAction { + @Override + public Matcher getConstraints() { + return CoreMatchers.instanceOf(View.class); + } + + @Override + public String getDescription() { + return "Simple view action"; + } + + @Override + @SuppressWarnings("unchecked") + public void perform(UiController uiController, View view) { + call(uiController, (V) view); + } + + protected abstract void call(UiController uiController, V view); +} diff --git a/app/src/androidTest/java/ru/ltst/u2020mvp/util/TestRxJavaSchedulersHook.java b/app/src/androidTest/java/ru/ltst/u2020mvp/util/TestRxJavaSchedulersHook.java new file mode 100644 index 0000000..a708714 --- /dev/null +++ b/app/src/androidTest/java/ru/ltst/u2020mvp/util/TestRxJavaSchedulersHook.java @@ -0,0 +1,22 @@ +package ru.ltst.u2020mvp.util; + +import rx.Scheduler; +import rx.plugins.RxJavaSchedulersHook; +import rx.schedulers.Schedulers; + +public class TestRxJavaSchedulersHook extends RxJavaSchedulersHook { + @Override + public Scheduler getComputationScheduler() { + return Schedulers.immediate(); + } + + @Override + public Scheduler getIOScheduler() { + return Schedulers.immediate(); + } + + @Override + public Scheduler getNewThreadScheduler() { + return Schedulers.immediate(); + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/ru/ltst/u2020mvp/util/TimerIdlingResource.java b/app/src/androidTest/java/ru/ltst/u2020mvp/util/TimerIdlingResource.java new file mode 100644 index 0000000..4539dcb --- /dev/null +++ b/app/src/androidTest/java/ru/ltst/u2020mvp/util/TimerIdlingResource.java @@ -0,0 +1,44 @@ +package ru.ltst.u2020mvp.util; + + +import android.os.Handler; +import android.os.Looper; +import android.support.test.espresso.IdlingResource; + +public class TimerIdlingResource implements IdlingResource { + private ResourceCallback resourceCallback; + private boolean idle = true; + private final Handler handler = new Handler(Looper.getMainLooper()); + + public TimerIdlingResource() { + + } + + public void scheduleTimeout(long timer) { + idle = false; + handler.postDelayed(() -> { + idle = true; + if (null != resourceCallback) { + resourceCallback.onTransitionToIdle(); + } + }, timer); + } + + @Override + public String getName() { + return "CloseKeyboardIdlingResource"; + } + + @Override + public boolean isIdleNow() { + return idle; + } + + @Override + public void registerIdleTransitionCallback(ResourceCallback callback) { + resourceCallback = callback; + if (idle) { + resourceCallback.onTransitionToIdle(); + } + } +} diff --git a/app/src/androidTest/java/ru/ltst/u2020mvp/util/TimerTestRule.java b/app/src/androidTest/java/ru/ltst/u2020mvp/util/TimerTestRule.java new file mode 100644 index 0000000..a5d3315 --- /dev/null +++ b/app/src/androidTest/java/ru/ltst/u2020mvp/util/TimerTestRule.java @@ -0,0 +1,32 @@ +package ru.ltst.u2020mvp.util; + + +import android.support.test.espresso.Espresso; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +public class TimerTestRule implements TestRule { + private final TimerIdlingResource resource; + + public TimerTestRule() { + resource = new TimerIdlingResource(); + } + + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + Espresso.registerIdlingResources(resource); + base.evaluate(); + Espresso.unregisterIdlingResources(resource); + } + }; + } + + public void scheduleTimeout(long millis) { + resource.scheduleTimeout(millis); + } +} diff --git a/app/src/androidTest/java/ru/ltst/u2020mvp/tests/util/ViewActions.java b/app/src/androidTest/java/ru/ltst/u2020mvp/util/ViewActions.java similarity index 96% rename from app/src/androidTest/java/ru/ltst/u2020mvp/tests/util/ViewActions.java rename to app/src/androidTest/java/ru/ltst/u2020mvp/util/ViewActions.java index 84ef912..b3e11d8 100644 --- a/app/src/androidTest/java/ru/ltst/u2020mvp/tests/util/ViewActions.java +++ b/app/src/androidTest/java/ru/ltst/u2020mvp/util/ViewActions.java @@ -1,4 +1,4 @@ -package ru.ltst.u2020mvp.tests.util; +package ru.ltst.u2020mvp.util; import android.support.test.espresso.PerformException; import android.support.test.espresso.UiController; @@ -17,6 +17,7 @@ import static android.support.test.espresso.matcher.ViewMatchers.isRoot; import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static org.hamcrest.CoreMatchers.any; import static org.hamcrest.CoreMatchers.anything; public class ViewActions { @@ -86,7 +87,7 @@ public static ViewAction waitAtLeast(final long millis) { return new ViewAction() { @Override public Matcher getConstraints() { - return anything(); + return any(View.class); } @Override @@ -122,7 +123,7 @@ public static ViewAction waitUntilIdle() { return new ViewAction() { @Override public Matcher getConstraints() { - return anything(); + return any(View.class); } @Override diff --git a/app/src/internalDebug/java/ru/ltst/u2020mvp/data/api/ServerDatabase.java b/app/src/internalDebug/java/ru/ltst/u2020mvp/data/api/ServerDatabase.java new file mode 100644 index 0000000..d12d1a8 --- /dev/null +++ b/app/src/internalDebug/java/ru/ltst/u2020mvp/data/api/ServerDatabase.java @@ -0,0 +1,84 @@ +package ru.ltst.u2020mvp.data.api; + +import android.content.SharedPreferences; +import android.support.annotation.NonNull; +import android.support.v4.util.ArrayMap; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +import javax.inject.Inject; + +import ru.ltst.u2020mvp.ApplicationScope; +import ru.ltst.u2020mvp.data.api.mock.MockGalleryResponse; +import ru.ltst.u2020mvp.data.api.mock.MockImage; +import ru.ltst.u2020mvp.data.api.mock.MockImageLoader; +import ru.ltst.u2020mvp.data.api.model.request.Section; +import ru.ltst.u2020mvp.data.api.model.response.Image; +import ru.ltst.u2020mvp.data.api.model.response.ImageResponse; +import ru.ltst.u2020mvp.util.EnumPreferences; +import timber.log.Timber; + +@ApplicationScope +public final class ServerDatabase { + private static final AtomicLong NEXT_ID = new AtomicLong(); + + public static long nextId() { + return NEXT_ID.getAndIncrement(); + } + + public static String nextStringId() { + return Long.toHexString(nextId()); + } + + private final MockImageLoader mockImageLoader; + private final SharedPreferences preferences; + + // TODO maybe id->image map and section->id multimap so we can re-use images? + private final Map> imagesBySection = new LinkedHashMap<>(); + private final ArrayMap imagesById = new ArrayMap<>(); + + private boolean initialized; + + @Inject + public ServerDatabase(MockImageLoader mockImageLoader, SharedPreferences preferences) { + this.mockImageLoader = mockImageLoader; + this.preferences = preferences; + } + + private synchronized void initializeMockData() { + if (initialized) return; + initialized = true; + Timber.d("Initializing mock data..."); + + List hotImages = new ArrayList<>(); + imagesBySection.put(Section.HOT, hotImages); + + final MockGalleryResponse enumValue = EnumPreferences.getEnumValue( + preferences, MockGalleryResponse.class, + MockGalleryResponse.class.getCanonicalName(), MockGalleryResponse.SUCCESS); + + if (enumValue.response.data != null) { + for (MockImage mockImage : enumValue.response.data) { + hotImages.add(mockImageLoader.newImage(mockImage)); + } + } + + for (Image hotImage : hotImages) { + imagesById.put(hotImage.id, hotImage); + } + } + + public List getImagesForSection(Section section) { + initializeMockData(); + return imagesBySection.get(section); + } + + public ImageResponse getImageForId(@NonNull String id) { + initializeMockData(); + return new ImageResponse(200, true, imagesById.get(id)); + } +} diff --git a/app/src/main/java/ru/ltst/u2020mvp/U2020App.java b/app/src/main/java/ru/ltst/u2020mvp/U2020App.java index 3eeb33b..233eb9b 100644 --- a/app/src/main/java/ru/ltst/u2020mvp/U2020App.java +++ b/app/src/main/java/ru/ltst/u2020mvp/U2020App.java @@ -23,7 +23,7 @@ public class U2020App extends Application { public void onCreate() { super.onCreate(); AndroidThreeTen.init(this); - LeakCanary.install(this); +// LeakCanary.install(this); if (BuildConfig.DEBUG) { Timber.plant(new DebugTree()); diff --git a/app/src/main/java/ru/ltst/u2020mvp/base/ActivityConnector.java b/app/src/main/java/ru/ltst/u2020mvp/base/ActivityConnector.java index 75c2875..ddbab8d 100644 --- a/app/src/main/java/ru/ltst/u2020mvp/base/ActivityConnector.java +++ b/app/src/main/java/ru/ltst/u2020mvp/base/ActivityConnector.java @@ -4,33 +4,35 @@ import android.support.annotation.Nullable; import java.lang.ref.WeakReference; +import java.util.Iterator; import java.util.LinkedList; +import java.util.WeakHashMap; public class ActivityConnector { - private LinkedList> linkedList = new LinkedList<>(); private WeakReference attachedObjectRef; + private WeakHashMap weakHashMap = new WeakHashMap<>(); public final void attach(@NonNull AttachedObject object) { final WeakReference weakReference = new WeakReference<>(object); - if (attachedObjectRef != null) { - linkedList.offer(weakReference); - return; - } + weakHashMap.put(object, new Object()); attachedObjectRef = weakReference; } - public final void detach() { - if (linkedList.isEmpty()) { - attachedObjectRef = null; + public final void detach(@NonNull AttachedObject object) { + if (weakHashMap.remove(object) == null) { + return; + } + + Iterator it = weakHashMap.keySet().iterator(); + if (it.hasNext()) { + attachedObjectRef = new WeakReference<>(it.next()); } else { - attachedObjectRef = linkedList.poll(); + attachedObjectRef = null; } } @Nullable protected AttachedObject getAttachedObject() { - if (!linkedList.isEmpty()) - return linkedList.getLast().get(); if (attachedObjectRef == null) return null; return attachedObjectRef.get(); diff --git a/app/src/main/java/ru/ltst/u2020mvp/base/mvp/BasePresenter.java b/app/src/main/java/ru/ltst/u2020mvp/base/mvp/BasePresenter.java index 09edf68..2d3165f 100644 --- a/app/src/main/java/ru/ltst/u2020mvp/base/mvp/BasePresenter.java +++ b/app/src/main/java/ru/ltst/u2020mvp/base/mvp/BasePresenter.java @@ -45,16 +45,15 @@ public final void dropView(V view) { onDestroy(); } + public boolean hasView() { + return view != null; + } + protected final V getView() { if (view == null) throw new NullPointerException("getView called when view is null. Ensure takeView(View view) is called first."); return view.get(); } - protected final boolean hasView(){ - return view != null; - } - - protected void onLoad(OnActivityResult onActivityResult) { if (onActivityResult != null) { onResult(onActivityResult); diff --git a/app/src/main/java/ru/ltst/u2020mvp/data/api/model/response/Image.java b/app/src/main/java/ru/ltst/u2020mvp/data/api/model/response/Image.java new file mode 100644 index 0000000..02a1354 --- /dev/null +++ b/app/src/main/java/ru/ltst/u2020mvp/data/api/model/response/Image.java @@ -0,0 +1,103 @@ +package ru.ltst.u2020mvp.data.api.model.response; + +public final class Image { + public final String id; + + public final String link; + public final String title; + public final String description; + + public final int width; + public final int height; + public final long datetime; + public final int views; + public final boolean is_album; + + public Image(String id, String link, String title, String description, + int width, int height, long datetime, int views, boolean is_album) { + this.id = id; + this.link = link; + this.title = title; + this.description = description; + this.width = width; + this.height = height; + this.datetime = datetime; + this.views = views; + this.is_album = is_album; + } + + public Image(Builder builder) { + this.id = builder.id; + this.link = builder.link; + this.title = builder.title; + this.description = builder.description; + this.width = builder.width; + this.height = builder.height; + this.datetime = builder.datetime; + this.views = builder.views; + this.is_album = builder.is_album; + } + + public static class Builder { + private String id; + + private String link; + private String title; + private String description; + + private int width; + private int height; + private long datetime; + private int views; + private boolean is_album; + + public Builder setId(String id) { + this.id = id; + return this; + } + + public Builder setLink(String link) { + this.link = link; + return this; + } + + public Builder setTitle(String title) { + this.title = title; + return this; + } + + public Builder setDescription(String description) { + this.description = description; + return this; + } + + public Builder setWidth(int width) { + this.width = width; + return this; + } + + public Builder setHeight(int height) { + this.height = height; + return this; + } + + public Builder setDatetime(long datetime) { + this.datetime = datetime; + return this; + } + + public Builder setViews(int views) { + this.views = views; + return this; + } + + public Builder setIsAlbum(boolean is_album) { + this.is_album = is_album; + return this; + } + + public Image build() { + return new Image(this); + } + } +} diff --git a/app/src/main/java/ru/ltst/u2020mvp/ui/UiModule.java b/app/src/main/java/ru/ltst/u2020mvp/ui/UiModule.java index 5418c76..d2a9e54 100644 --- a/app/src/main/java/ru/ltst/u2020mvp/ui/UiModule.java +++ b/app/src/main/java/ru/ltst/u2020mvp/ui/UiModule.java @@ -30,7 +30,7 @@ public void onActivityStarted(Activity activity) { @Override public void onActivityStopped(Activity activity) { - screenSwitcher.detach(); + screenSwitcher.detach(activity); } }; } diff --git a/app/src/main/java/ru/ltst/u2020mvp/ui/screen/main/MainActivity.java b/app/src/main/java/ru/ltst/u2020mvp/ui/screen/main/MainActivity.java index 5c9979d..8ff7372 100644 --- a/app/src/main/java/ru/ltst/u2020mvp/ui/screen/main/MainActivity.java +++ b/app/src/main/java/ru/ltst/u2020mvp/ui/screen/main/MainActivity.java @@ -54,7 +54,7 @@ protected void onStart() { @Override protected void onStop() { - activityScreenSwitcher.detach(); + activityScreenSwitcher.detach(this); super.onStop(); } diff --git a/app/src/main/java/ru/ltst/u2020mvp/ui/screen/main/MainScope.java b/app/src/main/java/ru/ltst/u2020mvp/ui/screen/main/MainScope.java index b645123..84ad26c 100644 --- a/app/src/main/java/ru/ltst/u2020mvp/ui/screen/main/MainScope.java +++ b/app/src/main/java/ru/ltst/u2020mvp/ui/screen/main/MainScope.java @@ -20,6 +20,7 @@ @Component(dependencies = {U2020Component.class}, modules = {MainActivityModule.class}) interface MainComponent { void inject(MainActivity mainActivity); + MainPresenter presenter(); } @Module diff --git a/app/src/main/java/ru/ltst/u2020mvp/util/RxTransformations.java b/app/src/main/java/ru/ltst/u2020mvp/util/RxTransformations.java new file mode 100644 index 0000000..dc11e8e --- /dev/null +++ b/app/src/main/java/ru/ltst/u2020mvp/util/RxTransformations.java @@ -0,0 +1,20 @@ +package ru.ltst.u2020mvp.util; + + +import java.util.concurrent.TimeUnit; + +import rx.Observable; +import rx.android.schedulers.AndroidSchedulers; +import rx.schedulers.Schedulers; + +public class RxTransformations { + public static Observable.Transformer applySchedulers() { + return observable -> observable.subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + + public static Observable.Transformer applyDelay() { + return observable -> Observable.timer(1000, TimeUnit.MILLISECONDS) + .flatMap(aLong -> observable); + } +} diff --git a/app/src/main/java/ru/ltst/u2020mvp/util/Strings.java b/app/src/main/java/ru/ltst/u2020mvp/util/Strings.java index f15133b..254277e 100644 --- a/app/src/main/java/ru/ltst/u2020mvp/util/Strings.java +++ b/app/src/main/java/ru/ltst/u2020mvp/util/Strings.java @@ -18,6 +18,8 @@ public static String valueOrDefault(String string, String defaultString) { } public static String truncateAt(String string, int length) { + if (isBlank(string) || length < 0) + return string; return string.length() > length ? string.substring(0, length) : string; } } diff --git a/app/src/test/java/ru/ltst/u2020mvp/data/InstantAdapterTest.java b/app/src/test/java/ru/ltst/u2020mvp/data/InstantAdapterTest.java new file mode 100644 index 0000000..9f13e6f --- /dev/null +++ b/app/src/test/java/ru/ltst/u2020mvp/data/InstantAdapterTest.java @@ -0,0 +1,25 @@ +package ru.ltst.u2020mvp.data; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; +import org.threeten.bp.Instant; + +import static org.junit.Assert.assertEquals; + +@RunWith(MockitoJUnitRunner.class) +public class InstantAdapterTest { + @Test + public void toJson() throws Exception { + Instant input = Instant.now(); + String expected = input.toString(); + assertEquals(expected, new InstantAdapter().toJson(input)); + } + + @Test + public void fromJson() throws Exception { + Instant expected = Instant.now(); + String input = expected.toString(); + assertEquals(expected, new InstantAdapter().fromJson(input)); + } +} \ No newline at end of file diff --git a/app/src/test/java/ru/ltst/u2020mvp/testutils/TestRxAndroidSchedulersHook.java b/app/src/test/java/ru/ltst/u2020mvp/testutils/TestRxAndroidSchedulersHook.java new file mode 100644 index 0000000..e237a9a --- /dev/null +++ b/app/src/test/java/ru/ltst/u2020mvp/testutils/TestRxAndroidSchedulersHook.java @@ -0,0 +1,12 @@ +package ru.ltst.u2020mvp.testutils; + +import rx.Scheduler; +import rx.android.plugins.RxAndroidSchedulersHook; +import rx.schedulers.Schedulers; + +public class TestRxAndroidSchedulersHook extends RxAndroidSchedulersHook { + @Override + public Scheduler getMainThreadScheduler() { + return Schedulers.immediate(); + } +} \ No newline at end of file diff --git a/app/src/test/java/ru/ltst/u2020mvp/testutils/TestRxJavaSchedulersHook.java b/app/src/test/java/ru/ltst/u2020mvp/testutils/TestRxJavaSchedulersHook.java new file mode 100644 index 0000000..33c6492 --- /dev/null +++ b/app/src/test/java/ru/ltst/u2020mvp/testutils/TestRxJavaSchedulersHook.java @@ -0,0 +1,22 @@ +package ru.ltst.u2020mvp.testutils; + +import rx.Scheduler; +import rx.plugins.RxJavaSchedulersHook; +import rx.schedulers.Schedulers; + +public class TestRxJavaSchedulersHook extends RxJavaSchedulersHook { + @Override + public Scheduler getComputationScheduler() { + return Schedulers.immediate(); + } + + @Override + public Scheduler getIOScheduler() { + return Schedulers.immediate(); + } + + @Override + public Scheduler getNewThreadScheduler() { + return Schedulers.immediate(); + } +} \ No newline at end of file diff --git a/app/src/test/java/ru/ltst/u2020mvp/util/EnumPreferencesTest.java b/app/src/test/java/ru/ltst/u2020mvp/util/EnumPreferencesTest.java new file mode 100644 index 0000000..5a7dabf --- /dev/null +++ b/app/src/test/java/ru/ltst/u2020mvp/util/EnumPreferencesTest.java @@ -0,0 +1,77 @@ +package ru.ltst.u2020mvp.util; + +import android.annotation.SuppressLint; +import android.content.SharedPreferences; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class EnumPreferencesTest { + @Mock + SharedPreferences sharedPreferences; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void getEnumValue() throws Exception { + when(sharedPreferences.getString("key1", null)) + .thenReturn("ITEM_TWO"); + when(sharedPreferences.getString("key2", null)) + .thenReturn("ITEM_THREE"); + + + TestEnumeration defaultVal = TestEnumeration.ITEM_ONE; + assertEquals(TestEnumeration.ITEM_TWO, EnumPreferences.getEnumValue(sharedPreferences, + TestEnumeration.class, "key1", defaultVal)); + assertEquals(TestEnumeration.ITEM_THREE, EnumPreferences.getEnumValue(sharedPreferences, + TestEnumeration.class, "key2", defaultVal)); + when(sharedPreferences.getString("key3", defaultVal.name())) + .thenReturn(defaultVal.name()); + assertEquals(defaultVal, EnumPreferences.getEnumValue(sharedPreferences, + TestEnumeration.class, "key3", defaultVal)); + + expectedException.expect(NullPointerException.class); + TestEnumeration enumeration = EnumPreferences.getEnumValue(sharedPreferences, + null, "key2", defaultVal); + enumeration = EnumPreferences.getEnumValue(null, null, "key2", defaultVal); + + when(sharedPreferences.getString("key4", null)) + .thenReturn(null); + assertNull(EnumPreferences.getEnumValue(sharedPreferences, TestEnumeration.class, + "key4", null)); + } + + @SuppressLint("CommitPrefEdits") + @Test + public void saveEnumValue() throws Exception { + when(sharedPreferences.edit()) + .thenReturn(mock(SharedPreferences.Editor.class)); + when(sharedPreferences.edit().putString(anyString(), eq(TestEnumeration.ITEM_ONE.name()))) + .thenReturn(mock(SharedPreferences.Editor.class)); + EnumPreferences.saveEnumValue(sharedPreferences, "key", TestEnumeration.ITEM_ONE); + expectedException.expect(NullPointerException.class); + EnumPreferences.saveEnumValue(null, "key", TestEnumeration.ITEM_ONE); + EnumPreferences.saveEnumValue(sharedPreferences, null, TestEnumeration.ITEM_ONE); + EnumPreferences.saveEnumValue(sharedPreferences, "key", null); + } + + private enum TestEnumeration { + ITEM_ONE, + ITEM_TWO, + ITEM_THREE + } + +} \ No newline at end of file diff --git a/app/src/test/java/ru/ltst/u2020mvp/util/PreconditionsTest.java b/app/src/test/java/ru/ltst/u2020mvp/util/PreconditionsTest.java new file mode 100644 index 0000000..1d6636f --- /dev/null +++ b/app/src/test/java/ru/ltst/u2020mvp/util/PreconditionsTest.java @@ -0,0 +1,31 @@ +package ru.ltst.u2020mvp.util; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class PreconditionsTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void checkNotNull() throws Exception { + Integer ref = 1; + Preconditions.checkNotNull(ref); + expectedException.expect(NullPointerException.class); + Preconditions.checkNotNull(null); + } + + @Test + public void checkNotNull1() throws Exception { + Integer ref = 1; + Preconditions.checkNotNull(ref); + expectedException.expectMessage("message"); + Preconditions.checkNotNull(null, "message"); + } + +} \ No newline at end of file diff --git a/app/src/test/java/ru/ltst/u2020mvp/util/StringsTest.java b/app/src/test/java/ru/ltst/u2020mvp/util/StringsTest.java new file mode 100644 index 0000000..51a5cb0 --- /dev/null +++ b/app/src/test/java/ru/ltst/u2020mvp/util/StringsTest.java @@ -0,0 +1,36 @@ +package ru.ltst.u2020mvp.util; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class StringsTest { + @Test + public void isBlank() throws Exception { + assertFalse(Strings.isBlank("123")); + assertTrue(Strings.isBlank(" ")); + assertTrue(Strings.isBlank("")); + assertTrue(Strings.isBlank(null)); + } + + @Test + public void valueOrDefault() throws Exception { + String defaultVal = "default"; + assertEquals(defaultVal, Strings.valueOrDefault(" ", defaultVal)); + assertEquals(defaultVal, Strings.valueOrDefault("", defaultVal)); + assertNotEquals(defaultVal, Strings.valueOrDefault(",", defaultVal)); + assertEquals(defaultVal, Strings.valueOrDefault(null, defaultVal)); + } + + @Test + public void truncateAt() throws Exception { + assertEquals("", Strings.truncateAt("abc", 0)); + assertEquals("abc", Strings.truncateAt("abcdef", 3)); + assertEquals("abc", Strings.truncateAt("abc", 6)); + assertEquals("", Strings.truncateAt("", 4)); + assertNull(Strings.truncateAt(null, 2)); + assertEquals("abc", Strings.truncateAt("abc", -1)); + assertNull(Strings.truncateAt(null, -1)); + } + +} \ No newline at end of file diff --git a/dependencies.gradle b/dependencies.gradle index 68310bf..f2e939f 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -16,14 +16,14 @@ ext.versions = [ versionBuild : 0, // bump for dogfood builds, public betas, etc. minSdk : 17, - targetSdk : 24, - compileSdk : 24, + targetSdk : 23, + compileSdk : 23, buildTools : '24.0.2', androidGradlePlugin : '2.1.3', aptGradlePlugin : '1.8', uptodate : '1.6.0', - retrolambdaGradlePlugin : '3.3.0-beta4', + retrolambdaGradlePlugin : '3.2.3', lombokGradlePlugin : '0.2.3.a2', supportLibrary : '24.2.0', @@ -56,6 +56,7 @@ ext.versions = [ hamcrest : '1.4-atlassian-1', truth : '0.28', testingSupportLib : '0.1', + robolectric : '3.1.2' ] // Gradle Plugin Dependencies @@ -108,5 +109,7 @@ ext.libraries = [ espressoCore : "com.android.support.test.espresso:espresso-core:$versions.espresso", junit : "junit:junit:$versions.junit", testRunner : "com.android.support.test:runner:$versions.testRunner", - testRules : "com.android.support.test:rules:$versions.testRules" + testRules : "com.android.support.test:rules:$versions.testRules", + mockito : "org.mockito:mockito-core:$versions.mockito", + robolectric : "org.robolectric:robolectric:$versions.robolectric" ] diff --git a/gradle.properties b/gradle.properties index 11ec7db..a268437 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,5 +26,5 @@ MIN_SDK_VERSION=15 TARGET_SDK_VERSION=23 #Library versions -SUPPORT_V4_VERSION=24.1.1 -SUPPORT_V7_VERSION=24.1.1 +SUPPORT_V4_VERSION=24.2.0 +SUPPORT_V7_VERSION=24.2.0 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a4d59b2..e2ef527 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -#Sat Sep 10 18:24:49 OMST 2016 +#Wed Aug 31 19:09:15 OMST 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME