Mobile test automation practices —
Part 2: preconditions, elements verification and independent steps

Dmitrii Makarenko
Sep 1 · 18 min read

This is Part 2 of an article on the mobile test automation practices we use at Bumble. In Part 1 we looked at the role of automation in our processes, gave details of the framework, and examined in detail three practices we employ when creating autotests.

Here in Part 2, we cover the following topics:

  1. Practice no. 4. Verification of element state transitions
  2. Practice no. 5. Guaranteed preconditions setup
  3. Practice no. 6. Simple and complex actions, or Independent steps in tests
  4. Practice no. 7. “Optional” elements verification (which is mandatory to do 🙂)
  5. Summary
  6. Bonus.

I would like to stress that the examples given in both Parts of this article are equally relevant for those just starting to roll out automated testing in their project, and those who are already actively using it. Also, it shouldn’t matter which framework you use. We generalise all our recommendations so that it’s easier for you to apply them.

Spoiler alert: We will share a test project with all practices located on Github at the end of this article.

Practice no. 4. Verification of element state transitions

For example, in the case of slow internet, elements received from a server are displayed with a significant delay. And try to verify them before they appear and the tests will fail.

Thus, before trying to verify elements we need to wait for them to appear on-screen. Obviously, this is not a new problem and standard solutions exist. For example, Selenium has different types of “wait” methods, and Calabash has the “wait_for” method.

Why we need our own bike/Why we didn’t go for a standard solution

After we added logs and analysed them, it turned out that the cases of frozen tests were linked to the “wait_for” method, which is part of the Calabash framework. “wait_for” uses the timeout method from the Ruby Timeout module, which is implemented on the main thread. The tests froze when this timeout method was nested within other methods: both within ours and within the Calabash framework methods.

For example, let’s take the case of a profile page being scrolled down to the “block user” button.

We can see that the “wait_for” method is used here. The screen is scrolled down, then there is a wait for the animation to complete, and finally, there is verification that the “block user” button is displayed.

Let’s take a look at the implementation of the “wait_until_no_animation” method.

The “wait_until_no_animation” method is also implemented with “wait_for”. It waits for the animation on-screen to finish. This means that “wait_for”, called inside “wait_for”, calls other methods. Just imagine that there are also “wait_for” calls inside Calabash methods. As the chain of “wait_for” methods inside “wait_for” methods lengthens, the risk of the test freezing increases as well. For this reason, we opted not to use this method but to devise our own solution.

The requirements for our own solution matched those for standard ones. We needed a method that would repeat verification until the required condition is met, or until the set timeout expires. Should our verification be unsuccessful within the fixed time frame, our method would need to raise an exception.

Initially, we created the “Poll” module with one “for” method, which repeated the standard “wait_for” method. Over time our own implementation allowed us to extend functionality as much as we needed to. We added methods, waiting for specific values of set conditions. For example, “Poll.for_true” and “Poll.for_false” clearly wait for the code being run to return “true” or “false”. In the examples below I show how the various Poll module’s methods are used.

We have also added various parameters for methods. We shall take a look at the “return_on_timeout” parameter in more detail. In essence, when using this parameter, our “Poll.for” method will not raise an exception, even if the required condition is not met but instead returns the result of the executed code block.

I can hear you asking two questions: “How does it work?” and “Why do we need it?” Let’s start with the first question. If in the case of the “Poll.for” method, we were to wait for 2 to become greater than 3, then we would always get a timeout error.

But if we add our “return_on_timeout” parameter and, similarly, wait for 2 to become greater than 3 then, once the timeout has finished, 2 will still not become greater than 3, but our test will not fail. Instead, the “Poll.for” method will return a result of the executed code block.

Why do we need this? We are using the “return_on_timeout” parameter to verify the state transitions of our elements. But this must be done very carefully as it could conceal real instances of tests failing. Used incorrectly, the tests will continue to run when the required conditions are not met when they should have raised an exception.

Element state transitions (various options)

It can only do one of two things: appear on your screen or disappear from your screen.

The first state transition option is called “Should appear”. This is where in state 1 our test object is not displayed, and in state 2 it appears.

Should appear

If it appears, then verification is successful.

The second state transition option is called “Should disappear”. This occurs when in state 1 our test object is displayed, and in state 2 it disappears.

Should disappear

The third option is not as obvious as the previous two because in this case, essentially, we are verifying that the state is unchanged. This is called “Should not appear”. This occurs when in state 1 our test object is not displayed on-screen, and sometime later in state 2 it still does not appear.

Should not appear

You have probably guessed the fourth option. This is called “Should not disappear”. This is when in state 1 the object is displayed on-screen, and sometime later in state 2 it is still there.

Should not disappear

State transitions verification implementation

In the case of the first two options, it’s all pretty simple. To verify the first one, we just need to wait for the element to appear, using our “Poll” method:

“Should appear” verification

To verify the second one, we need to wait for the element to disappear:

“Should disappear” verification

However, in the third and fourth cases, things are not so straightforward.

Now to consider the option “Should not appear”:

“Should not appear” verification

Here, firstly we check that the element is not displayed on the screen.

Then, using “Poll.for” with the “return_on_timeout” parameter, we wait for the element to appear. Here, the “Poll.for” method will not raise an exception if the element in question does not appear but will return “false”. The value obtained from “Poll.for” is saved in the “actual_state” variable.

Next, the “assert” method is used to verify that there has been no change in the status of the element.

To verify the “Should not disappear” option we use similar logic — waiting for the element to disappear from the screen (rather than appear on it):

“Should not disappear” verification

Verifications of these four state transition options are relevant for many mobile application elements. Since we have many engineers developing tests there is always a possibility that someone will forget to include some options when creating new verifications. For this reason, we have moved the implementation of verifications for all state transition options to a single method:

“yield” is the code for the block passed to the method in question. In the examples above this was the “elements_displayed?” method. But it could be any other method that returns the state of a required element. Ruby documentation.

Thus, we can verify any element state transition option by calling a single method, which makes life considerably easier for the whole testing team.

Conclusions:

  • It is helpful to move this verification to a generic method.

We recommend using a full system of possible state transition options for verifications in tests. What do we mean by ‘full’? Imagine that when an element is present the state is “true”, and when it is not the state is “false”.

We are building a matrix of all permutations. If a new state were to appear on the scene, the table could be extended, and new permutations obtained.

Practice no. 5. Guaranteed preconditions set up

Task definition

Examples of solutions

This is what the first example of implementing a method for switching off the location service in iOS looks like:

We are waiting for the switch element to appear on-screen. Once it does, then we verify its state. If it does not match up with the desired state, we change it.

After this, we close the settings and launch the application. And it can happen that all of a sudden we encounter a problem: for some reason the location service remains on. How can this be? After all, we have done everything we could to switch it off. It would seem that this is a problem with how the system settings in iOS work. When you leave the settings quickly (and, in the case of a test, this occurs instantly, once you click on the switch element), its new status is not saved. However, problems may also occur when configuring preconditions in our application.

Now to the second example, namely generating a chat history before starting to run a test. This is what implementation of the method looks like:

We use QaApi for sending messages via “user_id” and we send the required number of messages in the loop.

Next, we go into the chat and perform the necessary verifications related to loading the messages history. But sometimes not all the messages are displayed in the chat. Sometimes we continue to receive them while we are in the chat, but not before we have opened it. This is related to the fact that the server cannot send the necessary number of messages instantly. In some cases, there is a delay in delivering messages.

Let’s identify the common problem in both these examples. When the preconditions are set incorrectly, tests fail in subsequent verifications, irrespective of the fact that the applications themselves are operating as they should. When we start to examine the reasons for this we realise that the tests fail due to incorrect preconditions setup. Such cases are frustrating not only for testers but also for developers because the latter analyse tests results as they are developing additional functionality.

How can we resolve this problem? Add verifications to the methods for setting preconditions, confirming that a required action has been performed successfully before proceeding with test execution.

So, in this case, our method for switching off the location service will look like this:

Using the “Poll.for” method we ensure that the status of the switch element has changed before we move on to the following action as part of the test. This allows us to avoid the problems arising from the location service staying switched on from time to time.

In the second example, once again it is our QaApi methods that help us out.

Before sending messages, we receive the current number of messages in the chat, and afterwards, we ensure that the required number of messages has been sent. Only after this verification succeeds will the test continue to run. In this way, when we open the chat in the application, we see all the necessary messages and can perform the verifications we need.

Takeaways

In terms of a general recommendation, we would advise you to add to your tests verifications that all presets are set when asynchronous actions are performed.

You can read up in more detail about the problems described in this section in this article by Martin Fowler.

Practice no. 6. Simple and complex actions, or, independent steps in tests

Simple actions

Let’s start with a test to search for and send GIF messages.

Initially, we need to open a chat with the user to who we want to send a message:

Then we need to open the field for entering GIF messages:

Next, we need to enter a search query for GIF images, make sure that the list is updated, send an image from the list, and make sure that it has been sent.

Overall, this is what the scenario looks like:

Sending GIF scenario

Let’s focus on the step responsible for searching for a GIF:

Step searching for a GIF

Here, as in almost all other steps, this is what we do:

  1. First, we wait for the necessary page to open up (ChatPage)
  2. Then we save a list of all accessible GIF images
  3. Next, we enter a search keyword
  4. Then we wait for the list to update (after all, we said that it is useful to add verification that actions have been performed successfully before proceeding further).

Everything would appear to have been implemented correctly. After the search has been completed, we check that the list of images has been updated, and only then do we send one of them. But we face a problem if, for example, we want to write a test to verify that, after having entered an identical search query, the list of images will not update. In this case, we need to create a separate step for entering a search query for GIF images. This step will duplicate the existing one to a major extent.

A similar problem occurs in cases when a given action could lead to various results, and, in the step, we only handle one of them in the form of verification that this certain action has been performed. This leads to difficulties with reusing such steps and, therefore, to both a slow-down in test development and to duplication of code.

How can this be avoided? As you may have noticed, our search-for-GIF-images step actually included three actions:

  1. Saving the existing list
  2. Searching
  3. Verification that the list has been updated.

The solution to the re-use problem will be to divide this step into three simple, independent steps.

The first step saves the existing list of images:

The second step — searching for GIFs — allows you to type in a search keyword:

In the third step, we wait for the list to update:

At this point, this is what our initial scenario looks like:

Sending GIF scenario

In this way, we can use the first two steps even in a case where the list does not update in the test. This helps us save development time since we can reuse simple steps in different tests.

But, here too, there are a few minor issues. Steps cannot always be created to be simple and independent. These we call “complex”.

Complex actions

A mini-game is a screen where the user is offered profiles of different people who have written to them. You can either reply to the message or skip those users. We shall call the act of skipping “Voting No”.

Test user

We need to write a test that will “vote No” N times, will close the game screen, and will then re-open it and verify the user has the correct progress in the game.

“Voting No” is a simple action but if we create a simple step for this action, then in order to vote N times we need to use this step the same number of times at the level of the scenario. Reading such a scenario is inconvenient so it makes sense to create a more complex step using the parameter “Number of votes” which would be able to vote the required number of times.

This step would appear to be quite easy to implement. The only thing you need to do is vote on the page the required number of times.

But this is where we encounter a problem: sometimes the test votes too quickly, i.e. it presses the button before the previous button press has been processed. In this case, the application will not send the new vote to the server. At the same time, our step will be completed successfully. At the next step, when we want to make sure that the user has the correct progress in the game, the test will fail because the previous step has not performed its task, and has cast an insufficient number of votes.

As in the case with preconditions set up, this is where we need to get to the bottom of test failures occurring at the step where the test and the application work correctly, but the previous step worked incorrectly. Of course, no one likes such situations so this is a problem we need to solve.

After each vote is cast, we add verification of a change in progress in the mini-game. Only after this will we attempt to vote again. In this way, all the votes will be processed and we will eliminate test failures related to incorrect progress in the game.

Takeaways

To generalise these recommendations, we would advise you to identify independent methods for simple actions in tests and to add presets verification to complex actions.

Practice no. 7. “Optional” elements verification

Examples of alerts

You have probably seen similar dialogues in various mobile applications. In our two applications, there are over 70 of them. They appear in various places in response to various actions by users. What “optional” elements do they contain?

Task definition

  • Screenshot 1: header, description and two buttons
  • Screenshot 2: header, description and one button
  • Screenshot 3: description and two buttons.

Thus, the “optional” elements in these dialogues are the header and the second button. The locators for the elements on all the alerts are identical — irrespective of where they are shown in the application, or which user action they appear after. For this reason, we want to implement verifications for all types of alerts once, on their base page, and then inherit a class for each specific alert from it so that we don’t need to duplicate the code in question. Creating these kinds of verifications is something we will look at in this section.

Examples of solutions

In all these examples we call the “verify_alert” method, using lexemes for verifying the necessary elements. At the same time, as you can see, we don’t send a lexeme for the second button inside “WaitForReplyAlert” class since it should not be there, nor do we send a lexeme for the header inside “SpecialOffersAlert” class.

Here is the implementation of the verify_alert method:

First, we wait for the mandatory elements to appear on-screen. Next, we check that their texts match with the lexemes sent to the method. In the case of “optional” elements, if the lexeme was sent to the method, then we verify the text. Otherwise, we do nothing.

What is the problem with this approach? The problem is that we miss out on verification of the “optional” element not displaying when it should not be displayed. This can lead to confusion. For example, the following alert may appear:

The user doesn’t know which button to tap because both buttons appear to close the dialogue. This looks like a critical bug. Even if the application does not actually crash, this needs to be fixed. Tests need to be changed so that they pinpoint such problems.

To this end, we change this verification in the tests. From this…

to this.

We have changed the if-condition and have added verification of the second state. If we do not send a lexeme for the “optional” element, it means that this element should not be on-screen, which is what we are verifying. If there is some text in the “title”, then we understand that the element with this text should be there, and we verify it. We decided to move this logic to the shared method, which has been called “wait_for_optional_element_text”. It is used not only for alerts but also for any other screens in the application that contain “optional” elements. We can see that the if-condition from the example above is to be found within the new method:

The implementation of the “verify_alert” method has also changed:

We are still waiting for the mandatory elements to appear on-screen. After this, we verify whether their text matches the lexemes sent to the method. In the case with “optional” elements, we now use the “wait_for_optional_element_text” method, which helps us to make sure that these elements are not displayed when they should not be displayed.

Takeaways

Generic recommendation: we advise you to use a full system of possible elements’ states for verifications in tests and to extract shared methods for similar actions.

You will have noticed that we made similar recommendations earlier in this article. The point is that the examples in which they were applied differ.

Summary

  • Since verifications are the reason why we write tests, always use a full system of possible states
  • Do not forget to add presets verification for asynchronous actions
  • Extract shared methods for reusing similar code — both in the steps and in the methods on the pages
  • Implement simple test objects
  • Implement independent methods for simple actions in tests.

This advice might seem obvious to some, but we want to stress the fact that it can be (and should be) applied in various situations. If you have additional helpful recommendations to add to the list, please feel free to do so in the comments!

A little bonus

Bumble Tech

This is the Bumble tech team blog focused on technology and…