Try   HackMD

Android Automation

Sources:
Text Tutorial
Espresso Course

Main Android Terms

An activity

is one screen of an app. In that way the activity is very similar to a window in the Windows operating system. The most specific block of the user interface is the activity. An Android app contains activities, meaning one or more screens. Examples: Login screen, sign up screen, and home screen.

An activity in Android is a specific combination of XML files and JAVA files. It is basically a container that contains the design as well as coding stuff.

XML files provide the design of the screen and JAVA files deal with all coding stuff like handles, what is happening, design files, etc. JAVA files and XML files make your activity complete.

The concept of activties

To make an app we need activities that are bound loosely with each other. In the Android manifest we can organize the activities in parent-child relationships to aid navigation. The “main activity” is called the first activity.

Example: In social media, e-commerce, and other apps, you see the main activity whenever you open up that app.

Configuring the manifest

Activities must be declared and their certain attributes must be declared in the manifest so that we can use the activities in our app. Android manifest is a file that will give you information about the application.

Activity Lifecycle

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

View

is a simple building block of a user interface. It is a small rectangular box that can be TextView, EditText, or even a button. It occupies the area on the screen in a rectangular area and is responsible for drawing and event handling. View is a superclass of all the graphical user interface components.

  • TextView
  • EditText
  • Button
  • Image Button
  • Date Picker
  • RadioButton
  • CheckBox buttons
  • Image View

Android View Group

A View Group is a subclass of the ViewClass and can be considered as a superclass of Layouts. It provides an invisible container to hold the views or layouts. ViewGroup instances and views work together as a container for Layouts. To understand in simpler words it can be understood as a special view that can hold other views that are often known as a child view.

Following are certain commonly used subclasses for ViewGroup:

  • LinearLayout
  • RelativeLayout
  • FrameLayout
  • GridView
  • ListView

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Recycler View

is a widget that is more flexible and advanced version of GridView and ListView. It is a container for displaying large datasets which can be scrolled efficiently by maintaining limited number of views. You can use RecyclerView widget when you have data collections whose elements change at runtime depend on network event or user action. You supply the data and define how each item looks, and the RecyclerView library dynamically creates the elements when they're needed.

Following are used when recycler view is in place:

  1. ItemViewType: Type of view based on the position for reuse/recycle.
  2. Count: the method that will be called multiple times to determine the maximum size of the list. In most cases, it returns the size of the dataset that was used to create views in the adapter.
  3. RecyclerView is the ViewGroup that contains the views corresponding to your data. It's a view itself, so you add RecyclerView to your layout the way you would add any other UI element.
  4. Each individual element in the list is defined by a view holder object. When the view holder is created, it doesn't have any data associated with it. After the view holder is created, the RecyclerView binds it to its data. You define the view holder by extending RecyclerView.ViewHolder.
  5. The RecyclerView requests views, and binds the views to their data, by calling methods in the adapter. You define the adapter by extending RecyclerView.Adapter.
  6. The layout manager arranges the individual elements in your list. You can use one of the layout managers provided by the RecyclerView library, or you can define your own. Layout managers are all based on the library's LayoutManager abstract class.

Let’s imagine we have a RecyclerView with 30 items on the list, but only 5 of them are visible at first. RecyclerView causes the adapter to run the onCreateViewHolder function to initialize the view and the onBindViewHolder function to bind the data when constructing the showing item view.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

When scrolling down the process, item x becomes un-visible on screen and will be stored in the collection of scrapped views. And you have item 6 visible on the screen. A “scrapped” view is a collection view that is still attached to its parent RecyclerView but that has been marked for removal or reuse.

Before item 7 becomes visible when we scroll a second time, the recycler view will take a view from the collection of scrapped views and use the onBindViewHolder function to bind the new data instead of using the onCreateViewHolder function to construct a view. The view which is loaded from scrapped view is called a dirty view.

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Android Layout

Layout basically refers to the arrangement of elements on a page these elements are likely to be images, texts or styles. These are a part of Android Jetpack. They define the structure of android user interface in the app, like in an activity. All elements in the layout are built with the help of Views and ViewGroups. These layouts can have various widgets like buttons, labels, textboxes, and many others.

<?xml version="1.0" encoding="utf-8"?> 

  <LinearLayout
       android:id="@+id/layout2"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:layout_weight="1"
       android:background="#8ED3EB"
       android:gravity="center"
       android:orientation="vertical" >

       <TextView
           android:id="@+id/textView4"
           android:layout_width="match_parent"
           android:layout_height="wrap_content"
           android:layout_marginLeft="10dp"
           android:layout_marginTop="-40dp"
           android:fontFamily="@font/almendra_bold"
           android:text="This is a TextView" />

  </LinearLayout>

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

ViewGroup vs Layout

A ViewGroup is a special view that can contain other views. The ViewGroup is the base class for Layouts in android, like LinearLayout, RelativeLayout, FrameLayout etc.

In other words, ViewGroup is generally used to define the layout in which views(widgets) will be set/arranged/listed on the android screen.

ViewGroups acts as an invisible container in which other Views and Layouts are placed. Yes, a layout can hold another layout in it, or in other words a ViewGroup can have another ViewGroup in it.

Fragment

is a Graphical User Interface component of Android. It resides within the Activities of an Android application. It represents a portion of UI that the user sees on the screen. Android Fragments cannot exist outside an activity. Another name for Fragment can be Sub-Activity as they are part of Activities.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

How fragments work

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

The above image depicts two devices: a Handset and a Tablet.

In tablets, there is only one activity that is Activity 1.
In Activity 1, there are two fragments: Fragment A and Fragment B.
When we select an item from Fragment A, it gets open in Fragment B of the same activity.

In the case of mobiles, there are two activities that are:
Activity 1 with Fragment A and Activity 2 with Fragment B.
When we select an item from Fragment A, it gets open in the Fragment B of Activity 2.

Example of a fragment:

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Intents

Intents, in general, are used for navigating among various activities within the same (or not same) application, but note, is not limited to one single application.

They can also be utilized for moving from one application to another - e.g. jump from one application to another as a part of the whole process, for example, searching for a location on the browser and witnessing a direct jump into Google Maps or receiving payment links in Messages Application (SMS) and on clicking jumping to PayPal or GPay (Google Pay).

This process of taking users from one application to another is achieved by passing the Intent to the system.

Applications of Intents:

  • Sending the User to Another App
  • Getting a Result from an Activity
  • Allowing Other Apps to Start Your Activity
        btn.setOnClickListener(new View.OnClickListener() {
        
        @Override
        public void onClick(View v) {
               String url = editText.getText().toString();
               Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
               startActivity(intent);
           }
       }

Compose vs XML

Summary

  1. Jetpack Compose constructs UI with Composables and doesn’t use Android Views. Composable is also a UI element. It has semantics that describes its attributes. All composables are combined in a single UI tree with semantics that describes its children.
  2. Compose Layout doesn’t have IDs and tags. Instead, there’s the testTag attribute in semantics that allows you to add a unique identifier to a Composable.
  3. Different testing tools. Espresso and UIAutomator can still test a Compose Layout — searching by text, resource, etc. However, they don’t have access to Composables’ semantics and can’t fully test them. Therefore, it’s recommended to use the Jetpack Compose testing library as it can access semantics and fully test Composables on the screen.
  4. Compose tests are synchronized by default. Moreover, they don’t run in real time, but use a virtual clock so they can pass as fast as possible.
  5. Uses AndroidComposeTestRule or ComposeTestRule test rule.

XML

In the beginning of the Android development, developers where using xml files for ui development and had to use “findViewByID” for connecting ui items with application logic.(Button click listeners, setText for TextView and so on.)

and some add-ons to simplify things:

  1. Butter-knife - annotate fields with @BindView and a view ID for Butter Knife to find and automatically cast the corresponding view in your layout
class ExampleActivity extends Activity {
  @BindView(R.id.title) TextView title;
  @BindView(R.id.subtitle) TextView subtitle;
  @BindView(R.id.footer) TextView footer;

  @Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);
    ButterKnife.bind(this);
    // TODO Use fields...
  }
}
  1. Kotlin Synthetic - directly access to xml item by it’s id

  @Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);
    itemText.text = "Hello Android!"
  }
}
  1. Data Binding - to use less repetitve code.It moves UI operations out of the activities and fragments to the XML layout. You assign the attribute to a variable, in the XML layout

  2. View Binding - it generates a binding class for each XML layout file present in that module. With these binding classes we get references to all views that are in xml

Jetpack Compose

is an Android UI toolkit introduced by Google. It improves the way the UI is built in most current Android applications.

An XML interface can be rewritten with Compose with fewer lines of code:

Composable Functions

With compose, we define our interfaces as functions.

  1. Create a composable function
@Composable // Annotation to say: "This is a composable function"
fun ShowText(name: String) {
    Text(text = "Hello $name!") //Composable Text function
}
  1. Call this function in MainActivity:
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //Since Compose is not derived from Android views,
        //Instead of setContentView we use setContent 
        setContent {
            ShowText("Android")
            //As a Different Approach:
            //Text(text = "Hello Android!")
        }
    }
}

Run Output:

Compose Layouts

Name of the Component How it Works
Column Inserts item from up to down
Row Inserts item from left to right
Box Inserts item on top of each other

Espresso

Basics

  • ViewMatchers - allows to find view in the current view hierarchy
  • ViewActions - allows to perform actions on the views
  • ViewAssertions - allows to assert state of a view

Base Espresso Test

onView(ViewMatcher)      // finds the view 
 .perform(ViewAction)     // performs an action on the view
   .check(ViewAssertion); // validates an assertion

Testing Navigation Between Fragments

  1. Launch activity that has the fragment
val activityScenario = ActivityScenario.launch(MainActivity::class.java)
  1. Navigate inside the activity to interact with the fragment
onView(withId(R.id.movie_directors)).perform(click())
  1. Verify behaviour
onView(withId(R.id.fragment_directors_parent))
            .check(matches(isDisplayed()))

Testing Fragments in Isolation

launchFragmentInContainer() - is used to launch our desired fragment in an isolated environment (essentially an empty activity that houses that fragment)

@Test
fun tournamentsContainerIsDisplayed() {
    launchFragmentInContainer<TournamentsFragment>()
    
    onView(withId(R.id.recycler_tournaments))
            .check(matches(isDisplayed()))
}

The call to launchFragmentInContainer() can also be done with the addition of two arguments:

fragment arguments — to configure fragment behaviour. It’s likely that some of your fragments make use of fragment arguments - to test the behaviour given these conditional values. E.g. movie names in MovideDetailsFragment

val args = Bundle().apply {
            putString(ARG_FRAGMENT_MODE, "some_mode_key") 
}

fragment factory — allows you to provide a way to depict how the fragment should be instantiated. So that the outcome of this factory can also be tested, this factory can be passed in as an argument.

val factory = SomeFragmentFactory()

in the end the code looks like this:

launchFragmentInContainer<TournamentsFragment>(args, factory)

Recycler View Testing

  1. Set a rule where we would restart the activity with every new test
    @get:Rule
    val activityRule = ActivityScenarioRule(MainActivity::class.java)
  1. Check whether the view is displayed
    @Test
    fun test_isListFragmentVisible_onAppLaunch() {
        onView(withId(R.id.recycler_view)).check(matches(isDisplayed()))
    }
  1. Click on item in the RecyclerView

To click an item in a recyclerView we need to use actionOnItemAtPosition function: actionOnItemAtPosition<{AdaprerName}.{ViewHolder}>({itemToClick}, {action})

    @Test
    fun test_selectListItem_isDetailFragmentVisible() {
        // Click list item #LIST_ITEM_IN_TEST
        onView(withId(R.id.recycler_view))
            .perform(actionOnItemAtPosition<MovieViewHolder>(LIST_ITEM_IN_TEST, click()))

        // Confirm nav to DetailFragment and display title
        onView(withId(R.id.movie_title)).check(matches(withText(MOVIE_IN_TEST.title)))
    }

Testing Compose Layouts

Step 1. Preparation

Create a compose rule object which is the main key to testing our composable.

createComposeRule is required because it provides us access to our composable components. With its help, we can even test our composable in isolation.

In case we want our activity reference in testing then we can create createAndroidComposeRule<MainActivity>()

Step 1.1. Set our composable as content to our compose rule.

@RunWith(AndroidJUnit4::class)  // To indicate that we've to run it with AndroidJUnit runner
class CounterDisplayKtTest {

    @get: Rule
    val composeTestRule = createComposeRule()   // compose rule is required to get access to the composable component

    @Before
    fun setUp() {
        composeTestRule.setContent {    // setting our composable as content for test
            ApiTestingTheme {
                CounterDisplay(modifier = Modifier.fillMaxSize().background(Color.Black))
            }
        }
    }
}




Step 2. Test Cases
Let's assume we have an app with 3 states:


Case 1. Verify if all of our views in the default state exist or not.


@Test
fun verify_if_all_views_exists() {
    composeTestRule.onNodeWithTag("Counter Display").assertExists()
    composeTestRule.onNodeWithTag("Input").assertExists()
    composeTestRule.onNodeWithText("Copy").assertExists()
}
}

onNodeWithText and onNodeWithTag are of the same purpose but we can use either. We can set a testTag to our modifier so that we can identify the component when testing.

Case 2. Invalid state
If we perform a click action on the Copy button while the input is empty the display text should be an Invalid entry.

@Test
fun counterValue_with_emptyInput_displays_InvalidEntry() {
    composeTestRule.onNodeWithText("Copy").performClick()

    composeTestRule.onNodeWithTag("Counter Display").assertTextEquals("Invalid entry")
}

Line 3: we first identify our view with text Copy on it. Once identified, we perform a click action on it. Now the onClick block of our button will be called.

Line 5: We know that if the input is invalid it would be displayed as an Invalid entry. So we first identify our view with test tag “Counter Display” where the final output will be displayed. Then we assert if the text on it is “Invalid entry” or not.

Case 3. Valid State
Now, we’ll test if we correctly input an integer and click on the button, do we get the text as “Counter = 1” on the “Display Counter” text component?

@Test
fun withInput_as_1_onButtonClick_displayOnTop() {
    // mimic the user inputting the text
    composeTestRule.onNodeWithTag("Input").performTextInput("1")
    // mimic if the user clicked on the button
    composeTestRule.onNodeWithText("Copy").performClick()
    // mimic if the user is shown the desired text on UI
    composeTestRule.onNodeWithTag("Counter Display").assertTextContains("Counter = 1")
}