App & Browser Testing Made Easy

Give your users a seamless experience by testing on 3000+ real devices and browsers. Don't compromise with emulators and simulators

Home Guide TDD in Android : Test Driven Development Tutorial with Android

TDD in Android : Test Driven Development Tutorial with Android

By Sourojit Das, Community Contributor -

Android is the most widely used mobile operating system currently available. It has been created to be user-friendly and accessible to everyone, regardless of their technological knowledge or expertise. This is why it’s so popular: you get many features and applications for each device, and it’s free, configurable, and open source. In addition, other developers produce applications that improve Android.

Android’s built-in security mechanisms protect users from dangerous apps and hackers. It also provides access to Google Play, the official app store for Android smartphones, as well as a selection of apps and games. In addition, Android is continually updated with new features and enhancements to guarantee that it is compatible with the most recent technologies. This allows consumers to maximise their device’s functionality while being secure online. Moreover, Android handsets are compatible with a variety of accessories, like Bluetooth speakers, fitness trackers, virtual reality headsets, and others. Allowing users to further customise their smartphone and utilise all of its features.

As expected by the majority of consumers, a solid Android app provides an excellent user experience and is error- and bug-free. People have a myriad of Android app selection alternatives today. So, even a minor error or a few glitches can cause users to dislike or remove an application. Before being published on the Play Store, it is of the utmost significance that Android applications are thoroughly tested for bugs and problems.

To create an app that is both user-friendly and technically sound, app developers exert significant effort. So, they never want to release applications with faults and errors. To ensure an error- and bug-free app program, both the development and testing teams must exercise extreme caution when writing code and testing the app, respectively. Occasionally, it is also necessary to work both teams jointly.

What is Android Testing and Why is Android Testing Important

Android testing is the evaluation of the functionality and performance of Android applications. Android testing involves validating the application’s functionality across a variety of devices, operating systems, and configurations.

There are numerous Android testing types, including:

  1. Unit Testing
  2. Integration Testing
  3. Functional Testing
  4. UI Testing
  5. Performance Testing
  6. Security Testing

The Android testing framework is a crucial component of the development environment. It provides an architecture and potent tools for testing every aspect of your application. It may also be used to test all levels of application development, from unit to framework.

  • Android testing tools are based on JUnit. Plain JUnit can be used to test classes that do not call the Android API. Android’s JUnit extensions can be used to test Android components.
  • Android JUnit extensions that provide component-specific test case classes can be used to build mock objects and methods, which is helpful for controlling the component’s lifecycle.
  • Testers can use the Eclipse-integrated SDK tools for creating and testing. These utilities generate the various test package files.

So, to design an error-free app, robust TDD methods are extremely crucial.

Pro Tip: In an era of device fragmentation, the only way to guarantee app speed and reliability is to test them on real Android smartphones. Given that hundreds of them are in use worldwide, this poses a challenge. An Android Device Lab with decent device coverage is expensive to build. Developers and testers can execute tests on a real device cloud, in comparison, It allows enterprises to test app performance and efficacy on real Android devices without the requirement to collect or manage devices. BrowserStack provides the cloud-based infrastructure required for android testing operations. Also, they can test websites on actual browsers installed on Android mobile devices. Android testing can be performed by registering for free and choosing the desired device-Android version combination.

What is Test Driven Development

Prior to writing the actual code, Test-Driven Development (TDD) emphasises the production of unit test cases. It iteratively combines development, the creation of unit tests, and refactoring.

The origins of the TDD methodology are the Agile manifesto and Extreme programming. As its name implies, software development is driven by testing. In addition, it is an approach for structuring code that enables developers and testers to produce code that is both efficient and resilient over time.

Based on their initial comprehension, engineers begin writing small test cases for each feature using TDD. This method aims to only modify or develop new code if tests fail. This avoids multiple scripts for testing.

TDD can be represented by the Red-Green-Refactor Cycle.

TDD in FlutterRed-Green-Refactor in TDD

It comprises three essential steps:

  • Develop a test that will not pass (Red)
  • Develop code that can pass a test (Green)
  • Refactorize your code to attain high code quality (Refactor)

So what exactly does it mean? Before writing any code for a new project, you should be able to write a test that fails, then write the code necessary for the test to pass, then rewrite the code if necessary and begin the cycle again with another test.

Obviously, it is not always required to test every component of your application, especially when developing with Flutter; for example you will rarely need to test your entire UI and verify that each AppBar is displayed correctly,. Nevertheless, it may be beneficial to unit test some API calls if your application is consuming data from an external service or to do database-related tests if your application makes extensive use of the database. TDD will ultimately improve the stability and quality of your code greatly, especially if you maintain or contribute open-source code.

Why is TDD important in Android?

1. Easier code maintenance: With TDD, developers write code that is more readable, manageable, and maintainable. Also, it requires less effort to concentrate on smaller, more digestible code bits. When transferring a project to a different individual or group, it is advantageous to have clean code.

2. Allows for Modular Design: The focus is placed on a particular feature at a time until the test has been passed. These iterations make finding bugs and reusing code in a project simple. In addition, solution architecture is improved by adherence to certain design principles.

3. Facilitates Code Refactoring: Refactoring is the process of optimising existing code in order to make it easier to implement. If the code for a modest update or improvement passes the preliminary tests, it can be refactored to acceptable standards. This is a necessary TDD process step.

4. Reduces the dependency on code documentation: The TDD methodology eliminates the need for time-consuming and detailed documentation. TDD entails a large number of simple unit tests that can function as documentation. Also, these unit tests illustrate how the code should operate.

5. Decreases the necessity for debugging: When there are fewer problems in the code, developers spend less time correcting them. In addition, errors are easier to detect, and developers are notified faster when anything breaks. This is one of the major benefits of the TDD process.

How To Perform TDD in Android

In order to demonstrate TDD in Android a sample task is considered as given below in the Given-When-Then format used in BDD. 

This example describes an application using BDD. As can be seen, it does not define an Android, iOS, or Web application, but rather focuses on the desired behaviour. TDD is often used  to address an issue because we do not initially describe the behaviour in a technology-agnostic manner.

TDD is most effective when the architecture helps to isolate technical specifics (such as the GUI, DBMS, HTTP, Bluetooth, etc.), because testing these aspects automatically is time-consuming and error-prone.

However, developers can easily become preoccupied with technology and lose sight of the application’s added commercial value. The aforementioned scenarios will drive the development of the tests, as will be seen next.

Add Task Action

Given I see the Task List screen

When I click Add Task button

Then I see Save Task screen
Save Task

Given I see Save Task screen

And I write call mum in the description

When I click Save button

Then I see Task List screen

The Save Task scenario provides the most value to the user, and once it has been completed, the story will be concluded. We have a greenfield application. Therefore, beginning with Save Task will lengthen the feedback loop too much.

Add Task Action and Empty Task List are significantly easier circumstances. As the initial state of the application, it is more natural to implement the Empty Task List scenario before the Add Task Action scenario. Empty Task List is a corner case that provides no benefit to the user, so I will test its implementation without a graphical user interface. In addition, we should keep edge case situations at the bottom of the test pyramid and place happy path scenarios at the top.

@Test
fun `Given I have no tasks yet 
When I open task application 
Then I see Task List screen And I see no tasks`() {

// Given
val myTaskApplication = MyTaskApplication()

// When
myTaskApplication.open()

// Then
myTaskApplication.withScreenCallback { screen ->
assertEquals(emptyList<String>(), screen.taskList)
}
}

Since this test excludes the user interface, certain design decisions must be made while designing it. MyTaskApplication registers a callback to receive MyTaskListScreen containing the list of tasks.

/**
* This class represents a simple task application.
* It allows navigating between different screens.
*/
class MyTaskApplication {
// Current screen displayed in the application.
private lateinit var myScreen: MyScreen

/**
* Opens the task application and sets the current screen to the task list screen.
*/
fun open() {
myScreen = MyTaskListScreen(emptyList())
}

/**
* Simulates adding a task, which changes the current screen to the save task screen.
*/
fun addTask() {
myScreen = MySaveTaskScreen()
}

/**
* Allows interaction with the current screen by providing a callback function.
*
* @param callback The callback function to interact with the current screen.
*/
fun withScreenCallback(callback: (MyScreen) -> Unit) {
callback.invoke(myScreen)
}
}

/**
* Represents the task list screen, which displays a list of tasks.
*
* @property taskList The list of tasks displayed on this screen.
*/
data class MyTaskListScreen(
val taskList: List<String>
)

/**
* Represents the save task screen, where the user can add a new task.
*/
class MySaveTaskScreen : MyScreen

/**
* Interface for different screens in the application.
*/
interface MyScreen

To develop this test, we have to add a Screen interface in order to reuse the withScreenCallback function for MySaveTaskScreen.

interface MyScreen

/**
* Represents the task list screen, which displays a list of tasks.
*
* @property taskList The list of tasks displayed on this screen.
*/
data class MyTaskListScreen(
val taskList: List<String>
) : MyScreen

/**
* Represents the save task screen, where the user can add a new task.
*/
class MySaveTaskScreen : MyScreen

/**
* This class represents a simple task application.
* It allows navigating between different screens.
*/
class MyTaskApplication {
// Current screen displayed in the application.
private lateinit var myScreen: MyScreen

/**
* Opens the task application and sets the current screen to the task list screen.
*/
fun open() {
myScreen = MyTaskListScreen(emptyList())
}

/**
* Simulates adding a task, which changes the current screen to the save task screen.
*/
fun addTask() {
myScreen = MySaveTaskScreen()
}

/**
* Allows interaction with the current screen by providing a callback function.
*
* @param callback The callback function to interact with the current screen.
*/
fun withScreenCallback(callback: (MyScreen) -> Unit) {
callback.invoke(myScreen)
}
}

Now we also can apply certain refactors. The screenCallback variable can have a default value of no action, allowing us to bypass the? syntax and just call the function. MyScreen, MyTaskListScreen, and MySaveTaskScreen can also be moved to their appropriate files.

Based on the above, we can :

  • Provide a default value for screenCallback with a no-op function.
  • Move each class to its respective file.

MyScreen.kt:

interface MyScreen

MyTaskListScreen.kt:

data class MyTaskListScreen(

    val taskList: List<String>

) : MyScreen

MySaveTaskScreen.kt:

class MySaveTaskScreen : MyScreen

MyTaskApplication.kt:

class MyTaskApplication {
    private lateinit var myScreen: MyScreen
    private var screenCallback: (MyScreen) -> Unit = {}

    fun open() {
        myScreen = MyTaskListScreen(emptyList())
    }

    fun addTask() {
        myScreen = MySaveTaskScreen()
    }

    fun withScreenCallback(callback: (MyScreen) -> Unit = {}) {
        screenCallback = callback
        screenCallback.invoke(myScreen)

        // Reset the screenCallback to no action
        screenCallback = {}
    }
}

With these changes, we’ve provided a default value for screenCallback, allowing us to bypass the ? syntax when calling the function. Additionally, each class has been moved to its appropriate file for better code organization.

Finally we may address the Save Task scenario. Now we will add the user interface to the test, making it more comprehensive.

@RunWith(AndroidJUnit4::class)
class TaskManagerTest {

@Test
fun `Given I have no tasks 
And I see Save Task screen 
And I fill description 
When I tap Save button 
Then I see Task List screen with description`() {

// Launch the activity
val activityScenario = ActivityScenario.launch(MainActivity::class.java)

// Define view IDs
val addTaskButton = withId(R.id.view_task_list_add_task_button)
val descriptionInput = withId(R.id.view_add_task_description_input_field)
val saveButton = withId(R.id.view_add_task_save_task_button)

// Perform actions
onView(addTaskButton).perform(click())
onView(descriptionInput).perform(replaceText("My new task description"))
onView(saveButton).perform(click())

// Verify the result
onView(withText("My new task description")).check(matches(isDisplayed()))

// Close the activity scenario
activityScenario.close()
}
}

This test fails because we do not yet have a MainActivity and it has not been declared in the AndroidManifest. And the ids we are referencing don’t exist. To pass, some design considerations must be taken. As there will be two displays in the application, we might represent them using Fragments, Activities, or Views. Standard practice recommends that views and a single activity is employed for this.

// Application class for managing tasks
class MainApplication : Application() {
val taskApplication by lazy {
TaskApplication()
}

override fun onCreate() {
super.onCreate()
taskApplication.open()
}
}

// Main Activity for displaying different screens
class MainActivity : AppCompatActivity() {

private val taskApplication by lazy {
(application as MainApplication).taskApplication
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val frame = findViewById<FrameLayout>(R.id.activity_main_frame)

taskApplication.withScreenCallback { screen ->
frame.removeAllViews()
when (screen) {
is TaskListScreen -> {
val view = TaskListView(this)
view.application = taskApplication
frame.addView(view)
view.updateScreen(screen)
}
is SaveTaskScreen -> {
val view = SaveTaskView(this)
view.application = taskApplication
frame.addView(view)
}
}
}
}
}

// View for displaying the list of tasks
class TaskListView : ConstraintLayout {

lateinit var application: TaskApplication
private lateinit var listView: RecyclerView

constructor(context: Context) : super(context) {
init()
}

constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init()
}

constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init()
}

fun updateScreen(screen: TaskListScreen) {
(listView.adapter as TaskListAdapter).updateTasks(screen.tasks)
}

private fun init() {
inflate(context, R.layout.view_task_list, this)
findViewById<Button>(R.id.view_task_list_add_task_button).setOnClickListener {
application.addTask()
}
listView = findViewById(R.id.view_task_list_view)
listView.layoutManager = LinearLayoutManager(context);
listView.adapter = TaskListAdapter(LayoutInflater.from(context))
}
}

// View for saving a new task
class SaveTaskView : ConstraintLayout {

lateinit var application: TaskApplication
private lateinit var saveTaskButton: Button
private lateinit var descriptionInputField: EditText

constructor(context: Context) : super(context) {
init()
}

constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
init()
}

constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init()
}

private fun init() {
inflate(context, R.layout.view_add_task, this)
saveTaskButton = findViewById(R.id.view_add_task_save_task_button)
descriptionInputField = findViewById(R.id.view_add_task_description_input_field)
saveTaskButton.setOnClickListener {
application.saveTask(descriptionInputField.text.toString())
}
}
}

To pass the test we had to build numerous classes (we have omitted the xml files and the list adapter implementation). The feedback loop time was not optimal, but subsequent stories will require less code because the foundations will already exist. To prevent a lengthy feedback loop in greenfield systems, the first user story must be as minimal as possible while still offering value.

Best Practices For TDD in Android

  1. Frequent testing: the sooner teams begin testing, the better. Test the code shortly after writing it. This will allow teams to spot bugs sooner and avoid debugging already-functioning code.
  2. Keep your tests succinct and focused: Each test should evaluate exactly one notion. This will expedite the detection and correction of faults.
  3. Make readable assessments: The tests should be straightforward to read and comprehend. This will expedite bug noticing and correction of faults.

Conclusion

TDD involves traversing the test pyramid and determining at which tier a certain behaviour should be implemented. To accomplish this, the behaviour must initially be crystal apparent. You should prioritise putting a new behaviour at the apex of the test pyramid so that all happy pathways are covered by merging all non-slow application components.

By adhering to this methodology, you may prevent inadvertent complication and maintain our focus on bringing value to the user. In addition, as the size of the source code increases, the development rate remains constant because coverage gives developers the confidence to modify the application.

Even with the most severe TDD testing in Android, many device-OS combinations must be permitted for UI Testing to assure platform compatibility. Setting up and maintaining platforms becomes a time- and labor-intensive task as the number of platforms to support increases.

Yet, with BrowserStack App Live (Manual Testing) and BrowserStack App Automate (Automation Testing), you’ll have access to hundreds of popular mobile device-operating system combinations such as Samsung Galaxy (S4 -S22), Google Pixel, and Xiaomi devices for testing their application and script automation instances. 

This means that you are not responsible for purchasing hardware or implementing software updates or patches, yet you get the facility to test on real devices under real user conditions. You merely need to Sign-up for free, choose the needed device-operating system combination, and start testing their application.

Try BrowserStack App Automate for Free

Tags
Automation Testing Mobile App Testing

Featured Articles

What is Android UI Testing?

How to Inspect Element on Android Device ?

Curated for all your Testing Needs

Actionable Insights, Tips, & Tutorials delivered in your Inbox
By subscribing , you agree to our Privacy Policy.
thank you illustration

Thank you for Subscribing!

Expect a curated list of guides shortly.