jmarkman446
jmarkman446

Reputation: 156

Trivial, short, and simple android unit tests throwing NullPointerException

I have an itty-bitty Android project I cobbled together to learn about Android unit and instrumented testing. I use the dog.ceo API to get a random image of a dog. The program itself works as it should. However, whenever I run or debug my unit tests, I get NullPointerExceptions on all of them, with the following stack trace for each:

java.lang.NullPointerException
    at com.jmarkman.dog.DogAPI.getDogURL(DogAPI.java:27)
    at com.jmarkman.dog.DogAPITest.getJSON(DogAPITest.java:37)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMainV2.main(AppMainV2.java:131)

This is my main (only) activity: https://gist.github.com/jmarkman/733b0a1cbeb41a1c5733c0c715e4606b

This is my class for interacting with the dog.ceo API. It's a regular old Java class without any kind of design pattern applied to it: https://gist.github.com/jmarkman/c38546d99b0ca06099175a8da68d76b6

package com.jmarkman.dog;

import android.net.Uri;
import android.util.Log;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Random;
import java.util.Scanner;

public class DogAPI
{
    private final String DOG_API_URL = "https://dog.ceo/api";

    public DogAPI() { }

    public URL getDogURL()
    {
        URL url = null;

        Uri uri = Uri.parse(DOG_API_URL).buildUpon()
                .appendPath("breeds")
                .appendPath("image")
                .appendPath("random")
                .build();

        try
        {
            url = new URL(uri.toString());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }

        return url;
    }

    public URL getDogURL(String breed, boolean random)
    {
        URL url = null;
        Uri uri;

        if (random)
        {
            uri = Uri.parse(DOG_API_URL).buildUpon()
                    .appendPath("breed")
                    .appendPath(breed)
                    .appendPath("images")
                    .appendPath("random")
                    .build();
        }
        else
        {
            uri = Uri.parse(DOG_API_URL).buildUpon()
                    .appendPath("breed")
                    .appendPath(breed)
                    .appendPath("images")
                    .build();
        }

        try
        {
            url = new URL(uri.toString());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }

        return url;
    }

    public String getJSON(URL url) throws IOException
    {
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        InputStream stream = connection.getInputStream();
        Scanner scanner = new Scanner(stream);
        scanner.useDelimiter("\\A");

        boolean hasData = scanner.hasNext();

        try
        {
            if (hasData)
            {
                return scanner.next();
            }
            else
            {
                return null;
            }
        }
        catch (Exception e)
        {
            Log.d("Error", e.toString());
            return null;
        }
        finally
        {
            connection.disconnect();
        }
    }

    public Uri getImage(String json)
    {
        final String MESSAGE = "message";
        Random random = new Random();
        Uri imageURL = null;

        try
        {
            JSONObject imageObj = new JSONObject(json);
            Object imageMsg = imageObj.get(MESSAGE);
            if (imageMsg instanceof String)
                imageURL = Uri.parse(imageMsg.toString());
            else if (imageMsg instanceof JSONArray)
            {
                int urlIndex = random.nextInt(((JSONArray) imageMsg).length() + 1);
                imageURL = Uri.parse(((JSONArray) imageMsg).getString(urlIndex));
            }
        }
        catch (JSONException jse)
        {
            jse.printStackTrace();
        }

        return imageURL;
    }
}

The instrumented tests run without issue. My unit tests aren't overly complicated; they're even shorter than the ones in the Pluralsight course I watched about Android testing. The following is every line in my test file, verbatim:

package com.jmarkman.dog;

import android.net.Uri;
import org.junit.Test;
import java.net.URL;
import static org.junit.Assert.*;

public class DogAPITest {
    @Test
    public void getDogURL() throws Exception
    {
        DogAPI dog = new DogAPI();
        String expectedURL = "https://dog.ceo/api/breeds/image/random";
        String actualURL = dog.getDogURL().toString();

        assertEquals(expectedURL, actualURL);
    }

    @Test
    public void getDogURLForBreed() throws Exception
    {
        DogAPI dog = new DogAPI();
        String url = dog.getDogURL("corgi", true).toString();

        assertEquals("https://dog.ceo/api/breed/corgi/images/random", url);

    }

    @Test
    public void getJSON() throws Exception
    {
        DogAPI dog = new DogAPI();
        URL url = dog.getDogURL();
        String returnJSON = dog.getJSON(url);

        assertNotNull(returnJSON);
    }

    @Test
    public void getImage() throws Exception
    {
        DogAPI dog = new DogAPI();
        URL url = dog.getDogURL();
        String returnJSON = dog.getJSON(url);

        Uri uri = dog.getImage(returnJSON);

        assertNotNull(uri);
    }
}

I have unitTests.returnDefaultValues = true in my build.gradle file. I've gone to the build menu and cleaned and rebuilt the project. Yes, I read the "What is a NullPointerException" question already. What am I missing?

Upvotes: 0

Views: 1973

Answers (1)

Michael Dodd
Michael Dodd

Reputation: 10270

Your DogAPI class (specifically the getDogURL() method) makes use of android.net.Uri.parse(), which is in a class specific to the Android framework. Android-specific classes are not available within local JUnit tests, and are present as just stubs if you enable unitTests.returnDefaultValues = true. From the documentation:

If you run a test that calls an API from the Android SDK that you do not mock, you'll receive an error that says this method is not mocked. That's because the android.jar file used to run unit tests does not contain any actual code (those APIs are provided only by the Android system image on a device).

You've got two options. First is to use the Robolectric testing library, which provides functional versions of Android classes for running in local JUnit tests. This would only need the addition of @RunWith(RobolectricTestRunner.class) at the top of your test.

Alternatively, use a different Uri parser, such as java.net.URL, which coincidentally also has a toURI() method.

Upvotes: 4

Related Questions