6

I have an Activity that does nothing but showing a Toast message like the following.

public MyActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);

        Toast.makeText(this, "Some message", Toast.LENGTH_LONG).show();
        finish(); // Finish the activity here. 
    }
}

I want to write a unit test using Robolectric to verify the Toast content. I tried the following.

@RunWith(RobolectricTestRunner.class)
public class MyActivityTest {

    @Before
    public void setup() {
        Robolectric.buildActivity(MyActivity.class).create();
    }

    @Test
    public void testToast() {
        assertTrue(ShadowToast.showedCustomToast("Some message", R.string.some_message));
        assertThat(ShadowToast.getTextOfLatestToast().toString(), equalTo("Some message"));
    }
}

Looks like none of them are working. Any ideas?

Thanks in advance!

Reaz Murshed
  • 23,691
  • 13
  • 78
  • 98
  • This seems problematic on several levels: 1) you can send a Toast from anywhere: why create an Activity to do it? 2) Why test the Android Toast system? If it didn't work, what would you do about it? 3) Toast is a thin wrapper around Binder: it transfers the text to an entirely different application and that application prints the message. It is going to be quite difficult to Shadow that. – G. Blake Meike Dec 24 '19 at 05:08
  • did you try `ShadowToast.showedToast(CharSequence message)`? – ahasbini Dec 24 '19 at 20:32
  • @ahasbini yes, I think I tried that one too. Thank you for your comment! – Reaz Murshed Dec 24 '19 at 20:35
  • @G.BlakeMeike thank you so much for the comment. I am trying to respond here. The activity is just a dummy to provide an example. I do not want to test the Android Toast system. I want to test the content of the Toast and check if that appeared. Looks like Robolectric provides some Shadow technique, but so far none of them worked for me. – Reaz Murshed Dec 24 '19 at 20:39
  • It is going to be very hard to test the output of the Toast. That happens in a completely different process. I suggest you test the input to the routine that composes the Toast, and assume that Toast.show works. – G. Blake Meike Dec 25 '19 at 01:42
  • @G.BlakeMeike yes, that is correct. I could come up with a custom toast implementation and looks like it is working. Please check my answer below. I have also added a project in github with a working example. Please have a look if you have some time - https://github.com/masudias/toaster/ – Reaz Murshed Dec 25 '19 at 05:41
  • Using a `Snackbar` instead of a `Toast` would likely make it easier ... – Martin Zeitler Dec 27 '19 at 07:15
  • @MartinZeitler, correct. However, I have `Toast` in my case. I found a workaround and added that as an answer. You might consider having a look. Thank you! – Reaz Murshed Dec 27 '19 at 15:46
  • 2
    @ReazMurshed you might call it `Toast`, but it isn't really `Toast` anymore (as Blake explained). Only could imagine that one could get a handle to an actual `Toast` with `UiAutomator2` (which can access views outside of the scope of the application). – Martin Zeitler Dec 28 '19 at 05:04
  • @MartinZeitler Thank you for your insight! I will take a look at that. – Reaz Murshed Dec 28 '19 at 06:12

1 Answers1

1

I could manage to get a workaround for my problem and ended up having a custom layout for showing toast messages and testing the content of the toast using Robolectric as follows. I have added a GitHub project here with a working example.

public class MainActivityTest {

    @Before
    public void setup() {
        Robolectric.buildActivity(MainActivity.class).create();
    }

    @Test
    public void testToastMessage() {
        ToastLayoutBinding binding = DataBindingUtil.getBinding(ShadowToast.getLatestToast().getView());
        binding.executePendingBindings();

        assertEquals(binding.toastMsg.getContext().getString(R.string.some_toast),
                binding.toastMsg.getText());
    }

    @Test
    public void testToastDuration() {
        assertEquals(Toast.LENGTH_LONG, ShadowToast.getLatestToast().getDuration());
    }
}

The layout for the custom toast is simply as follows.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <import type="android.text.TextUtils" />

        <variable
            name="msg"
            type="String" />
    </data>

    <FrameLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:elevation="4dp">

        <TextView
            android:id="@+id/toast_msg"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:fontFamily="sans-serif"
            android:gravity="center"
            android:text="@{TextUtils.isEmpty(msg) ? @string/no_msg : msg}" />
    </FrameLayout>
</layout>

And the function for displaying the toast message is as follows.

/**
* Display a custom toast with some message provided as a function parameter
*
* @param activity An {@link Activity} where the toast message will be shown
* @param msg      a {@link String} that holds the message to be shown as a Toast
* @throws IllegalArgumentException if a null activity is passed to the function
* @apiNote If a null String is passed as a #msg parameter, then this function shows a default text (no_toast)
*/
public static void showToast(Activity activity, String msg) {

    if (activity == null) {
        throw new IllegalArgumentException("The passed in activity cannot be null");
    }

    if (msg == null) {
        msg = activity.getString(R.string.no_msg);
    }

    ToastLayoutBinding toastLayout = DataBindingUtil.inflate(
            activity.getLayoutInflater(), R.layout.toast_layout, null, false);
    toastLayout.setMsg(msg); // Set the toast message here

    Toast toast = new Toast(activity);
    toast.setView(toastLayout.getRoot());
    toast.setGravity(Gravity.TOP, 0, 200);
    toast.setDuration(Toast.LENGTH_LONG);
    toast.show();
}

I have used data-binding, however, this should also work with other layout inflation approaches.

I hope that helps other developers having the same problem.

Reaz Murshed
  • 23,691
  • 13
  • 78
  • 98