My wife recently encouraged me to buy an HTC G1 phone. She's great, isn't she! Two days later, I'm using the development kit, working on my first application. After poking around at the API, and getting the lay of the land it was time to get serious and understand how to interact with the platform. To that end I need to write some unit tests.
So I want to write a unit test for an implementation of android.view.View.OnClickListener. The interface has only one method, no surprise here, it's an onClick method: "public void onClick(View view)". Here's my implementation:
class MyOnclickListener implements OnClickListener {What the "activity.startActivityForResult" method does is not germane to this story. Just know that I need a test that makes sure that method is called with the right Intent. How do I know it's the right Intent object? So here's my first pass at a test:
public void onClick(View view) {
Intent intent = new Intent("a.unique.string");
intent.addCategory(Intent.CATEGORY_DEFAULT);
activity.startActivityForResult(intent, 1);
}
}
@TestI'm betting here that the Intent object has an equals method based on that constructor argument. But running this test fails horribly. Now, I should note that in RCP fashion I've created a separate project to hold my unit tests. So the test project (in Eclipse) depends on the "real" project. Both projects have the android buildSpec and nature.
public void onClick_WithRealIntent() throws Exception {
Activity mockActivity = Mockito.mock(Activity.class);
View mockView = Mockito.mock(View.class);
MyOnclickListener testObject = new MyOnclickListener(mockActivity);
testObject.onClick(mockView);
Intent expectedIntent = new Intent("com.google.zxing.client.android.SCAN");
Mockito.verify(mockActivity).startActivityForResult(expectedIntent, 1);
}
#So I poke around after the crash file reveals little to no details of what happened. And low, and behold, there is evidence that android junit run configurations fail by default. So we have to edit the run configuration as suggested and try again. I'll point out that I'm not exactly as they suggest. I'm using Junit4, my bootstrap classpath is only the JRE library, and I added android.jar to the UserEntries under the default classpath.
# An unexpected error has been detected by Java Runtime Environment:
#
# Internal Error (434C41535326494C453041525345520E4350500B65), pid=14086, tid=3084753808
#
# Java VM: Java HotSpot(TM) Server VM (1.6.0_03-b05 mixed mode)
# An error report file with more information is saved as hs_err_pid14086.log
#
# If you would like to submit a bug report, please visit:
# http://java.sun.com/webapps/bugreport/crash.jsp
#
java.lang.RuntimeException: Stub!Argh! What's that mean! We could take a look at the android source code to see what happens on Intent.java:27. Our implementation class will still have to call "Intent intent = new Intent("a.unique.string");". Now comes in another habit picked up from the day job. I'm still not sure if this habit is a good one. When dealing with the JMock tests for the projects server there is a series of "provider" classes. These are really simple factory pattern classes that don't have any logic, they just new up an object, they they provide instead of factor(y). Here's what I mean:
at android.content.Intent.<init>(Intent.java:27)
...
public class MyOnclickListener implements OnClickListener {That package visible constructor is for our test. Now our test can use a mock Intent because the test is going to provide a mock IntentProvider:
private final Activity activity;
private final IntentProvider intentProvider;
public MyOnclickListener(Activity activity) {
this(activity, new IntentProvider());
}
MyOnclickListener(Activity activity, IntentProvider intentProvider) {
this.activity = activity;
this.intentProvider = intentProvider;
}
public void onClick(View view) {
Intent intent = intentProvider.provideIntent();
activity.startActivityForResult(intent, 1);
}
static class IntentProvider {
public Intent provideIntent() {
Intent intent = new Intent("a.unique.string");
intent.addCategory(Intent.CATEGORY_DEFAULT);
return intent;
}
}
@Testand now we have a green bar. But at what cost? This is an important question. We just created a constructor and an inner class JUST FOR THE TEST. Those two elements doubled the amount of code in MyOnClickListener! This is not the end. I'll contiue to analyze this issue.
public void onClick_startsActivity_WithTheRightIntent() throws Exception {
Activity mockActivity = Mockito.mock(Activity.class);
Intent mockIntent = Mockito.mock(Intent.class);
Mockito.when(mockIntentProvider.provideIntent()).thenReturn(mockIntent);
MyOnclickListener testObject = new MyOnclickListener(mockActivity, mockIntentProvider);
View mockView = Mockito.mock(View.class);
testObject.onClick(mockView);
Mockito.verify(mockActivity).startActivityForResult(mockIntent, 1);
}
7 comments:
I liked your post. I'm trying to figure out myself how I can mock objects when using android.test classes for testing. You are mocking it, but without using the Android testing framework, so I'm guessing you don't run (execute) your test as Android App but as JUnit test, am I right?
you are correct. I have not gotten Mockito to run inside an Android test.
http://mcondev.wordpress.com/2010/06/09/java-lang-runtimeexception-stub-at-junit-framework-testsuite-testsuite-java7-at-org-junit-internal-runners-junit38classrunner-junit38classrunner-java67-at-org-junit-internal-builders-junit3/
How does this test that you have the right Intent though? I know it wasn't right, but your first attempt at the test at least tried to check that the Intent was the right kind. How are you checking that in your solution?
First attempt at test tried to check that the intent was a zxing scan intent, but I don't see that happening in the final solution.
Intent expectedIntent = new Intent("com.google.zxing.client.android.SCAN");
Ahhh, but it doesn't . The idea here was to try to use verify a Mock object interaction. Which was expensive in lines of code. I don't do it this way anymore. I recommend you dig into Robolectric. https://github.com/robolectric/robolectric/blob/master/robolectric/src/test/java/org/robolectric/shadows/ShadowActivityTest.java#L149
Thanks, awesome to see that you help out even on questions related to old posts! I will check this out.
Post a Comment