Test-Driven Android: Testing Layout Elements with Subclass and Override

Reading Time: 6 minutes

There are a lot of reasons to write well-tested code: to drive software design decisions, to document the existing system, or to create a safety harness for confidently executing changes and refactors. But the Android framework (like most mobile and web frameworks) does not always lend itself to easy unit testing. Today we’ll talk about one particular case: the one in which you want to test that a method call on a view object that gets instantiated in the layout for an activity or a fragment.

For example, let’s say that I want to animate a particular view that I have stuck into my fragment layout. My layout file might look something like this:

<?xml version=”1.0″ encoding=”utf-8″?>
<LinearLayout
android:id=”@+id/fragment_example”
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:gravity=”center”
android:orientation=”vertical”>
<RelativeLayout
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”>
<ImageView
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:id=”@+id/example_animated_thingo”
android:src=”@drawable/thingo”
/>
</RelativeLayout>
</LinearLayout>

My fragment animates the example_animated_thingo in its onResume() method with a custom animation that I inject into the fragment with Dagger:

public class ExampleFragment extends android.support.v4.app.Fragment {

  @Inject ExampleAnimation exampleAnimation;

  public static ExampleFragment newInstance(Bundle bundle) {
      ExampleFragment fragment = new ExampleFragment();
      fragment.setArguments(bundle);
      return fragment;
  }

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
((ExampleApplication) getActivity().getApplication()).inject(this);
}

   @Override
  public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
      return inflater.inflate(R.layout.fragment_example, container, false);
  }

  @Override
  public void onResume() {
      super.onResume();
      …
      exampleAnimation.setNewAngle(200);
      exampleAnimation.setDuration(1000);
     getView().findViewById(R.id.example_animated_thingo).startAnimation(exampleAnimation);
  }
}

What I want to do here is test that startAnimation() gets called on my view. But I don’t have a handle on that view in the test right now because I don’t inject it into the fragment: the fragment grabs it straight out of the layout. What to do?

Michael Feathers provides an answer in his book, Working Effectively With Legacy Code. The book discusses how to extend, refactor, and test a code base originally written in a hard-to-test way.1

LegacyCode

One of Feathers’s patterns, called Subclass and Override, suggests creating a subclass of the object under test that specifically gets used for testing its internal components. In the book’s examples, the pattern gets used to test a class that news up one of its dependencies instead of injecting it from the outside. We have a very similar situation here: our fragment is getting a View object out of its own layout. Rather than look for a way to make the thing programmatically, which might make the code more confusing, we can instead implement the subclass and override pattern to test it just the way it is.

…Well, almost just the way it is. We do need to make one change to ExampleFragment. Instead of starting the animation on the view object itself, we need to encapsulate the getting of the view object in a method that we can override in a subclass. So instead of calling this:

getView().findViewById(R.id.example_animated_thingo).startAnimation(exampleAnimation);

we will now call this:

getExampleAnimatedView().startAnimation(exampleAnimation);

and we will add this method to our ExampleFragment so it can grab our view item just like before.

public ImageView getExampleAnimatedView() {
     return (ImageView) getView().findViewById(R.id.example_animated_thingo);
}

Now, here comes the test portion of the program. We can create a mock of that view in our test, then override ExampleFragment’s getExampleAnimatedView() method to return that mock. The mock will allow us to verify that a certain method was called on it. In this example, I’m using Mockito mocks. The same technique can apply with a test double of any other origin. 

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 18)
public class ExampleFragmentTest {
  @Inject
  ExampleAnimation mockExampleAnimation;
  @Mock
  ImageView mockExampleAnimatedView;

  @Before
  public void setup() {
      initMocks(this);
      ((TestExampleApplication) RuntimeEnvironment.application).inject(this);
  }

  @Test
  public void presentsExampleFragment() {
      TestableExampleFragment testFragment = TestableExampleFragment.newInstance(bundle);
      testFragment.setMockExampleAnimatedView(mockExampleAnimatedView);
      SupportFragmentTestUtil.startVisibleFragment(detailsFragment);

      verify(mockExampleAnimation).setNewAngle(200);
      verify(mockExampleAnimation).setDuration(1000);
      verify(mockExampleAnimatedView).startAnimation(mockEllipseAnimation);

  }

  public static class TestableExampleFragment extends ExampleFragment {
      ImageView mockExampleAnimatedView;

      public static TestableExampleFragment newInstance(Bundle bundle) {
          TestableExampleFragment fragment = new TestableExampleFragment();
          fragment.setArguments(bundle);
          return fragment;
      }

      public void setMockExampleAnimatedView(ImageView mockExampleAnimatedView) {
          this.mockExampleAnimatedView = mockExampleAnimatedView;
      }

      @Override
      public ImageView getExampleAnimatedtView() {
          return mockExampleAnimatedView;

      }
  }
}

So we have two things going on in this test file.

  1. Our test is not testing an instance of the ExampleFragment. It is testing an instance of our subclass, the TestableExampleFragment. The TestableExampleFragment allows this by having its own newInstance() method (as one cannot override the static newInstance() method in the real ExampleFragment class). However, for any behavior that this fragment does not override from its superclass, it will get the behavior of the real thing. So we get to test the behavior of the real thing every place except for those that we specifically choose to override.
  2. Our TestableExampleFragment lives inside this test class, under the test method, and it overrides getExampleAnimatedView(). Instead of getting the layout view here, we return a view that we can pass into our object via a setter. I have called my setter setMockExampleAnimatedView(). We make a new instance of TestableExampleFragment, set its example animated view to the mock one that we instantiate in our test class, and then verify inside our test that startAnimation() got called on that mock.

There is one more thing I needed to change in order to get this to work. TestableExampleFragment and ExampleFragment don’t get the animated view injected when the OS instantiates them. However, ExampleFragment does have something that gets injected upon instantiation: the exampleAnimation. Dagger dependency injection for Android takes care of this for us, but because TestableExampleFragment extends ExampleFragment, it has all the same dependencies that ExampleFragment does and so it needs the exampleAnimation injected into it, too. In order for Dagger to do this, TestableExampleFragment needs to be listed in a module that provides ExampleAnimation instances. We have one of these that injects mocks into the test class for us. We need to add ExampleFragmentTest.TestableExampleFragment.class to the list of classes that the module can inject into.

package com.ExampleApp.example;
import org.mockito.Mock;
import dagger.Module;
import dagger.Provides;
import static org.mockito.MockitoAnnotations.initMocks;

@Module(
  injects = {
      ExampleFragmentTest.class,
      ExampleFragmentTest.TestableExampleFragment.class
  },
  complete = false,
  overrides = true,
  library = true

)
public class TestExampleModule {

  @Mock  ExampleAnimation mockExampleAnimation;
  public TestExampleModule() {
      initMocks(this);
  }

  @Provides
  ExampleAnimation provideMockExampleAnimation() {
      return mockEllipseAnimation;
  }
}

Now, inside the ‘real’ ExampleFragment, getExampleAnimatedView() returns the view from the layout, and the fragment calls startAnimation() on that. But the TestableExampleFragment subclass returns its own field instead from that method. So we set that field to a mock ExampleAnimation before starting a TestableExampleFragment, and then when we start the fragment, we can ask that mock if startAnimation() got called on it. If that has not happened, Mockito provides an ‘Actually, there were zero interactions with this mock’ failure message. And when it has happened correctly, the test goes green!

1. It feels odd, and maybe wrong, to apply a term like ‘legacy’ to Android: the term conjures up images of derelict monoliths very unlike our ever-changing mobile framework. That said, what the Feathers book really gives us is a set of tools to test and design our code in situations where we don’t have control over what all the code looks like. This might be the case in an existing, cryptic, monolithic app. But it’s also the case anytime we integrate with…any other code, really, including the Android framework.

One comment

  1. […] In Android, there’s an additional concern: exercising the Android framework in tests is slow. So it makes sense to exercise the framework as little as possible in tests. This gets complicated when we consider the way the Android framework is built, though. First of all, it works chiefly by allowing us to subclass Android classes and override life cycle methods. This is not a recipe for easy dependency injection. Furthermore, idiosyncratic implementations of Android sometimes involve obtaining dependencies (like views from the layout) via a method besides injection. (We talk about how to test dependencies in that scenario in this post). […]

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.