woensdag 12 februari 2014

Robolectric - BroadcastReceiver and IntentService testing

One of the added values on mobile apps is the ability to send Push notifications from a back-end application to a specific device (i.e. Push message) or multiple devices (i.e. Broadcast message).

When implementing a form of Push or Broadcast messaging, you generally require two components: a BroadcastReceiver and an IntentService, which we kindly submit to rigorous unit testing.

Testing the BroadcastReceiver

In this example, we just created a BroadcastReceiver that dispatches the intent to the IntentService for further processing.

MyBroadcastReceiver

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;

import be.acuzio.mrta.service.MyBroadcastIntentService;


public class MyBroadcastReceiver extends BroadcastReceiver {
    private final static String TAG = MyBroadcastReceiver.class.getSimpleName();

    @Override
    public void onReceive(Context context, Intent intent) {
        Log.d(TAG, "onReceive was triggered");

        Intent service = new Intent(context, MyBroadcastIntentService.class);
        service.putExtra("ACTION", intent.getStringExtra("PERFORM"));

        //start the service which needs to handle the intent
        context.startService(service);
    }
}

In the application manifest we defined the receiver to listen to incoming GCM messages, but you might even have BroadcastReceivers listening to local broadcast intents.

ApplicationManifest

      
        ...
        <receiver android:name=".receiver.MyBroadcastReceiver" 
                     android:permission="com.google.android.c2dm.permission.SEND">
            <intent-filter>
                <action android:name="com.google.android.c2dm.intent.RECEIVE" />
                <category android:name="be.acuzio.mrta" />
            </intent-filter>
        </receiver>
        ...


MyBroadcastReceiverTest

  
package be.acuzio.mrta.test.receiver;

import android.content.BroadcastReceiver;
import android.content.Intent;

import junit.framework.Assert;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.shadows.ShadowApplication;
import org.robolectric.shadows.ShadowLog;

import java.util.List;

import be.acuzio.mrta.receiver.MyBroadcastReceiver;
import be.acuzio.mrta.service.MyBroadcastIntentService;

/**
 * Created by vandekr on 12/02/14.
 */

@RunWith(RobolectricTestRunner.class)
public class MyBroadcastReceiverTest {
    static {
        // redirect the Log.x output to stdout. Stdout will be recorded in the test result report
        ShadowLog.stream = System.out;
    }

    @Before
    public void setup() {}

    /**
     * Let's first test if the BroadcastReceiver, which was defined in the manifest, is correctly
     * load in our tests
     */
    @Test
    public void testBroadcastReceiverRegistered() {
        List<ShadowApplication.Wrapper> registeredReceivers = Robolectric.getShadowApplication().getRegisteredReceivers();

        Assert.assertFalse(registeredReceivers.isEmpty());

        boolean receiverFound = false;
        for (ShadowApplication.Wrapper wrapper : registeredReceivers) {
            if (!receiverFound)
                receiverFound = MyBroadcastReceiver.class.getSimpleName().equals(
                                         wrapper.broadcastReceiver.getClass().getSimpleName());
        }

        Assert.assertTrue(receiverFound); //will be false if not found
    }

    @Test
    public void testIntentHandling() {
    /** TEST 1
         ----------
         We defined the Broadcast receiver with a certain action, so we should check if we have
         receivers listening to the defined action
         */
        Intent intent = new Intent("com.google.android.c2dm.intent.RECEIVE");

        ShadowApplication shadowApplication = Robolectric.getShadowApplication();
        Assert.assertTrue(shadowApplication.hasReceiverForIntent(intent));

        /**
         * TEST 2
         * ----------
         * Lets be sure that we only have a single receiver assigned for this intent
         */
        List<broadcastreceiver> receiversForIntent = shadowApplication.getReceiversForIntent(intent);

        Assert.assertEquals("Expected one broadcast receiver", 1, receiversForIntent.size());

        /**
         * TEST 3
         * ----------
         * Fetch the Broadcast receiver and cast it to the correct class.
         * Next call the "onReceive" method and check if the MyBroadcastIntentService was started
         */
        MyBroadcastReceiver receiver = (MyBroadcastReceiver) receiversForIntent.get(0);
        receiver.onReceive(Robolectric.getShadowApplication().getApplicationContext(), intent);

        Intent serviceIntent = Robolectric.getShadowApplication().peekNextStartedService();
        Assert.assertEquals("Expected the MyBroadcast service to be invoked",
                MyBroadcastIntentService.class.getCanonicalName(),
                serviceIntent.getComponent().getClassName());

    }
}


Testing the IntentService

Testing IntentServices is a somewhat more complicated matter. The current implementation of Robolectric does not invoke the "onHandleIntent", ergo nothing happens. A way to fix this, is to stub or mock your IntentService class in your tests and override the "onHandleIntent(Intent intent)" method to broaden its scope from protected to public.

Because of the introduction of Mock objects in our project, we need to update the build.gradle file to exclude all files that are not named "Test" from the TestRunner. Add  this snippet to the bottom of your build.gradle file.

// prevent the "superClassName is empty" error for classes not annotated as tests
tasks.withType(Test) {
    scanForTestClasses = false
    include "**/*Test.class" // whatever Ant pattern matches your test class files
}

As mentioned in the comment, if you do not add this snippet, you will get these nasty "superClassName is empty" exceptions, which can give you a true run for your money.

MyBroadcastIntentService

import android.app.IntentService;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
import android.util.Log;

import be.acuzio.mrta.MainActivity;
import be.acuzio.mrta.R;

public class MyBroadcastIntentService extends IntentService {
    private final static String TAG = MyBroadcastIntentService.class.getSimpleName();
    public final static int NOTIFICATION_ID = 335446435;
    public final static String NOTIFICATION_TAG = "BNTAG_ACTION";

    public MyBroadcastIntentService() {
        super(MyBroadcastIntentService.class.getSimpleName());
        Log.d(TAG, "Creating new instance of MyBroadcastIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        Log.d(TAG, "onHandleIntent was called");

        Bundle extras = intent.getExtras();

        if (extras != null && !extras.isEmpty()) {  // has effect of unparcelling Bundle
            Log.d(TAG, "Extras were found");

            String action = intent.getStringExtra("ACTION");

            this.sendNotification(action);
        }
    }

    private void sendNotification(String action) {
        Log.d(TAG, "Sending notification");

        NotificationManager notificationManager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);

        //set-up the action for authorizing the action
        Intent intent = new Intent(getApplicationContext(), MainActivity.class);

        PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), 1, intent, PendingIntent.FLAG_UPDATE_CURRENT);


        NotificationCompat.Builder builder =
                new NotificationCompat.Builder(this)
                        .setSmallIcon(R.drawable.ic_launcher)
                        .setContentTitle(this.getString(R.string.app_name))
                        .setAutoCancel(Boolean.TRUE)
                        .setContentText("You are going to " + action);


        builder.setContentIntent(pendingIntent);

        notificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, builder.build());
    }
}



MyBroadcastIntentServiceTest
Our IntentService test will have to test cases: one without a bundle (which should display no notification) and one with a correctly provided bundle (which does display 1 notification).

In the latter case we will also verify that the text (displayed in the notification) contains "You are going to eat an apple", which contains the actual action that was sent using the intent in the test case.

import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;

import junit.framework.Assert;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.shadows.ShadowLog;
import org.robolectric.shadows.ShadowNotification;
import org.robolectric.shadows.ShadowNotificationManager;

import be.acuzio.mrta.service.MyBroadcastIntentService;


@RunWith(RobolectricTestRunner.class)
public class MyBroadcastIntentServiceTest {
    static {
        ShadowLog.stream = System.out;
    }

    @Before
    public void setup() {}

    @Test
    public void testNoBundleExtrasFound() {
        Intent serviceIntent = new Intent(Robolectric.application, MyBroadcastIntentServiceMock.class);
        NotificationManager notificationManager = (NotificationManager) Robolectric.application.getSystemService(Context.NOTIFICATION_SERVICE);

        //Robolectric.getShadowApplication().startService(serviceIntent);
        MyBroadcastIntentServiceMock service = new MyBroadcastIntentServiceMock();
        service.onCreate();
        service.onHandleIntent(serviceIntent);

        Assert.assertEquals("Expected no notifications", 0, Robolectric.shadowOf(notificationManager).size());
    }

    @Test
    public void testWithBundleExtrasFound() {
        Intent serviceIntent = new Intent(Robolectric.application, MyBroadcastIntentServiceMock.class);
        Bundle bundle = new Bundle();
        bundle.putString("ACTION", "eat an apple");
        serviceIntent.putExtras(bundle);

        NotificationManager notificationManager = (NotificationManager) Robolectric.application.getSystemService(Context.NOTIFICATION_SERVICE);

        //Robolectric.getShadowApplication().startService(serviceIntent);
        MyBroadcastIntentServiceMock service = new MyBroadcastIntentServiceMock();
        service.onCreate();
        service.onHandleIntent(serviceIntent);


        ShadowNotificationManager manager = Robolectric.shadowOf(notificationManager);
        Assert.assertEquals("Expected one notification", 1, manager.size());

        Notification notification = manager.getNotification(MyBroadcastIntentService.NOTIFICATION_TAG, MyBroadcastIntentService.NOTIFICATION_ID);
        Assert.assertNotNull("Expected notification object", notification);

        ShadowNotification shadowNotification = Robolectric.shadowOf(notification);
        Assert.assertNotNull("Expected shadow notification object", shadowNotification);

        Assert.assertEquals("You are going to eat an apple", shadowNotification.getLatestEventInfo().getContentText());
    }

    class MyBroadcastIntentServiceMock extends MyBroadcastIntentService {
        @Override
        public void onHandleIntent(Intent intent) {
            super.onHandleIntent(intent);
        }
    }
}


The test class contains the MyBroadcastIntentServiceMock (might deserve a better name) which extends the MyBroadcastIntentService. By overriding and exposing the onHandleIntent(Intent intent) method we are able to actually test the code.

Geen opmerkingen:

Een reactie plaatsen