In my last post, a continuation of a written version of the talk I gave at Florida dev fest, I tried to give an answer to the question, “What makes apps testable?” The answer: seams. Seams make apps testable, and in this post, I want to talk about a particular kind of seam: object seams.
The key insight behind object seams in this:
The fundamental thing to recognize is that when we look at a call in an object-oriented program, it does not define which method will actually be executed.
-Michael Feathers, Working Effectively with Legacy Code
When we use this fact to modify the behavior of a particular piece of code without editing the code in that place, then we’re using an object seam. The refactoring I showed in my last post was an example of exploiting an object seam, but we’re going to look at another example.
Before we do that, however, I want to point out that dependency injection is one of the key ways1 that we create object seams. There still seems to be some confusion about dependency injection, so let me quickly try to clarify: Dependency injection is pattern; its not dagger or any other library.
Here’s a definition of dependency injection:
The code that needs dependencies is not responsible for getting them
If that’s true of code that you’re writing, you’re using dependency injection.2
With this in mind, we’re now in a position to see something that I struggled to discover over a year ago when I wrote my series on posts on why android unit testing is hard: the reason MVP makes our apps more testable is because MVP creates object seams via dependency injection.
Let’s see an example of this. Here’s a video of some functionality in the 2015 Google I/O app. Notice that when I first open the app, I’m presented with some cards that ask my preferences on a few things. Once I return to the app, however, those cards are no longer present.
Here’s the relevant code for this functionality:
private void setupCards(CollectionView.Inventory inventory) {
if (SettingsUtils.isAttendeeAtVenue(getContext())) {
if (!ConfMessageCardUtils.hasAnsweredConfMessageCardsPrompt(getContext())) {
inventoryGroup
= new InventoryGroup(GROUP_ID_MESSAGE_CARDS);
MessageData conferenceMessageOptIn = MessageCardHelper
.getConferenceOptInMessageData(getContext());
inventoryGroup.addItemWithTag(conferenceMessageOptIn);
inventoryGroup.setDisplayCols(1);
inventory.addGroup(inventoryGroup);
} // ...
}
}
Take a look at the first two lines. Right away, we have a red flag: static methods. Every use of a static method is a missed opportunity to create an object seam. If we decide we need to unit test some of the code here, we’re going to have trouble arranging in our test code.
Look at that last line of code. In order to convince yourself that you really understand the concept of a seam, ask yourself, “Is there a seam at this line of code?”
We can change the behavior of this particular line of code without editing the source file, so there is in fact a seam here. We can change the behavior of this line of code by passing in various subclasses of CollectionView.Inventory
to this method.
Ok, so we’ve got a mixed bag in terms of our seams. Let’s say we refactor this code to use MVP. Our presenter looks something like this:
class Presenter {
public void presentCards() {
if (mIsAttendeeAtVenue) {
if (!mMsgSettings.hasAnsweredMessagePrompt()) {
mExploreView.addMessageOptInCard();
} // Stuff
}
}
}
The view is one of the injected dependencies and we can easily verify that the appropriate method has been called in our tests. This is the main way in which MVP helps us write more testable code: all of the interaction with the UI can now be verified by swapping out the injected View with an implementation that records its interaction with the Presenter. In our case, this implementation is usually generated by mockito.
One other thing to notice here: we’ve replaced static method calls with calls to injected dependencies. This isn’t necessarily mandated by MVP, but its something that we need to do if want to make this code unit testable.
There’s another kind of seam that we haven’t explored yet: linking seams. Linking seams are created using build variants, and we’ll talk about those more in my next post.
Notes:
-
Another way is by using inheritance. Feathers’ Extract and Override Method, Push Down Dependencies, and Pull Up Dependencies techniques are three interesting ways of creating object seams using inheritance.
-
Martin Fowler’s article is the definitive source on DI. He basically coined the phrase.