1
Android Studio 4.0.1
Robolectric 4.3.1

I am writing a robolectric test and the test will fail with a error below. And seems to be related to the TextInputLayout that can't be inflated. If I remove the TextInputLayout the test will pass ok.

This is the error message I am getting.

android.view.InflateException: Binary XML file line #47: Binary XML file line #47: Error inflating class com.google.android.material.textfield.TextInputLayout
Caused by: android.view.InflateException: Binary XML file line #47: Error inflating class com.google.android.material.textfield.TextInputLayout
Caused by: java.lang.IllegalArgumentException: The style on this component requires your app theme to be Theme.AppCompat (or a descendant).

The test class itself

@Config(sdk = [Build.VERSION_CODES.O_MR1])
@RunWith(AndroidJUnit4::class)
class CharitiesAdapterTest {

    private lateinit var charitiesAdapter: CharitiesAdapter

    @Before
    fun setUp() {
        charitiesAdapter = CharitiesAdapter()
    }

    @Test
    fun `should create viewHolder`() {
        // Act & Assert
        assertThat(createViewHolder()).isNotNull
    }

    private fun createViewHolder(): CharitiesViewHolder {
        val constraintLayout = ConstraintLayout(ApplicationProvider.getApplicationContext())

        return charitiesAdapter.onCreateViewHolder(constraintLayout, 0)
    }
}

The actual adapter under test

class CharitiesAdapter : RecyclerView.Adapter<CharitiesViewHolder>() {

    private val charitiesList: MutableList<Charity> = mutableListOf()
    private var selectedCharity: (Charity) -> Unit = {}

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharitiesViewHolder {
        val charitiesViewHolder = CharitiesViewHolder(
            LayoutInflater.from(parent.context).inflate(R.layout.charity_item, parent, false))

        return charitiesViewHolder
    }

    override fun getItemCount(): Int = charitiesList.count()

    override fun onBindViewHolder(holder: CharitiesViewHolder, position: Int) {
        // left blank
    }
}

This is the part of the layout that is having the issue. When removing just the TextInputLayout the test will pass OK

 <com.google.android.material.textfield.TextInputLayout
        android:id="@+id/textInputLayoutPostcode"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/ivLogo">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/editTextPostcode"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </com.google.android.material.textfield.TextInputLayout>
    

This is the style I am using

    <resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>
</resources>

Thanks for any advice

Martin Zeitler
  • 1
  • 19
  • 155
  • 216
ant2009
  • 27,094
  • 154
  • 411
  • 609

2 Answers2

2

You could configure your tests to use a mock application that uses the app theme as shown below:

Kotlin Implementation

class TestApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        setTheme(R.style.AppTheme) //or just R.style.Theme_AppCompat
    }
}

Then configure your tests to use this mock application

@Config(application=TestApplication::class, sdk = [Build.VERSION_CODES.O_MR1])
@RunWith(AndroidJUnit4::class)
class CharitiesAdapterTest {

Java Implementation

public class TestApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        setTheme(R.style.AppTheme); //or just R.style.Theme_AppCompat
    }
}

Then configure your tests to use this mock application

@Config(application=TestApplication.class, sdk = [Build.VERSION_CODES.O_MR1])
@RunWith(AndroidJUnit4.class)
class CharitiesAdapterTest {
ggordon
  • 9,790
  • 2
  • 14
  • 27
  • Not sure if setting a theme for the application would change anything, because it has no views (the application doesn't have a theme because it doesn't require a theme without any views to style). `R.style.AppTheme` is rather the default theme for all `Activity`, but not for `Application`. – Martin Zeitler Sep 28 '20 at 22:14
  • So what if there are two `Activity`, one which uses `Theme.MaterialComponents.Light.DarkActionBar` or `NoActionBar` and the other, which uses a theme that doesn't work. How would your test-approach be able to detect the issue? See my approach for testing with the actual theme(s), as defined in `AndroidManifest.xml`. – Martin Zeitler Sep 28 '20 at 22:25
  • Thanks for the follow up questions, I tested this locally, with and without the modifications before posting to ensure that it worked. It seems to just require the theme and no activity. – ggordon Sep 29 '20 at 00:46
  • He is testing the Charities Adapter, not how the Adapter works in a particular Activity. He could write a separate test for that activity and that test would reveal any issues related to that activity and how the adapter performs in the activity thus enabling separation of concerns. – ggordon Sep 29 '20 at 00:48
  • @ggordon That is correct. I am only testing the CharitiesAdapter as a stand-only unit test. I have removed the unimportant logic of binding the views to some mock data, as I wanted to keep the snippets short for this question. I have tested like this before with other adapters. However, I have never tested an adapter that has a `TextInputLayout` in its layout. – ant2009 Sep 29 '20 at 05:32
  • More than one component isn't a unit-test anymore, but already an integration test; especially if it loads records from SQLite (which would also need to be mocked, to break that dependency). One can already see by the runner, that it's an integration test (on device) and not a local unit test (that's why I've retagged the question). Generally it's directory `androidTest` vs. directory `test`, which tells what it is. – Martin Zeitler Sep 29 '20 at 18:31
1

You are passing the wrong Context class: ApplicationProvider.getApplicationContext().

The ApplicationContext has no theme at all, using an Activity's Context suggested (which will always have the current Activity's theme, no matter which Activity or Theme it is). By the code provided it is unclear how you start the Activity (there the Context should be accessible).

Testing with an ActivityScenario or ActivityScenarioRule might be the proper way to do it.
The documentation shows how it works: AndroidX Test & JUnit4 rules with AndroidX Test.

Martin Zeitler
  • 1
  • 19
  • 155
  • 216
  • 1
    This is just an recyclerview adapter test. So I am not actually starting any activity just using robolectric to inflate the layout. Thanks – ant2009 Sep 29 '20 at 00:15
  • Mocking that `Context` is problematic, because the test might pass, even if the app would crash. When running integration tests, it is save to assume that the adapter works, while there are items available. When testing more than one component (which you do), this isn't unit testing anymore (while it is rather pointless to unit-test Google's libraries, which are commonly well-tested already). This would be something whole different, when you'd test a custom implementation of such a component. – Martin Zeitler Sep 29 '20 at 18:16