Monday, May 16, 2011

Getting things done?

Today, we're going to attack the testing I'm trying to from a standpoint of ActivityUnitTestCase. The reason I'm kind of forced into doing this is that the QuestionActivity is getting its value to test with from a subclass of the Application object, and that is getting regenerated with random values when I run my test. So, I need to mock and inject the application object with fixed values to get a repeatable test. That's where ActivityUnitTestCase comes in. So today, without going down a rathole, I want to take a brief look at how this works.

Here's some information:

"This class provides isolated testing of a single activity. The activity under test will be created with minimal connection to the system infrastructure, and you can inject mocked or wrappered versions of many of Activity's dependencies. Most of the work is handled automatically here by setUp() and tearDown().

I found an example on SO:

public class ActivityTester extends ActivityUnitTestCase (brackets T brackets) {
public ActivityTester() {
super(A.class);
}

public void testOne() {
Intent intent = new Intent(getInstrumentation().getTargetContext(), A.class);
// This fails with my IllegalStateException
startActivity(intent, null, null);
}
}


Let's modify this for our question class

public class QuestionActivityTester extends ActivityUnitTestCase {
public QuestionActivityTester() {
super(QuestionActivity.class);
}

public void testOne() {
Intent intent = new Intent(getInstrumentation().getTargetContext(), QuestionActivity.class);
startActivity(intent, null, null);
}
}


And run it.

So, this initial test succeeds. So, let's add this:

public void testOne() {
Intent intent = new Intent(getInstrumentation().getTargetContext(),
QuestionActivity.class);

QuestionActivity qa = startActivity(intent, null, null);
TextView mQuestion = (TextView) qa.findViewById(R.id.display_question);
Assert.assertEquals("青", mQuestion.getText().toString());
}


This crashes on a null pointer exception. Perusal of logcat doesn't reveal any trace statement from QuestionActivity.

The doc obtained via Eclipse shows this:

"QuestionActivity android.test.ActivityUnitTestCase.startActivity(Intent intent, Bundle savedInstanceState, Object lastNonConfigurationInstance)

protected T startActivity (Intent intent, Bundle savedInstanceState, Object lastNonConfigurationInstance)
Since: API Level 1

Start the activity under test, in the same way as if it was started by Context.startActivity(), providing the arguments it supplied. When you use this method to start the activity, it will automatically be stopped by tearDown().

This method will call onCreate(), but if you wish to further exercise Activity life cycle methods, you must call them yourself from your test case.

Do not call from your setUp() method. You must call this method from each of your test methods.

Parameters
intent The Intent as if supplied to startActivity(Intent).
savedInstanceState The instance state, if you are simulating this part of the life cycle. Typically null.
lastNonConfigurationInstance This Object will be available to the Activity if it calls getLastNonConfigurationInstance(). Typically null.
Returns
Returns the Activity that was created"

The key phrase is "This method will call onCreate(), but if you wish to further exercise Activity life cycle methods, you must call them yourself from your test case."

In fact, I'm using the onResume method, the reason being to ensure the state is set properly when returning from the activity being killed.

So if we change the method to this:

public void testOne() {
Intent intent = new Intent(getInstrumentation().getTargetContext(),
QuestionActivity.class);

QuestionActivity qa = startActivity(intent, null, null);
qa.onResume();
TextView mQuestion = (TextView) qa.findViewById(R.id.display_question);
Assert.assertEquals("青", mQuestion.getText().toString());
}

We get an index-out-of-bounds exception. That's progress. So, this is where the stand-alone aspect of this class comes in - it doesn't get the usual framework, so, it requires some more setup. Let's find out where this crashed.

It's in the "incrementQuestion" method

This is called if the initial question is zero. However, if we add the setApplication:

public void testOne() {
Intent intent = new Intent(getInstrumentation().getTargetContext(),
QuestionActivity.class);

AppState appState = new AppState();
appState.jlptLevel = 5;
appState.currentQuestion = 2;
setApplication(appState);

QuestionActivity qa = startActivity(intent, null, null);
qa.onResume();
TextView mQuestion = (TextView) qa.findViewById(R.id.display_question);
Assert.assertEquals("青", mQuestion.getText().toString());
}


The same result ensues. Clearly, I'm having the same problem as before, i.e. the app object isn't the same. Let's check out the source of ActivityUnitTestCase. http://tinyurl.com/6ylo5sh


Ah ha. Here are some methods of interest:

Set the application for use during the test. You must call this function before calling startActivity(android.content.Intent,android.os.Bundle,java.lang.Object). If your test does not call this method,
Parameters:
application The Application object that will be injected into the Activity under test.
182
183 public void setApplication(Application application) {
184 mApplication = application;
185 }


If you wish to inject a Mock, Isolated, or otherwise altered context, you can do so here. You must call this function before calling startActivity(android.content.Intent,android.os.Bundle,java.lang.Object). If you wish to obtain a real Context, as a building block, use getInstrumentation().getTargetContext().
191
192 public void setActivityContext(Context activityContext) {
193 mActivityContext = activityContext;
194 }


Note the application is not getting set into the context; it actually sets in into the activity. I've always gotten may application from the context.

Let's change the code to get the application from the activity.

Ok that's failing, but for a different reason - some other setup work hasn't been done.

If I move that setup to get the application, instead of GetApplicationContext, then I start to get a consistently blank result on the fail, which means I probably am getting it to work and just pointing it at the wrong question - with no kanji.

If I change the question number to a different one, it comes up with this:

junit.framework.ComparisonFailure: expected:<青> but was:<火>

So, I'm still getting the problem, but I've narrowed it down, because the display of the application parameters inside the application is now the same as the one's that I've been setting.

I changed the QuestionActivity to put back an a call to init, which sets some unrelated parameters on app object, and which I had temporarily commented out. I also changed the init routine to get the application object.

When I rerun, I'm getting a null pointer exception. The interesting thing is, the exception is happening once it gets past pulling up the data from the database, and instead fails in a word-grouping routine. It's consistently pulling up data from the same row, in accordance with the mocked application object. So it's just a question if getting enough setup to get this to run.

I'm trying to figure out where exactly it crashed, but my dumps to console aren't showing up once I get out of question activity.
Tracing through a debug, it turns out that theres a wordGrouping list I have setup which needs to be initialized.

To make the test work, I might check it for null and return a null - not that doesn't work. What I need to do is either initialize this wordGrouping hash, or make the test check against something other than the display.

Well, I stubbornly called the method to initialize the hash, InitUtils.initializeQuestions(qa, appState):

public void testOne() {
Intent intent = new Intent(getInstrumentation().getTargetContext(),
QuestionActivity.class);

AppState appState = new AppState();
appState.jlptLevel = 5;
appState.currentQuestion = 3;
setApplication(appState);
QuestionActivity qa = startActivity(intent, null, null);
InitUtils.initializeQuestions(qa, appState);
qa.onResume();
TextView mQuestion = (TextView) qa.findViewById(R.id.display_question);
Assert.assertEquals("青", mQuestion.getText().toString());
}


but the result is unexpected:

junit.framework.ComparisonFailure: expected:<...> but was:<...い>

Huh?

And here is my assert question:

Assert.assertEquals("青", mQuestion.getText().toString());

Let's try this experiment:

Assert.assertEquals("xxx", mQuestion.getText().toString());

The result:

junit.framework.ComparisonFailure: expected: but was:<青い>

Actually, :青い" is correct, not "青", that was left over since I had changed the lookup number previously.

So, change it to this:

Assert.assertEquals("青い", mQuestion.getText().toString());

And - at last - we get a pass. Was it worth it? I'm not sure. Multiple hours went into trying to get it to work with both ActivityInstrumentationTestCase2 and then ActivityUnitTestCase. But I did learn about the ActivityUnitTestCase class, which may be what I need for the next test.

One other thing I should do is change all the source code to use getApplication from the activity, instead of the context, just to keep things consistent. But first, let's commit our changes.

Ok. The next post will have to do with cleaning up some annoying database not closed methods.

No comments:

Post a Comment