Coding with Titans

so breaking things happens constantly, but never on purpose

HowTo: Using assets in Android unit-test project

You have probably heard already a tremendous number of times that unit-testing your Android code is important. It’s all true and still valid. And there is one recipe I tend to forget quite often, which I also found extremely useful, while writing tests exercising (or relying onto) any external data.

In this scenario, all the content is kept outside of the unit-test itself and is simply read at runtime from resources (aka assets). It gives much better clarity as usually as moving it away of Java/Kotlin source code also avoids adding lots of escaping characters. Then once this content is loaded, it can be validated using AAA Fundamentals ➡️ some model could be deserialized from text representation and used to execute predefined action. In some pseudo-Java code it could look like following:

public final class ModelTests {

    @Test
    public void test_verify_correct_model_deserialization() {

        // Arrange

        final String data123 = ""; // somehow load the data from "data123.json" file
        assertNotNull(data123);

        // deserialize from JSON to construct an object-model structure
        final Data123Model model = new GsonBuilder().create().fromJson(data123, Data123Model.class);
        assertNotNull(model);

        // Action

        // Assert
    }
}

Now, depending on how to run this test, there are two distinct approaches:

  1. on simulator / real device - that means we can use any Android API to load the file. It all will be handled as Android Instrumented Test in Android Studio. This will also have a tendency to fail, when there is no simulator available.

  2. on the build machine itself - that means test will be executed with full-native speed using JUnit and without any other additional dependencies or abstraction layers.

So far, so good. Let’s see, how to achieve it in both configurations.

Setup

Before we move to the actual tests, let’s try to add some resources into the project. It’s common for both presented further ways of running the test. Please be patient, this shouldn’t take long.

  1. Add assets folder into the library project.

    Right click on the project node then New > Folder > Assets Folder.

    Add assets folder

  2. Remember however to use test custom location, so those files are only visible when running unit-tests.

    Test asset location

    The library’s build.gradle file should point to the assets folder, yet it should be different for both flavors (main and test).

    android {
        // ... other definitions remove for clarity ...
    
        sourceSets {
            main {
                assets {
                    srcDirs 'src\\main\\assets'
                }
            }
    
            test {
                resources {
                    srcDirs 'src\\test\\assets'
                }
            }
        }
    }
    
  3. Now add a data123.json file into the project.

    Make a double check, that it is physically located under src\test\assets directory, so it’s not included in the application release bundle and used only during tests.

    data123.json

    It’s content could be as simple as following:

    {
        "data": 123
    }
    

Android Instrumented Test

Finally, write a test using it.

Remember you need a configured and running AVD at this stage to see any progress and be able to set breakpoint.

import android.content.Context;

import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import org.junit.Test;
import org.junit.runner.RunWith;

import static org.junit.Assert.assertNotNull;

@RunWith(AndroidJUnit4.class)
public final class ModelTests {
    @Test
    public void test_verify_correct_model_deserialization() {

        // Arrange
        final Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
        final String data123 = ResourceHelper.loadString(appContext, "test_data/data123.json");

        assertNotNull(data123);

        // Action ...

        // Assert ...
    }
}

Results at runtime will probably be similar to: Running instrumentation test

The missing piece is of course the ResourceHelper class, that is responsible for reading the file and returning it as String instance. In its most basic form it would look like following.

Notice that it simply returns null value in case of any errors. It exposes 2 methods. One that converts data from an input stream as UTF-8-encoded string. Second one actually gets the AssetsManager instance out of the current application context and opens a stream for given embedded resource. Name parameter carries of course the relative path of the resource to load from assets folder.

import android.content.Context;
import android.content.res.AssetManager;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public final class ResourceHelper {

    @Nullable
    public static String loadString(@NonNull Context context, @NonNull String name) {
        final AssetManager assets = context.getAssets();

        if (assets == null) {
            return null;
        }

        try {
            try (final InputStream input = assets.open(name)) {
                return loadString(input);
            }
        } catch (IOException e) {
            return null;
        }
    }

    @Nullable
    public static String loadString(@Nullable InputStream inputStream) {
        if (inputStream == null) {
            return null;
        }

        try {
            try (final ByteArrayOutputStream result = new ByteArrayOutputStream()) {
                byte[] buffer = new byte[4096];
                int length;

                while ((length = inputStream.read(buffer)) > 0) {
                    result.write(buffer, 0, length);
                }
                return result.toString("UTF-8");
            }
        } catch (IOException e) {
            return null;
        }
    }
}

Local Test

Running it locally is even simpler. The same testing assets are reused, but since we don’t run any Android app anymore, there is no need to use the application Context to access them.

Hint: Android Instrumented Test file and Local Test file must be different otherwise Android Studio will have problem understanding, which one we want to run and how.

import androidx.annotation.Nullable;
import org.junit.Test;

import static org.junit.Assert.assertNotNull;

public final class RawModelTests {
    @Test
    public void test_verify_correct_model_deserialization() {
        @Nullable
        final String data123 = ResourceHelper.loadString("test_data/data123.json");
        assertNotNull(data123);

        // rest of the test...
    }
}

Results at runtime will probably be similar to: Running local test

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public final class ResourceHelper {

    @Nullable
    public static String loadString(@NonNull String name) {
        return loadString(ResourceHelper.class.getClassLoader(), name);
    }

    @Nullable
    public static String loadString(@Nullable ClassLoader loader, @NonNull String name) {
        if (loader == null) {
            return null;
        }

        try {
            try (final InputStream inputStream = loader.getResourceAsStream(name)) {
                return loadString(inputStream);
            }
        } catch (IOException | NullPointerException e) {
            return null;
        }
    }

    @Nullable
    public static String loadString(@Nullable InputStream inputStream) {
        if (inputStream == null) {
            return null;
        }

        try {
            try (final ByteArrayOutputStream result = new ByteArrayOutputStream()) {
                byte[] buffer = new byte[4096];
                int length;

                while ((length = inputStream.read(buffer)) > 0) {
                    result.write(buffer, 0, length);
                }
                return result.toString("UTF-8");
            }
        } catch (IOException e) {
            return null;
        }
    }
}

The name of the resource is again relative within the assets folder. Notice also that this time the function loading content from input stream is identical to the one from Android Instrumented Test version. The biggest difference here is only on how the input stream is obtained. Instead of using assets-manager, the library-specific class loader is used, which is capable of opening streams for specified embedded resources.

Summary

Of course I am not the only one, who knows how to read resources for testing. There are some other very good guides and explanations here, here or project here.

I really believe it helps you too. Cheers!