Pre-ramble
Appium is an excellent tool for testing an app running "for reals". No faking talking to other systems - it has to hit the real stuff. I prefer Appium for this as it forces that separation. It requires the application to be treated as a black box. If I want to use Espresso (and I might later) to validate UI behavior; I'll be able to treat the application as a whitebox and configure things as required to know what should be displayed. Appium; while I could fake servers and test environments; should operate on "unknown" data.
This is the distinction I make between Espresso/androidTest type tests and those run via Appium; one is validating the UI is running as expected. Espresso is Functional Tests on the UI. Appium is integration tests on the app.
I find the distinction meaningful to maintain the mental model of the purpose of the tests. With integration tests; you can't expect certain data (most of the time). You need to work with unknown and changing information.
These can end up being far harder tests to write. You can't check that the top story title is exactly "Some Fancy Title"; but just that there's... something. The advantage is that you can store this title; go to another view; and confirm the string there matches the previous. While doable in Espresso; you can check each of those independently; you're not forced to track information.
This is actually a good distinction; via an espresso test; I can directly test a non-launchable activity. I don't need to use the app to navigate to that UI. It can (and in my view should) skip the intermediary steps.
Appium does not. You must take those steps to get there. This is why they are both valuable (says the guy skipping Espresso) and should be utilized to have a product where anyone can mercilessly refactor; as well as having no fear when working in the code.
Setting up Appium
Please check out my earlier TagAlong on Appium post where I got Appium set up locally. I would recap... but it's like 2 steps... The rest is the madness to figure them out. :-P
IntelliJ
This is going to require IntelliJ to run the Appium tests; for now. I can probably get it all configured into Gradle; but that's not the purpose here. I currently have it running via Maven/IntelliJ; I'll continue it there to be able to focus on the main point - an Appium Test.
Hmmm... An emulator is apparently useful... so; getting that going.
Unlike the iOS appium testing I set up previously; I need the JAVA_HOME
and ANDROID_HOME
configured. I used this StackExchange to make it a quick process.
After a bit of futzing around with it - I've got a super simple Appium Test running.
public class AppiumTest {
private AppiumDriver driver;
@Before
public void setUp() throws Exception {
DesiredCapabilities capabilities = new DesiredCapabilities();
File app = new File("../app/build/outputs/apk/app-debug.apk");
//capabilities.setCapability("avd","Nexus 5X API 25");
capabilities.setCapability("deviceName","Android Emulator");
capabilities.setCapability("platformVersion", "7.1");
capabilities.setCapability("app", app.getAbsolutePath());
capabilities.setCapability("appPackage", "com.quantityandconversion.hackernews");
capabilities.setCapability("appActivity", ".MainActivity");
driver = new AndroidDriver<>(new URL("http://127.0.0.1:4723/wd/hub"), capabilities);
driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);
}
@Test
public void sampleTest(){
WebElement txt = driver.findElement(By.id("hello_world"));
assertEquals("Hello World!", txt.getText());
}
@After
public void tearDown() throws Exception {
if (driver != null) driver.quit();
}
}
I'm kinda regretting buying a mac-mini w/only 8GB of RAM... It's not happy running Android Studio; IntelliJ, Appium, Emulator... hehehe
This is a quick simple appium set up. Currently it requires a device or emulator be running. I've gotta cycle back to get my environment variables set up correctly to have Appium launch an emulator.
First Integration Test
The first integration test I want is a simple display of the number of top stories.
This is easily done by setting a string value and when it gets the total count; trying to parse it to a string. The number of stories doesn't matter; just that it's a number.
I should get the Appium tests to run via command line... Later.
Right now; working on the app going up; and failing due to not having a number! The test fails due to parsing! Exactly what we're looking for.
Now to wire up the ItemAccess
class and display a number of top posts.
This actually get's us a little into applying VBM; we're going to have all the relevant layers. We could hack something in; but.. If you don't have time to do it right the first time; how in the hell will you have time to fix it?
Sure... I'm writing a blog; but - Do it right the first time.
I'm going to start from the default activity
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
and add a method to bind controls.
Normally we want to write the test first; but we're at a boundary layer. Tests are harder.
And that sounds like a cop out. It really does... I don't have a good way to TDD these.
Additionally; the only thing I could do is testing the implementation - so; gonna see where this goes without it.
I'm still looking for having test coverage that breaks if we delete the code; this just might end up being the Appium tests.
Adding; the method to bind the control is pretty simple/straight forward - Bind it.
private void bindViews() {
topStoryCount = (TextView)findViewById(R.id.top_story_count);
}
Now to get data; This is where our Bridge comes into play. This is the class responsible for putting data into the View. We'll be able to unit test this class.
A question that comes up for me is; "Do I want to drive from the UI down? or... Knowing that I'll need the VBM layers, go from bottom up?"
I normally want to build bottom up; hence the network layer first. I find it easier to build when working on the foundational stuff first; but this is the UI layer; it makes sense to work top to bottom. Though I expect it'll end up being driven by the Bridge; which if you've read the VBM post; know I consider it the zipper; so it touches both sides.
I'm writing some tests against the constructor of the MainActivityBridge
. This will (likely) evolve into what I call "double constructor dependency injection". Once that comes about; I'll show it, but I don't want to force it in; gotta get the tests in place for it to evolve.
Tangent: Integration vs Unit - Testing
As I'm working on how to test make our Appium Test pass; I'm TDDing up the Bridge class and find an internal debate going on about how to TDD this.
Do I create the Dependency Injection double constructor so that I can fake/mock the bookends; or do I do the tests as integration tests; have the test run all the way down to the network and have it faked there.
... I've spent a bit of time thinking about this; I favor the integration style tests. Here's what got me to favor them.
- The second constructor is just for unit tests; that always feels wrong; but I can accept a little intrusion for tests.
- Using a Mock/Fake ends up testing implementation.
- Doesn't (or is hacked in) async behavior
I still have a strong urge; and would love some discussion with the more experienced about this; where does the unit test of implementation get dropped in favor of integration tests? What kinda points is that switch made?
I'm going to work with integration style tests for now; they feel like they produce cleaner code.
I'm bad at the constant checking bit...
I've got the View-Bridge-Mediator set up; and all tests pass... except... I wasn't expecting it to. Part of running with the integration tests I expected to have to set up the unit tests with the MockWebServer
and configure that as we have for the network layer.
This unexpectedly passing test was a shock - some quick debugging and... DUH... It's making an actual network request - we've got a Functional Test going on. I don't want this. Thought THIS test is unlikely to be flakey; no need to hammer the API when all I need is some fake data. The glorious bit of this; and it speaks to the power of having some functional tests - My API interface for Retrofit was wrong.
5 lines of code - bug.
/* package */ interface ItemApi {
String URL = "https://hacker-news.firebaseio.com/v0/";
@GET("/topstories.json")
Call<Items> topStories();
}
Can you see it? It might take some experience with Retrofit to nail it down. I won't do the full debug adventure; but the URL that was getting called was https://hacker-news.firebaseio.com/topstories.json
.
...
OK - It's in the @GET
annotation; the leading /
tells Retrofit that this is expected at the root of the path.
Removing the /
resolves the issue:
/* package */ interface ItemApi {
String URL = "https://hacker-news.firebaseio.com/v0/";
@GET("topstories.json")
Call<Items> topStories();
}
But now - We're getting rid of the network.
Now commit on github we have the fake network.
OK; Mediator does it's job - Back to the Bridge... Which we now know needs the fake network in place to behave as I'm expecting.
Since more of the Bridge was wired up for the test; it's how we got to requiring the Mediator; getting the test going was very simple.
If you check the above linked commit; and this commit you'll see a lot of refactoring of the unit tests. I started to C&P the fake networking code and C&P is a smell; so that all got refactored into helper classes. If you call things "helper" or "util" I'll want to delete the code.
All tests pass!
If you take a look at the MainActivityBridge
from the last commit; particularly line #13; you'll see that I'm doing new MainActivityMediator(...)
inline; instead of in the constructor. While the constructor is where I'll move it to; TECHNICALLY - this is the simplest code to get it working; there's no need for it in the constructor.
This is TDD; get it working; why move it? It's not duplicative; it's not needed anywhere; not persistence required... Looks fine to me.
We all can expect this to change later; but it's not required to be different now; and I can't make it simpler.
I've intentionally set up that method to fail our Appium Test. It set's the control to "value"
and not a number. This exemplifies how focusing on a narrow strip won't guarantee correctness. The code under tests functions perfectly. The app will still break. Also highlights how integration and functional tests can find issues that slip through unit testing.
All my Android Studio Unit Tests pass; let's go back to Appium and ... failed. But not via "value"
it still tried to parse the original value. Looks like it's not waiting for the network round trip. Wonder how to fix that...
I have a delay waiting for the text value to change... It's important to note that actually triggering the data load is useful...
Rebuilding the app to be sure all changes are in place...
HAHA... Internet permission...
OK; the wait worked; it failed on trying to parse what we expected
java.lang.NumberFormatException: For input string: "value"
Go us!
Now to wire it up to return the correct value...
Slight modifications to the MainActivityBridge
to take the Items
from the Mediator.
Let's build and Appium test!
PASSING! BOO-YAH!
It's kinda awesome. In the test
@Test
public void sampleTest(){
final WebDriverWait wait = new WebDriverWait(driver, 10);
final WebElement txt = driver.findElement(By.id("top_story_count"));
final String txtValue = txt.getText();
wait.until(new ExpectedCondition<Boolean>() {
@Override
public Boolean apply(final WebDriver webDriver) {
return !txtValue.equals(webDriver.findElement(By.id("top_story_count")).getText());
}
});
Integer.parseInt(txt.getText());
}
I don't care what the value is; just that it's an integer. While this test will go away; this is my approach to UI testing; exact values are fragile.
We now have a fully functional test along with our unit and integration tests. This is awesome!
And the end of this post...
Summary
Functional and TDD
Appium testing is set up and ready to go. It can take a lot of work in the app to make a functional test work.
If this work is done TDD; as we did here; then it's going to be fine to have these functional tests in place. If there is just the functional test; then the innards will go putrid as all code does, but at an accelerated rate. I make this accelerated claim because the developers will be under the false impression that the test covered code results in good code.
TDD is an approach to developing code; a mindset. The tests are the least important part of TDD. Any belief that tests ensure quality is a dilusion.
I'll probably write a post on that later.
We started the implementation of the View-Bridge-Mediator pattern to implement just one test. While there was an eye towards this pattern; it's what I've found falls out cleanly when not attempting to force other conventions; which does include databinding as a convention.
The project at this point can be cloned from here for reference.