Created
September 22, 2017 08:38
-
-
Save uzzu/53f8206f3a8622907fbd498d369783a0 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
dependencies { | |
compile fileTree(dir: 'libs', include: ['*.jar']) | |
compile 'com.android.support:recyclerview-v7:22.2.1' | |
compile 'com.squareup.dagger:dagger:1.2.2' | |
provided 'com.squareup.dagger:dagger-compiler:1.2.2' | |
testCompile 'com.squareup.assertj:assertj-android:1.1.0' | |
testCompile 'org.robolectric:robolectric:3.0' | |
testCompile('org.robolectric:shadows-support-v4:3.0') { | |
exclude module: 'support-v4' | |
} | |
testCompile 'org.mockito:mockito-all:1.10.19' | |
testCompile 'net.javacrumbs.json-unit:json-unit-fluent:1.5.6' | |
testCompile 'com.squareup.assertj:assertj-android-recyclerview-v7:1.1.0@aar' | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | |
android:orientation="vertical" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent"> | |
<android.support.v7.widget.RecyclerView | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:id="@+id/candies_list" /> | |
</LinearLayout> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@RunWith(RobolectricGradleTestRunner.class) | |
@Config(constants = BuildConfig.class, sdk = 18) | |
// those annotations make it possible to use an application, cast to a TestCandiesApplication, for dependency injection. | |
public class CandiesFragmentTest { | |
private TestableCandiesFragment fragment; | |
// We could not inject a layoutManager as a dependency because it is an Android class and it has no public, zero-argument constructor. | |
// The solution here was to new one up in the real fragment in a method called getLayoutManager(), then subclass that fragment specifically for testing purposes | |
// and override getLayoutManager() to return a layout manager from a field on the object. | |
// we set that field to the mockLayoutManager seen below. | |
private LinearLayoutManager mockLayoutManager; | |
@Inject | |
CandiesBroadcastReceiver mockBroadcastReceiver; | |
@Inject | |
CandiesListAdapter mockAdapter; | |
@Before | |
public void setUp() { | |
((TestCandiesApplication) RuntimeEnvironment.application).inject(this); | |
//making a mock of the layout manager... | |
mockLayoutManager = Mockito.mock(LinearLayoutManager.class); | |
fragment = new TestableCandiesFragment(); | |
//setting it on our testable subclass... | |
fragment.setLayoutManager(mockLayoutManager); | |
//Start the fragment! | |
SupportFragmentTestUtil.startFragment(fragment); | |
} | |
@Test | |
public void defaultDisplay() { | |
RecyclerView recyclerView = (RecyclerView) fragment.getView().findViewById(R.id.candies_list); | |
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); | |
//assertThat(LayoutManager layoutManager) is provided to us by the assertj-android-recyclerview library. | |
assertThat(layoutManager).isEqualTo(mockLayoutManager); | |
} | |
@Test | |
public void onViewCreated_setsAdapterAndAttributes() { | |
//because we inject our mockAdapter into our fragment, we use a mock here to make sure that we give it | |
//everything it needs to function. | |
//it needs the list of candies in order to present them, | |
//and it needs a context so that, when we click on a candy, the adapter can use the context | |
//to launch a new activity that displays details about the candy. | |
verify(mockAdapter).setCandies(Matchers.<List>any()); | |
verify(mockAdapter).setContext(fragment); | |
} | |
//Now we make sure that the adapter is showing us our candies in the list. | |
@Test | |
public void onSuccess_populatesViewWithListOfCandies() { | |
Candy candy = new Candy(); | |
candy.setName("Chocolate Frogs"); | |
candy.setDescription("Eat this delicious magical treat before it hops away!"); | |
fragment.onSuccess(CandyBroadcastReceiver.Actions.GET_CANDIES, asList(candy)); | |
CandiesListAdapter adapter = (CandiesListAdapter) fragment.getRecyclerView().getAdapter(); | |
Assertions.assertThat(adapter.getItemCount()).isEqualTo(1); | |
Assertions.assertThat(adapter.getItemAtPosition(0)).isSameAs(candy); | |
Candy otherCandy = new Candy(); | |
fragment.onSuccess(CandyBroadcastReceiver.Actions.GET_CANDIES, asList(otherCandy)); | |
Assertions.assertThat(adapter.getItemCount()).isEqualTo(1); | |
Assertions.assertThat(adapter.getItemAtPosition(0)).isSameAs(otherCandy); | |
} | |
//Here is the subclass of CandiesFragment that we use for testing. //It overrides getLayoutManager to return a mock so we can assert on the mock. | |
public static class TestableCandiesFragment extends CandiesFragment { | |
private LinearLayoutManager mockLayoutManager; | |
public void setLayoutManager(LinearLayoutManager mockLayoutManager) { | |
this.mockLayoutManager = mockLayoutManager; | |
} | |
@Override | |
public LinearLayoutManager getLayoutManager() { | |
return mockLayoutManager; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class CandiesFragment extends Fragment { | |
@Inject | |
CandiesBroadcastReceiver receiver; | |
@Inject | |
CandiesListAdapter candiesListAdapter; | |
private ArrayList listOfCandies = new ArrayList(); | |
@Override | |
public void onCreate(Bundle savedInstanceState) { | |
super.onCreate(savedInstanceState); | |
((CandiesApplication) getActivity().getApplication()).inject(this); | |
receiver.register(this); | |
getActivity().registerReceiver(receiver, new IntentFilter(CandiesBroadcastReceiver.Actions.GET_CANDIES)); | |
getActivity().startService(new Intent(getActivity().getApplicationContext(), CandiesService.class)); | |
} | |
@Override | |
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { | |
return inflater.inflate(R.layout.fragment_candiesrecycler_view, container, false); | |
} | |
@Override | |
public void onViewCreated(View view, Bundle savedInstanceState) { | |
super.onViewCreated(view, savedInstanceState); | |
//Here we are setting our layout manager on our recycler view! | |
getRecyclerView().setLayoutManager(getLayoutManager()); | |
//Now we pass the list adapter everything it needs... | |
candiesListAdapter.setContext(this); | |
candiesListAdapter.setCandies(listOfCandies); | |
//and we set it on the recycler view, too. | |
getRecyclerView().setAdapter(candiesListAdapter); | |
} | |
// Here is the method we extract to override in our testable subclass | |
public LinearLayoutManager getLayoutManager() { | |
return new LinearLayoutManager(getActivity()); | |
} | |
// I make a habit of extracting methods to encapsulate android classes reaching into their | |
// layouts to get elements. Often these elements need to be mocked in testing. | |
public RecyclerView getRecyclerView() { | |
return (RecyclerView) getView().findViewById(R.id.candies_list); | |
} | |
@Override | |
public void onSuccess(String action, Object data) { | |
listOfCandies.clear(); | |
List candies = (List)data; | |
listOfCandies.addAll(candies); | |
candiesListAdapter.setCandies(listOfCandies); | |
getRecyclerView().getAdapter().notifyDataSetChanged(); | |
} | |
@Override | |
public void onFailure(CandiesApiError error) { | |
} | |
@Override | |
public void onDestroy() { | |
super.onDestroy(); | |
getActivity().unregisterReceiver(receiver); | |
} | |
public ArrayList getListOfCandies() { | |
return listOfCandies; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@RunWith(RobolectricGradleTestRunner.class) | |
@Config(constants = BuildConfig.class, sdk = 18) | |
public class ExampleListAdapterTest { | |
private CandiesListAdapter adapter; | |
private CandiesListAdapter.CandyViewHolder holder; | |
private View mockView; | |
private Fragment mockFragment; | |
@Before | |
public void setUp() throws Exception { | |
((TestApplication) RuntimeEnvironment.application).inject(this); | |
adapter = new CandiesListAdapter(); | |
mockView = mock(View.class); | |
mockFragment = mock(Fragment.class); | |
stub(mockFragment.getString(anyInt())).toReturn("Candy"); | |
} | |
@Test | |
public void itemCount() { | |
Candy candy = new Candy(); | |
adapter.setCandies(asList(candy, candy, candy)); | |
assertThat(adapter.getItemCount()).isEqualTo(3); | |
} | |
@Test | |
public void getItemAtPosition() { | |
Candy firstCandy = new Candy(); | |
Candy secondCandy = new Candy(); | |
adapter.setCandies(asList(firstCandy, secondCandy)); | |
assertThat(adapter.getItemAtPosition(0)).isEqualTo(firstCandy); | |
assertThat(adapter.getItemAtPosition(1)).isEqualTo(secondCandy); | |
} | |
@Test | |
public void onBindViewHolder_setsTextAndClickEventForCandyView() { | |
Candy candy = new Candy(); | |
candy.setName(asList("Gumdrops")); | |
candy.setDescription("Don't leave these sticky treats in a car during the summer."); | |
adapter.setCandies(asList(candy)); | |
adapter.setContext(mockFragment); | |
LayoutInflater inflater = (LayoutInflater) RuntimeEnvironment.application.getSystemService(Context.LAYOUT_INFLATER_SERVICE); | |
//We have a layout especially for the items in our recycler view. We will see it in a moment. | |
View listItemView = inflater.inflate(R.layout.list_adapter_candies_item, null, false); | |
holder = new CandiesListAdapter.CandyViewHolder(listItemView); | |
adapter.onBindViewHolder(holder, 0); | |
assertThat(holder.nameView.getText().toString()).isEqualTo("Gumdrops"); | |
assertThat(holder.descriptionView.getText().toString()).isEqualTo("Don't leave these sticky treats in a car during the summer."); | |
holder.itemView.performClick(); | |
Intent intent = new Intent(mockFragment.getActivity(), CandyDetailActivity.class); | |
intent.putExtra("candy", candy); | |
verify(mockFragment).startActivity(intent); | |
} | |
@Test | |
public void onCreateViewHolder_returnsNewCandyViewHolderOfCorrectLayout() { | |
TestableCandiesListAdapter testableAdapter = new TestableCandiesListAdapter(); | |
testableAdapter.setMockView(mockView); | |
CandiesListAdapter.CandyViewHolder candyViewHolder = testableAdapter.onCreateViewHolder(new FrameLayout(RuntimeEnvironment.application), 0); | |
assertThat(candyViewHolder.itemView).isSameAs(mockView); | |
} | |
//Here we subclass and override the test subject again so we can use a mock view for testing, instead of the real one. | |
static class TestableCandiesListAdapter extends CandiesListAdapter { | |
public View mockView; | |
public void setMockView(View mockView) { | |
this.mockView = mockView; | |
} | |
@Override | |
public View getLayout(ViewGroup parent) { | |
return mockView; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class CandiesListAdapter extends RecyclerView.Adapter { | |
private List candies; | |
private Fragment context; | |
public Fragment getContext() { | |
return context; | |
} | |
public void setContext(Fragment context) { | |
this.context = context; | |
} | |
@Override | |
public CandyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { | |
View view = getLayout(parent); | |
return new CandyViewHolder(view); | |
} | |
@Override | |
public void onBindViewHolder(CandyViewHolder holder, int position) { | |
final Candy candy = getItemAtPosition(position); | |
//This seemed the most sensible place to put an item-specific onClickListener, since this is where all of the item-specific settings are handled. | |
holder.itemView.setOnClickListener(new View.OnClickListener() { | |
@Override | |
public void onClick(View v) { | |
Intent intent = new Intent(getContext().getActivity(), CandyDetailActivity.class)); | |
intent.putExtra("candy", candy); | |
context.startActivity(intent); | |
} | |
}); | |
holder.nameView.setText(candy.getName()); | |
holder.descriptionView.setText(candy.getDescription()); | |
} | |
@Override | |
public int getItemCount() { | |
return candies.size(); | |
} | |
public View getLayout(ViewGroup parent) { | |
return LayoutInflater.from(parent.getContext()).inflate(R.layout.list_adapter_candy_item, null); | |
} | |
public Candy getItemAtPosition(int position) { | |
return candies.get(position); | |
} | |
public void setCandies(List candies) { | |
this.candies = candies; | |
} | |
// This view holder allows the Adapter to hang onto the instances of an individual item view | |
// and reuse them when they go offscreen for the new views that have to come onscreen. | |
public static class CandyViewHolder extends RecyclerView.ViewHolder { | |
public TextView nameView; | |
public TextView descriptionView; | |
public View itemView; | |
public CandyViewHolder(View itemView) { | |
super(itemView); | |
this.itemView = itemView; | |
nameView = (TextView) itemView.findViewById(R.id.candy_name); | |
descriptionView = (TextView) itemView.findViewById(R.id.candy_description); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="utf-8"?> | |
<LinearLayout | |
xmlns:android="http://schemas.android.com/apk/res/android" | |
xmlns:tools="http://schemas.android.com/tools" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:padding="@dimen/gutter" | |
android:orientation="vertical" | |
> | |
<TextView | |
style="@style/FontSizeS" | |
android:id="@+id/candy_name" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
tools:text="Rainbow Jawbreakers" | |
/> | |
<TextView | |
style="@style/FontSizeXS" | |
android:id="@+id/candy_description" | |
android:layout_width="wrap_content" | |
android:layout_height="wrap_content" | |
android:textColor="@color/medium_gray" | |
android:singleLine="true" | |
tools:text="Rainbow jawbreakers are large sugary spheres that look fun and innocent...but don't bite them!" | |
/> | |
</LinearLayout> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Module( | |
injects = { | |
... | |
CandiesFragmentTest.class, | |
//Because the TestableCandiesFragment subclasses CandiesFragment, it needs to be on the list | |
//to receive any injections that its superclass needs. So here it is. | |
//We only need it in the TestCandiesModule, not the CandiesModule, because we do not | |
//instantiate one anywhere outside of the tests. | |
CandiesFragmentTest.TestableCandiesFragment.class, | |
CandiesListAdapterTest.class, | |
}, | |
complete = false, | |
overrides = true, | |
library = true | |
) | |
public class TestCandiesModule { | |
@Mock | |
CandiesService mockCandiesService; | |
@Mock | |
CandiesListAdapter mockAdapter; | |
public TestApplicationModule() { | |
initMocks(this); | |
} | |
@Provides | |
CandiesListAdapter provideMockAdapter() { | |
return mockAdapter; | |
} | |
@Provides | |
CandiesService provideCandiesService() { | |
return mockCandiesService; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Module( | |
injects = { | |
CandiesFragment.class, | |
CandiesListAdapter.class, | |
}, | |
complete = false | |
) | |
public class CandiesModule { | |
@Provides | |
CandiesService provideCandiesService() { | |
return new CandiesService(); | |
} | |
@Provides | |
CandiesListAdapter provideCandiesAdapter() { | |
return new CandiesListAdapter(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment