Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to mock a Android NFC Tag object for unit testing

I'm currently working on an Android project which needs NFC integration. Now I want to write some (j)unit tests to see if the application can receive NFC intents (specifically ACTION_TECH_DISCOVERED) and put the given tag (in NfcAdapter.EXTRA_TAG) on a bus system. However to my surprise I cannot create a Tag instance or mock one. Can someone explain to me how I can (unit) test?

At this point I would even accept a form of integration testing, the process of:

  1. Detect the NFC Intent
  2. Get the Tag object
  3. Put it on the bus wrapped in a CardDetectedEvent.

I have a phone with NFC enabled and a few cards for testing.

Android SDK version: 19
Libraries used: robolectric, junit and mockito

like image 873
Jaap Oudejans Avatar asked Jun 15 '15 09:06

Jaap Oudejans


1 Answers

It's possible to create a mock tag object instance using reflection (note that this is not part of the public Android SDK, so it might fail for future Android versions).

  1. Get the createMockTag() method though reflection:

    Class tagClass = Tag.class;
    Method createMockTagMethod = tagClass.getMethod("createMockTag", byte[].class, int[].class, Bundle[].class);
    
  2. Define some constants for preparing the mock tag instance:

    final int TECH_NFC_A = 1;
    final String EXTRA_NFC_A_SAK = "sak";    // short (SAK byte value)
    final String EXTRA_NFC_A_ATQA = "atqa";  // byte[2] (ATQA value)
    
    final int TECH_NFC_B = 2;
    final String EXTRA_NFC_B_APPDATA = "appdata";    // byte[] (Application Data bytes from ATQB/SENSB_RES)
    final String EXTRA_NFC_B_PROTINFO = "protinfo";  // byte[] (Protocol Info bytes from ATQB/SENSB_RES)
    
    final int TECH_ISO_DEP = 3;
    final String EXTRA_ISO_DEP_HI_LAYER_RESP = "hiresp";  // byte[] (null for NfcA)
    final String EXTRA_ISO_DEP_HIST_BYTES = "histbytes";  // byte[] (null for NfcB)
    
    final int TECH_NFC_F = 4;
    final String EXTRA_NFC_F_SC = "systemcode";  // byte[] (system code)
    final String EXTRA_NFC_F_PMM = "pmm";        // byte[] (manufacturer bytes)
    
    final int TECH_NFC_V = 5;
    final String EXTRA_NFC_V_RESP_FLAGS = "respflags";  // byte (Response Flag)
    final String EXTRA_NFC_V_DSFID = "dsfid";           // byte (DSF ID)
    
    final int TECH_NDEF = 6;
    final String EXTRA_NDEF_MSG = "ndefmsg";              // NdefMessage (Parcelable)
    final String EXTRA_NDEF_MAXLENGTH = "ndefmaxlength";  // int (result for getMaxSize())
    final String EXTRA_NDEF_CARDSTATE = "ndefcardstate";  // int (1: read-only, 2: read/write, 3: unknown)
    final String EXTRA_NDEF_TYPE = "ndeftype";            // int (1: T1T, 2: T2T, 3: T3T, 4: T4T, 101: MF Classic, 102: ICODE)
    
    final int TECH_NDEF_FORMATABLE = 7;
    
    final int TECH_MIFARE_CLASSIC = 8;
    
    final int TECH_MIFARE_ULTRALIGHT = 9;
    final String EXTRA_MIFARE_ULTRALIGHT_IS_UL_C = "isulc";  // boolean (true: Ultralight C)
    
    final int TECH_NFC_BARCODE = 10;
    final String EXTRA_NFC_BARCODE_BARCODE_TYPE = "barcodetype";  // int (1: Kovio/ThinFilm)
    
  3. Create the tech-extras bundle for your tag type. For instance, for an NFC-A tag with an NDEF message:

    Bundle nfcaBundle = new Bundle();
    nfcaBundle.putByteArray(EXTRA_NFC_A_ATQA, new byte[]{ (byte)0x44, (byte)0x00 }); //ATQA for Type 2 tag
    nfcaBundle.putShort(EXTRA_NFC_A_SAK , (short)0x00); //SAK for Type 2 tag
    
    Bundle ndefBundle = new Bundle();
    ndefBundle.putInt(EXTRA_NDEF_MAXLENGTH, 48); // maximum message length: 48 bytes
    ndefBundle.putInt(EXTRA_NDEF_CARDSTATE, 1); // read-only
    ndefBundle.putInt(EXTRA_NDEF_TYPE, 2); // Type 2 tag
    NdefMessage myNdefMessage = ...; // create an NDEF message
    ndefBundle.putParcelable(EXTRA_NDEF_MSG, myNdefMessage);  // add an NDEF message
    
  4. Prepare an anti-collision identifier/UID for your tag (see Tag.getId() method). E.g. a 7-byte-UID for a Type 2 tag:

    byte[] tagId = new byte[] { (byte)0x3F, (byte)0x12, (byte)0x34, (byte)0x56, (byte)0x78, (byte)0x90, (byte)0xAB };
    
  5. Then you can create a mock tag instance by invoking the createMockTag() method

    Tag mockTag = (Tag)createMockTagMethod.invoke(null,
            tagId,                                     // tag UID/anti-collision identifier (see Tag.getId() method)
            new int[] { TECH_NFC_A, TECH_NDEF },       // tech-list
            new Bundle[] { nfcaBundle, ndefBundle });  // array of tech-extra bundles, each entry maps to an entry in the tech-list
    

Once you created that mock tag object, you can send it as part of an NFC discovery intent. E.g. for a TECH_DISCOVERED intent:

Intent techIntent = new Intent(NfcAdapter.ACTION_TECH_DISCOVERED);
techIntent.putExtra(NfcAdapter.EXTRA_ID, tagId);
techIntent.putExtra(NfcAdapter.EXTRA_TAG, mockTag);
techIntent.putExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, new NdefMessage[]{ myNdefMessage });  // optionally add an NDEF message

You can then send this intent to your activity:

techIntent.setComponent(...); // or equivalent to optionally set an explicit receiver
startActivity(techIntent);

The receiver can even use the mock tag object to retrieve instances of the technology classes. However, any method that requires IO operations will fail.

like image 153
Michael Roland Avatar answered Oct 29 '22 10:10

Michael Roland