Wednesday, December 1, 2010

Creating a Shadow for Robolectric in 6 steps

Some days ago I asked for a pull request in Robolectric's github repo. I finished my ShadowConnectivityManager. The discussion can be read here. I will explain how I did it so someone else can continue creating Shadows :).

Why do I need a ShadowConnectivityManager?
The app we are doing right now requires internet to work so we did a nice SplashScreen that checks if internet is available.
If there is internet go to the main Activity, else show a cute msg saying that you don't have internet.

So we created a class called ConnectionStatus which has a method:

boolean isOnline();


NOTE: We should have created the test first but since we are also giving a try to roboguice we did the code before the test.


public class ConnectionStatus implements IConnectionStatus {

private ConnectivityManager cm;

@Inject
public ConnectionStatus(ConnectivityManager cm) {
this.cm = cm;
}

public boolean isOnline() {
NetworkInfo netInfo = cm.getActiveNetworkInfo();
if (netInfo != null && netInfo.isConnectedOrConnecting()) {
return true;
}

return false;
}
}


Instead of doing:
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
we can inject it :)

Ok, time to test. When I try to inject a ConnectivityManager to a test there was an exception saying that my variable should have a @Nullable annotation.

Conclusion:
Null instance in test scope == "Robolectric needs your help! Create a Shadow object!"

STEP 1: Understand how Android works
Robolectric basically mocks Android's behavior. You need to understand how it works for you to mock it correctly.

In our case:

* You use an Android's context to get the ConnectivityManager which is a SystemService.
* You ask the ConnectivityManager to give you the ActiveNetworkInfo.
* The class NetworkInfo has a method isConnectedOrConnecting() which will determine if your are online or not.

STEP 2: Decide how the Shadow should be used

We want to be able to set if we are connected or not and check if our ConnectionStatus class is returning things accordingly.
To do that we will leave a flag inside the NetworkInfo's Shadow to let the user decide what it should return.

STEP 3: Code the Shadow
In our case we need to code ShadowConnectivityManager and ShadowNetworkInfo

// With the Implements annotation we tell Robolectric which Android's class this is shadow of.
@Implements(ConnectivityManager.class)
public class ShadowConnectivityManager {

// We will hold here an instance of the ActiveNetworkInfo.
private NetworkInfo networkInfo;


// All methods that exists in the real Android's class and overridden by the shadow
// must have an Implementation annotation. I don't know what for :(
@Implementation
public NetworkInfo getActiveNetworkInfo() {
return networkInfo == null ? networkInfo = newInstanceOf(NetworkInfo.class) : networkInfo;
}
}

Ok, my first big doubt was: "Ok, I need to return a NetworkInfo but I need it to be a shadow, how do I do it?"
That what Robolectric.newInstanceOf() is for :)



@Implements(NetworkInfo.class)
public class ShadowNetworkInfo {

private boolean isConnected = true;

@Implementation
public boolean isConnectedOrConnecting() {
return isConnected;
}

/**
* Non-Android accessor
* Sets up the return value of {@link #isConnectedOrConnecting()}.
*
* @param isConnected the value that {@link #isConnectedOrConnecting()} will return.
*/
public void setConnectionStatus(boolean isConnected) {
this.isConnected = isConnected;
}
}


Pretty easy as well. We have an Non-Android accesor that we will use to set the wanted connectivity status.


STEP 4: Modify Robolectric.java

* Add your Shadow classes to the list in the getDefaultShadowClasses() method.
* Create the shadowOf methods:

public static ShadowNetworkInfo shadowOf(NetworkInfo instance) {
return (ShadowNetworkInfo) shadowOf_(instance);
}

public static ShadowConnectivityManager shadowOf(ConnectivityManager instance) {
return (ShadowConnectivityManager) shadowOf_(instance);
}

* Optional: I needed to modify ShadowApplication as well.

Why?
Application is in charge of the getSystemService's method.
We need to modify it to return our ShadowConnectivityManager just adding this:

if (name.equals(Context.CONNECTIVITY_SERVICE)) {
return connectivityManager == null ? connectivityManager = newInstanceOf(ConnectivityManager.class) : connectivityManager;
}



STEP 5: Test your Shadow
I will just copy and paste the test we did. It's quite simple:

@RunWith(WithTestDefaultsRunner.class)
public class ConnectivityManagerTest {
private ConnectivityManager connectivityManager;
private ShadowNetworkInfo networkInfo;

@Before
public void setUp() throws Exception {
Robolectric.bindDefaultShadowClasses();
Robolectric.application = new Application();
connectivityManager = (ConnectivityManager) Robolectric.application.getSystemService(Context.CONNECTIVITY_SERVICE);
networkInfo = Robolectric.shadowOf(connectivityManager.getActiveNetworkInfo());
}

@Test
public void getConnectivityManagerShouldNotBeNull() {
assertNotNull(connectivityManager);
assertNotNull(connectivityManager.getActiveNetworkInfo());
}

@Test
public void networkInfoShouldReturnTrueCorrectly() {
networkInfo.setConnectionStatus(true);

assertTrue(connectivityManager.getActiveNetworkInfo().isConnectedOrConnecting());
}

@Test
public void networkInfoShouldReturnFalseCorrectly() {
networkInfo.setConnectionStatus(false);

assertFalse(connectivityManager.getActiveNetworkInfo().isConnectedOrConnecting());
}
}


STEP 6: Share it!
Ask for a pull request!


How did our testcase end up being?


@RunWith(XTestRunner.class)
public class ConnectionStatusTest {

@Inject
private ConnectivityManager cm;

@Test
public void connectionStatusShouldBeFalseWhenOffline() {
ConnectionStatus connectStatus = new ConnectionStatus(
getConnectivityManager(false));
assertFalse(connectStatus.isOnline());
}

@Test
public void connectionStatusShouldBeTrueWhenOnline() {

ConnectionStatus connectStatus = new ConnectionStatus(
getConnectivityManager(true));
assertTrue(connectStatus.isOnline());

}

private ConnectivityManager getConnectivityManager(boolean connected) {

ShadowNetworkInfo ni = Robolectric.shadowOf(cm.getActiveNetworkInfo());
ni.setConnectionStatus(connected);

return cm;
}
}



Conclusion:

While I code this simple Shadow for ConnectiviyManager someone else created a Shadow for AsyncTask and SQLite!
Shadows amount is increasing version by version => Android TDD is getting easier and easier thanks to Robolectric.

Robolectric it's a very cool fw. I consider myself a newbie to it but I could place some commits to it ;)
The support in the mailing list is excellent and I guess in the next release we will be able to add mvn support completely.

What are you waiting to give it a try?

1 comment: