Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
This project implements AndroidClean architecture(http://fernandocejas.com/2014/09/03/architecting-android-the-clean-way/).
The objective is the separation of concers and facilitating testing. The task is to fetch movie events in Finnkino and display them in the app.

There are three modules:

1.domain: A Java module without Android dependencies.
-Application business rules
-All use cases(interactors)
-JUnit plus mockito for unit tests

2.data: An Android module from where all the data is retrieved. Finnkino Api is chosen in this module(http://www.finnkino.fi/XML)
-Deliver data needed by app
-JUnit plus mockito for unit tests

3.app: An android module that represents the presentation layer.
-Presentation Layer
-Espresso for functional testing.
Note: NowShowingInTheatresFragment is implemented in MVP fashion

Libraries used in the project:
Dagger2 - Facilitate dependency injections and testing.
Rxjava - Faciliitate async operations and apply reactive programming
Picasso - Image download and display, there are some alternatives such as Glide and Fresco. Picasso is the easiest one to use.
Retrofit - HTTP client
LeakCanary - Memory leak detection
Retrolambda - Support Lambda Expressions
Note: please install jdk 8 before building

Mosby - Android MVP library. The intention of using this library is to facilicate implemnting Loading-Content-Error in Fragment.
Usually a Fragment is doing the same thing over and over again. It loads data in background, display a loading view (i.e ProgressBar) while loading, displays the loaded data on screen or displays an error view if loading failed.
Binary file added app-debug.apk
Binary file not shown.
Empty file modified app/.gitignore
100644 → 100755
Empty file.
59 changes: 52 additions & 7 deletions app/build.gradle
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,27 +1,72 @@
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'me.tatarka:gradle-retrolambda:3.2.3'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4'
//classpath 'com.jakewharton.hugo:hugo-plugin:1.2.1'
}
}

apply plugin: 'me.tatarka.retrolambda'
apply plugin: 'com.android.application'
apply plugin: 'android-apt'

android {
compileSdkVersion 22
buildToolsVersion "22.0.1"
compileSdkVersion rootProject.ext.android.compileSdkVersion
buildToolsVersion rootProject.ext.android.buildToolsVersion

defaultConfig {
applicationId "android.coding.interview.makeitawesome"
minSdkVersion 14
targetSdkVersion 22
minSdkVersion rootProject.ext.android.minSdkVersion
targetSdkVersion rootProject.ext.android.targetSdkVersion
versionCode 1
versionName "1.0"

testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}

compileOptions {
encoding "UTF-8"
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

}

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.2.0'
compile 'com.android.support:design:22.2.0'
compile 'com.android.support:recyclerview-v7:22.2.0'
compile project(':data')
compile rootProject.ext.dependencies.supportAppCompat
compile rootProject.ext.dependencies.supportDesign
compile rootProject.ext.dependencies.supportRecyclerview
compile rootProject.ext.dependencies.supportAnnotations
compile rootProject.ext.dependencies.rxandroid
compile rootProject.ext.dependencies.mosby
compile rootProject.ext.dependencies.mosbyViewState
compile rootProject.ext.dependencies.dagger2
compile rootProject.ext.dependencies.picasso

debugCompile rootProject.ext.dependencies.leakcanary

apt rootProject.ext.dependencies.dagger2Compiler
provided 'org.glassfish:javax.annotation:10.0-b28'

testCompile rootProject.ext.dependencies.junit
testCompile rootProject.ext.dependencies.mockito

androidTestCompile rootProject.ext.dependencies.mockito
androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.1'
androidTestCompile rootProject.ext.dependencies.supportAnnotations
androidTestCompile 'com.android.support.test:runner:0.4.1'
androidTestCompile 'com.android.support.test:rules:0.4.1'
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
androidTestCompile 'org.hamcrest:hamcrest-library:1.3'
}
Empty file modified app/proguard-rules.pro
100644 → 100755
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package android.coding.interview.makeitawesome;

import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;

import org.junit.Rule;
import org.junit.runner.RunWith;

/**
* Created by liang on 01/02/16.
*/
@RunWith(AndroidJUnit4.class)
public class DetailActivityTest {
//TODO
@Rule
public ActivityTestRule<DetailActivity> mActivityRule = new ActivityTestRule<DetailActivity>(DetailActivity.class);


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
package android.coding.interview.makeitawesome;

import android.coding.interview.makeitawesome.di.AppComponent;
import android.coding.interview.makeitawesome.di.DaggerAppComponent;
import android.coding.interview.makeitawesome.domain.Repository;
import android.coding.interview.makeitawesome.domain.entity.Event;
import android.coding.interview.makeitawesome.domain.entity.Events;
import android.coding.interview.makeitawesome.domain.entity.Images;
import android.support.test.InstrumentationRegistry;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.ArrayList;
import java.util.List;

import rx.Observable;

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.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.RootMatchers.withDecorView;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.not;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;


/**
* Created by liang on 01/02/16.
*/
@RunWith(AndroidJUnit4.class)
public class MainActivityTest {
public final ActivityTestRule<MainActivity> mainActivityActivityTestRule = new ActivityTestRule<>(MainActivity.class);

@Mock
Repository mMockRepository;

@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
}

@Test
public void testErrorViewIsDisplayedWhenAnErrorIsReturned() throws InterruptedException {

App app = (App) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();

//create mock repository returns error;
when(mMockRepository.getNowShowingMovies(anyInt())).thenReturn(Observable.error(new Throwable("test")));
AppComponent component = DaggerAppComponent.builder()
.appModule(new MockAppModule(app, mMockRepository))
.build();
app.setAppComponent(component);

mainActivityActivityTestRule.launchActivity(null);
onView(withId(R.id.errorView)).check(matches(isDisplayed()));
onView(withId(R.id.contentView)).check(matches(not(isDisplayed())));
onView(withId(R.id.emptyView)).check(matches(not(isDisplayed())));
onView(withId(R.id.loadingView)).check(matches(not(isDisplayed())));
}

@Test
public void testContentViewIsDisplayedWhenAnEventsIsReturned() throws InterruptedException {
App app = (App) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();

//create mock repository returns an Events instance
when(mMockRepository.getNowShowingMovies(anyInt())).thenReturn(Observable.just(createEvents()));
AppComponent component = DaggerAppComponent.builder()
.appModule(new MockAppModule(app, mMockRepository))
.build();
app.setAppComponent(component);

mainActivityActivityTestRule.launchActivity(null);
onView(withId(R.id.contentView)).check(matches(isDisplayed()));
onView(withId(R.id.errorView)).check(matches(not(isDisplayed())));
onView(withId(R.id.emptyView)).check(matches(not(isDisplayed())));
onView(withId(R.id.loadingView)).check(matches(not(isDisplayed())));
}

@Test
public void testEmptyViewIsDisplayedWhenAnEventsWithEmptyEventListIsReturned() throws InterruptedException {

App app = (App) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();

//create mock repository returns an Events instance with empty Event list
Events events = new Events();
events.setEvents(new ArrayList<>());
when(mMockRepository.getNowShowingMovies(anyInt())).thenReturn(Observable.just(events));
AppComponent component = DaggerAppComponent.builder()
.appModule(new MockAppModule(app, mMockRepository))
.build();
app.setAppComponent(component);

mainActivityActivityTestRule.launchActivity(null);
onView(withId(R.id.errorView)).check(matches(not(isDisplayed())));
onView(withId(R.id.loadingView)).check(matches(not(isDisplayed())));
onView(withId(R.id.contentView)).check(matches(isDisplayed()));
onView(withId(R.id.emptyView)).check(matches(isDisplayed()));
}

@Test
public void testLoadingViewIsDisplayedWhenAnRequestIsGoingOn() throws InterruptedException {

App app = (App) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();
//create mock repository never returns result.
when(mMockRepository.getNowShowingMovies(anyInt())).thenReturn(Observable.never());
AppComponent component = DaggerAppComponent.builder()
.appModule(new MockAppModule(app, mMockRepository))
.build();
app.setAppComponent(component);

mainActivityActivityTestRule.launchActivity(null);
onView(withId(R.id.loadingView)).check(matches(isDisplayed()));
onView(withId(R.id.errorView)).check(matches(not(isDisplayed())));
onView(withId(R.id.contentView)).check(matches(not(isDisplayed())));
onView(withId(R.id.emptyView)).check(matches(not(isDisplayed())));
}

@Test
public void testToastMessageIsDisplayedWhenAnErrorReturnedTriggeredBySwipeRefreshLayout() throws InterruptedException {

App app = (App) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();
//create mock repository returns an Events.
when(mMockRepository.getNowShowingMovies(anyInt())).thenReturn(Observable.just(mock(Events.class)));
AppComponent component = DaggerAppComponent.builder()
.appModule(new MockAppModule(app, mMockRepository))
.build();
app.setAppComponent(component);

mainActivityActivityTestRule.launchActivity(null);

//create mock repository returns an error.
when(mMockRepository.getNowShowingMovies(anyInt())).thenReturn(Observable.error(new Throwable("test error")));
onView(withId(R.id.contentView)).perform(swipeDown());
onView(withId(R.id.loadingView)).check(matches(not(isDisplayed())));
onView(withId(R.id.errorView)).check(matches(not(isDisplayed())));
onView(withId(R.id.contentView)).check(matches(isDisplayed()));

//verify if Toast message is displayed
onView(withText("test error"))
.inRoot(withDecorView(not(mainActivityActivityTestRule.getActivity().getWindow().getDecorView())))
.check(matches(isDisplayed()));
}

@Test
public void testLoadingViewIsDisplayedWhenErrorViewIsClicked() throws InterruptedException {

App app = (App) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();

//create mock repository returns error;
when(mMockRepository.getNowShowingMovies(anyInt())).thenReturn(Observable.error(new Throwable("test")));
AppComponent component = DaggerAppComponent.builder()
.appModule(new MockAppModule(app, mMockRepository))
.build();
app.setAppComponent(component);

mainActivityActivityTestRule.launchActivity(null);

//create mock repository never returns result;
when(mMockRepository.getNowShowingMovies(anyInt())).thenReturn(Observable.never());
onView(withId(R.id.errorView)).check(matches(isDisplayed()));
onView(withId(R.id.errorView)).perform(click());
onView(withId(R.id.errorView)).check(matches(not(isDisplayed())));
onView(withId(R.id.contentView)).check(matches(not(isDisplayed())));
onView(withId(R.id.emptyView)).check(matches(not(isDisplayed())));
onView(withId(R.id.loadingView)).check(matches(isDisplayed()));
}

@Test
public void testContentViewIsDisplayedWhenErrorViewIsClicked() throws InterruptedException {

App app = (App) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();

//create mock repository returns error;
when(mMockRepository.getNowShowingMovies(anyInt())).thenReturn(Observable.error(new Throwable("test")));
AppComponent component = DaggerAppComponent.builder()
.appModule(new MockAppModule(app, mMockRepository))
.build();
app.setAppComponent(component);

mainActivityActivityTestRule.launchActivity(null);

//create mock repository returns an Event;
when(mMockRepository.getNowShowingMovies(anyInt())).thenReturn(Observable.just(mock(Events.class)));
onView(withId(R.id.errorView)).check(matches(isDisplayed()));
onView(withId(R.id.errorView)).perform(click());
onView(withId(R.id.errorView)).check(matches(not(isDisplayed())));
onView(withId(R.id.contentView)).check(matches(isDisplayed()));
onView(withId(R.id.loadingView)).check(matches(not(isDisplayed())));
}

@Test
public void testErrorViewIsDisplayedWhenErrorViewIsClicked() throws InterruptedException {

App app = (App) InstrumentationRegistry.getInstrumentation().getTargetContext().getApplicationContext();

//create mock repository returns error;
when(mMockRepository.getNowShowingMovies(anyInt())).thenReturn(Observable.error(new Throwable("test")));
AppComponent component = DaggerAppComponent.builder()
.appModule(new MockAppModule(app, mMockRepository))
.build();
app.setAppComponent(component);

mainActivityActivityTestRule.launchActivity(null);

onView(withId(R.id.errorView)).check(matches(isDisplayed()));
onView(withId(R.id.errorView)).perform(click());
onView(withId(R.id.errorView)).check(matches(isDisplayed()));
onView(withId(R.id.contentView)).check(matches(not(isDisplayed())));
onView(withId(R.id.loadingView)).check(matches(not(isDisplayed())));

}

private Events createEvents() {
Events events = new Events();
Event event = new Event();
event.setTitle("test");
Images images = new Images();
images.setEventMediumImagePortrait("http://media.finnkino.fi/1012/Event_10655/portrait_medium/Zoolander2_1080t.jpg");
event.setImages(images);
List<Event> eventList = new ArrayList<>();
eventList.add(event);
events.setEvents(eventList);
return events;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package android.coding.interview.makeitawesome;

import android.app.Application;
import android.coding.interview.makeitawesome.data.RepositoryImpl;
import android.coding.interview.makeitawesome.di.AppModule;
import android.coding.interview.makeitawesome.domain.Repository;

/**
* Mock module to facilitate testing
*
* Created by liang on 01/02/16.
*/
public class MockAppModule extends AppModule {

private Repository mRepository;

public MockAppModule(Application app, Repository repository) {
super(app);
mRepository = repository;
}

@Override
public Repository provideRepository(RepositoryImpl repository) {
return mRepository;
}
}
Loading