Skip to content

Instantly share code, notes, and snippets.

@uzzu
Created September 22, 2017 08:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save uzzu/53f8206f3a8622907fbd498d369783a0 to your computer and use it in GitHub Desktop.
Save uzzu/53f8206f3a8622907fbd498d369783a0 to your computer and use it in GitHub Desktop.
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'
}
<?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>
@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;
}
}
}
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;
}
}
@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;
}
}
}
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);
}
}
}
<?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>
@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;
}
}
@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