Our mobile devices are, well, mobile! We can send and receive various kinds of communication through them, including text/SMS messages. Sometimes, SMS messages might be an integral part of the user experience of our own apps. This is certainly true for messaging apps themselves, but you could imagine a variety of other applications that tie into the SMS world.
The case study we'll look at today involves hooking into SMS messages in order to automatically verify a second factor of authentication. What does this mean? It's common practice nowadays when signing up for a service to confirm your phone number as a way to verify your identity for future requests. The flow for verifying your phone number is often a pain: you have to receive a text message with a code, navigate to your message app, then back to the app you're trying to use to enter the code.
Fortunately, Android apps can hook into the SMS system and read text messages for you, creating the possibility of automatically verifying your phone number based on a code received from an app's backend SMS service. I have added just this kind of functionality to The App---fake, of course! First, we navigate to a screen that looks like this:
And nothing happens until we get a text message that contains the appropriate code. Once we do, the view changes:
Of course, since this is just a demo app, the verification code is statically hard-coded into the app itself, whereas in the real world it would be randomly generated and sent by a backend service. But this is great for now. The question becomes, how do we test this flow with Appium? How can we have Appium send a text message to the emulator so we can see the app verification behavior at work? The answer is to use the aptly-named sendSMS
command:
driver.sendSMS("555-555-5555", "Your message here!");
And that's really it. Simply supply the two String parameters ("from" phone number and message). Well, there is one more requirement for this whole scenario to work: you have to be testing on emulators, since the sendSMS
command is not available for real devices. So to prove to Appium that we're running an emulator and not a real device, it's currently required to send in the avd
capability with the name of the emulator you're running the test on. If that emulator is already up and running, Appium will simply use it as is. If it's not up and running, then Appium will start it for you. If you don't provide this capability, Appium will complain when you try to use the sendSMS
command that it only works on emulators.
(Curious how you might test the same flow with a real device? You could mix in something like the Twilio SMS API to send actual SMS messages to the phone number associated with your device).
So, with this new command at our disposal, we can now construct a simple test to verify the flow we discussed:
// assume we have a driver and a wait object all ready to go by this point
// first set up the locators for the elements we use for verification
By waiting = MobileBy.xpath("//android.widget.TextView[contains(@text, 'Waiting to receive')]");
By verified = MobileBy.xpath("//android.widget.TextView[contains(@text, 'verified')]");
// start the flow by asserting we are in the 'waiting' state
wait.until(ExpectedConditions.presenceOfElementLocated(waiting));
// first test an incorrect code and ensure that the waiting message is still present
driver.sendSMS("555-555-5555", "Your code is 654321");
driver.findElement(waiting);
// now test the correct code and assert that the verification message is present
driver.sendSMS("555-555-5555", "Your code is 123456");
wait.until(ExpectedConditions.presenceOfElementLocated(verified));
// if we get here we have successfully checked the auto-verification functionality
It's pretty fun to watch a fake text message show up on the emulator screen! Of course, if we wanted, we could also switch apps to the Messages app itself, and verify other properties of the message that came through. Anyway, a full code sample involving the flow above is below (or you can check it out on GitHub:
import io.appium.java_client.MobileBy;
import io.appium.java_client.android.AndroidDriver;
import java.io.IOException;
import java.net.URL;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.openqa.selenium.By;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
@RunWith(JUnit4.class)
public class Edition011_Android_SMS {
private String APP = "https://github.com/cloudgrey-io/the-app/releases/download/v1.3.0/TheApp-v1.3.0.apk";
private String AVD_NAME = "emu27";
private By verifyScreen = MobileBy.AccessibilityId("Verify Phone Number");
private By waiting = MobileBy.xpath("//android.widget.TextView[contains(@text, 'Waiting to receive')]");
private By verified = MobileBy.xpath("//android.widget.TextView[contains(@text, 'verified')]");
@Test
public void testSMS () throws IOException {
DesiredCapabilities capabilities = new DesiredCapabilities();
capabilities.setCapability("platformName", "Android");
capabilities.setCapability("deviceName", "Android Emulator");
capabilities.setCapability("automationName", "UiAutomator2");
capabilities.setCapability("avd", AVD_NAME);
capabilities.setCapability("app", APP);
// Open the app.
AndroidDriver driver = new AndroidDriver(new URL("http://localhost:4723/wd/hub"), capabilities);
WebDriverWait wait = new WebDriverWait(driver, 5);
try {
wait.until(ExpectedConditions.presenceOfElementLocated(verifyScreen)).click();
wait.until(ExpectedConditions.presenceOfElementLocated(waiting));
// first test an incorrect code and ensure that the waiting message is still present
driver.sendSMS("555-555-5555", "Your code is 654321");
driver.findElement(waiting);
// now test the correct code and assert that the verification message is present
driver.sendSMS("555-555-5555", "Your code is 123456");
wait.until(ExpectedConditions.presenceOfElementLocated(verified));
} finally {
driver.quit();
}
}
}