-
Notifications
You must be signed in to change notification settings - Fork 664
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Test automation starter #44
Changes from all commits
f4ec208
aebd2b2
4841f0f
9bd38a8
014061e
dcd75d5
30711f7
00d4fdb
a6abf1e
6836161
7ad5d75
309a3a7
f33e9d8
217b15b
700d476
4228a78
5d603ce
5e2f7d4
1d14e36
52a6235
fc234ea
b1a48a8
7bd4780
b226bb6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
*.iml | ||
.gradle | ||
/local.properties | ||
/.idea/workspace.xml | ||
/.idea/libraries | ||
.DS_Store | ||
/build | ||
/captures | ||
.externalNativeBuild |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
## Overview | ||
|
||
A sample retail store app that showcases current testing methodologies for Android. It comes with a simple Heroku server that can be run locally. | ||
|
||
The aim is to provide a hands-on introduction to those new to test engineering and a challenge for those who want to further improve the current implementation. | ||
|
||
|
||
## Getting started | ||
|
||
Before launching the app you'll need to start the server. | ||
|
||
Navigate to the `simplesinatraapi` directory in this project. | ||
Make sure you have Ruby installed by running `ruby -v` from the terminal. If it's not installed run `brew install ruby` | ||
|
||
Next, run `bundle install` | ||
|
||
Finally, run `ruby server.rb` | ||
|
||
|
||
You can check if the server is running by going to http://localhost:4567/ where you should see **Sinatra doesn’t know this ditty.** | ||
|
||
Now all you need to do is run the app from Android Studio! | ||
|
||
## Tests | ||
|
||
We have tests to validate both the UI and the JSON Schema. The first are in `src/androidTest/java/com/novoda/androidstoreexample` and the latter in `src/test/java/com/novoda/androidstoreexample` | ||
|
||
To find out more about our approach to UI tests, see https://docs.google.com/document/d/1S6f3tVwB0se1Xe3qc1Cv4I5ZVch-7lKJiuVgW_ktLkY/edit (to be replaced with wiki pages) | ||
|
||
## Current limitations | ||
|
||
* The content from the server can only be viewed on an emulator. | ||
|
||
* Right now we only have data for the "Hats" section. | ||
|
||
* The shopping basket is still work in progress. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/build |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
apply plugin: 'com.android.application' | ||
apply plugin: 'kotlin-android' | ||
apply plugin: 'kotlin-android-extensions' | ||
apply plugin: 'kotlin-kapt' | ||
|
||
def API_URL = 'API_URL' | ||
def STRING = 'String' | ||
|
||
kapt { | ||
generateStubs = true | ||
} | ||
|
||
android { | ||
compileSdkVersion 26 | ||
defaultConfig { | ||
applicationId "com.novoda.testautomationstarter" | ||
minSdkVersion 26 | ||
targetSdkVersion 26 | ||
versionCode 1 | ||
versionName "1.0" | ||
buildConfigField STRING, API_URL, '"http://10.0.2.2:4567"' | ||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" | ||
} | ||
buildTypes { | ||
release { | ||
minifyEnabled false | ||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' | ||
} | ||
} | ||
|
||
} | ||
|
||
dependencies { | ||
implementation fileTree(include: ['*.jar'], dir: 'libs') | ||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" | ||
implementation 'com.android.support:appcompat-v7:26.1.0' | ||
implementation 'com.android.support.constraint:constraint-layout:1.0.2' | ||
implementation 'com.android.support:recyclerview-v7:26.1.0' | ||
kapt 'com.google.dagger:dagger-compiler:2.11' | ||
implementation 'com.google.dagger:dagger:2.11' | ||
implementation 'com.squareup.retrofit2:retrofit:2.3.0' | ||
implementation 'com.squareup.retrofit2:converter-moshi:2.3.0' | ||
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0' | ||
testImplementation 'junit:junit:4.12' | ||
testImplementation 'io.rest-assured:rest-assured:3.0.6' | ||
testImplementation 'io.rest-assured:json-schema-validator:3.0.6' | ||
androidTestImplementation 'com.android.support.test:runner:1.0.1' | ||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' | ||
androidTestImplementation 'com.squareup.okhttp:mockwebserver:1.3.0' | ||
androidTestImplementation 'org.mockito:mockito-core:2.8.9' | ||
androidTestImplementation('com.android.support.test.espresso:espresso-contrib:3.0.1') { | ||
exclude module: 'support-annotations' | ||
exclude module: 'support-v4' | ||
exclude module: 'design' | ||
exclude module: 'recyclerview-v7' | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# Add project specific ProGuard rules here. | ||
# You can control the set of applied configuration files using the | ||
# proguardFiles setting in build.gradle. | ||
# | ||
# For more details, see | ||
# http://developer.android.com/guide/developing/tools/proguard.html | ||
|
||
# If your project uses WebView with JS, uncomment the following | ||
# and specify the fully qualified class name to the JavaScript interface | ||
# class: | ||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview { | ||
# public *; | ||
#} | ||
|
||
# Uncomment this to preserve the line number information for | ||
# debugging stack traces. | ||
#-keepattributes SourceFile,LineNumberTable | ||
|
||
# If you keep the line number information, uncomment this to | ||
# hide the original source file name. | ||
#-renamesourcefileattribute SourceFile |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
{ | ||
"categories": [ | ||
{ | ||
"title": "SHIRTS", | ||
"image": "shirtimage" | ||
}, | ||
{ | ||
"title": "Thingies to wear", | ||
"image": "hoodieimage" | ||
}, | ||
{ | ||
"title": "HATS", | ||
"image": "hatimage" | ||
} | ||
] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
Feature: Product Category list | ||
|
||
Scenario: User can see the Product Category list | ||
Given The user has launched the app | ||
Then The user can see the following Product Categories: | ||
| Shirts | | ||
| Hoodies | | ||
| Hats | | ||
| Digital Goods | | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package com.novoda.androidstoreexample | ||
|
||
import com.novoda.androidstoreexample.dagger.module.HostModule | ||
|
||
class EspressoHostModule(val port: Int) : HostModule() { | ||
override fun provideBaseUrl(): String { | ||
return "http://127.0.0.1:$port" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package com.novoda.androidstoreexample | ||
|
||
import android.support.test.espresso.matcher.BoundedMatcher | ||
import android.support.v7.widget.RecyclerView | ||
import com.novoda.androidstoreexample.adapters.CategoryAdapter | ||
import com.novoda.androidstoreexample.adapters.ProductListAdapter | ||
import org.hamcrest.Description | ||
import org.hamcrest.Matcher | ||
|
||
object ViewMatchers { | ||
|
||
@JvmStatic | ||
fun withCategoryTitle(title: String): Matcher<RecyclerView.ViewHolder> { | ||
return object : BoundedMatcher<RecyclerView.ViewHolder, CategoryAdapter.Holder>(CategoryAdapter.Holder::class.java) { | ||
override fun matchesSafely(item: CategoryAdapter.Holder): Boolean { | ||
return item.categoryName?.let { it.text.toString().equals(title, true) } ?: false | ||
} | ||
|
||
override fun describeTo(description: Description) { | ||
description.appendText("view holder with title: $title") | ||
} | ||
} | ||
} | ||
|
||
@JvmStatic | ||
fun withProductTitle(title: String): Matcher<RecyclerView.ViewHolder> { | ||
return object : BoundedMatcher<RecyclerView.ViewHolder, ProductListAdapter.Holder>(ProductListAdapter.Holder::class.java) { | ||
override fun describeTo(description: Description) { | ||
description.appendText("view holder with title: $title") | ||
} | ||
|
||
override fun matchesSafely(item: ProductListAdapter.Holder?): Boolean { | ||
return item?.productName?.let { it.text.toString().equals(title, true) } ?: false | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package com.novoda.androidstoreexample.pageobjects | ||
|
||
import android.support.test.espresso.Espresso.onView | ||
import android.support.test.espresso.ViewInteraction | ||
import android.support.test.espresso.assertion.ViewAssertions.matches | ||
import android.support.test.espresso.matcher.ViewMatchers.isDisplayed | ||
import android.support.test.espresso.matcher.ViewMatchers.withId | ||
import com.novoda.androidstoreexample.R | ||
|
||
class HeaderPageObject { | ||
private val HEADER: ViewInteraction = onView(withId(R.id.action_bar_container)) | ||
|
||
fun verifyHeader() { | ||
HEADER.check(matches(isDisplayed())) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package com.novoda.androidstoreexample.pageobjects | ||
|
||
import android.support.test.espresso.Espresso.onView | ||
import android.support.test.espresso.action.ViewActions.click | ||
import android.support.test.espresso.assertion.ViewAssertions.matches | ||
import android.support.test.espresso.contrib.RecyclerViewActions.actionOnHolderItem | ||
import android.support.test.espresso.matcher.ViewMatchers.isDisplayed | ||
import android.support.test.espresso.matcher.ViewMatchers.withId | ||
import com.novoda.androidstoreexample.R | ||
import com.novoda.androidstoreexample.ViewMatchers | ||
|
||
class MainActivityPageObject { | ||
|
||
private val TITLE = onView(withId(R.id.titleTextView)) | ||
|
||
fun verifyTitle() { | ||
TITLE.check(matches(isDisplayed())) | ||
} | ||
|
||
fun navigateToProductList() { | ||
val categoryMatcher = ViewMatchers.withCategoryTitle("HATS") | ||
|
||
onView(withId(R.id.categoryListView)) | ||
.perform(actionOnHolderItem(categoryMatcher, click())) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package com.novoda.androidstoreexample.pageobjects | ||
|
||
import android.support.test.espresso.Espresso.onView | ||
import android.support.test.espresso.assertion.ViewAssertions.matches | ||
import android.support.test.espresso.matcher.ViewMatchers.isDisplayed | ||
import android.support.test.espresso.matcher.ViewMatchers.withId | ||
import com.novoda.androidstoreexample.R | ||
|
||
class ProductDetailsPageObject { | ||
|
||
fun assertProductDetailsDisplayed() { | ||
onView(withId(R.id.productDetailDescription)).check(matches(isDisplayed())) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package com.novoda.androidstoreexample.pageobjects | ||
|
||
import android.support.test.espresso.Espresso.onView | ||
import android.support.test.espresso.action.ViewActions | ||
import android.support.test.espresso.contrib.RecyclerViewActions | ||
import android.support.test.espresso.matcher.ViewMatchers.withId | ||
import com.novoda.androidstoreexample.R | ||
import com.novoda.androidstoreexample.ViewMatchers | ||
|
||
class ProductListPageObject { | ||
|
||
fun navigateToProductDetails() { | ||
val productMatcher = ViewMatchers.withProductTitle("hat white") | ||
|
||
onView(withId(R.id.productListView)).perform(RecyclerViewActions.actionOnHolderItem(productMatcher, ViewActions.click())) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package com.novoda.androidstoreexample.tests; | ||
|
||
import android.support.test.rule.ActivityTestRule; | ||
import android.support.v7.widget.RecyclerView; | ||
|
||
import com.novoda.androidstoreexample.R; | ||
import com.novoda.androidstoreexample.ViewMatchers; | ||
import com.novoda.androidstoreexample.activities.MainActivity; | ||
|
||
import org.hamcrest.Matcher; | ||
import org.junit.Rule; | ||
import org.junit.Test; | ||
|
||
import static android.support.test.espresso.Espresso.onView; | ||
import static android.support.test.espresso.action.ViewActions.click; | ||
import static android.support.test.espresso.assertion.ViewAssertions.matches; | ||
import static android.support.test.espresso.contrib.RecyclerViewActions.actionOnHolderItem; | ||
import static android.support.test.espresso.contrib.RecyclerViewActions.scrollToHolder; | ||
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; | ||
import static android.support.test.espresso.matcher.ViewMatchers.withId; | ||
|
||
public class EspressoJavaTestExample { | ||
|
||
@Rule | ||
public ActivityTestRule<MainActivity> activityTestRule = new ActivityTestRule<MainActivity>(MainActivity.class); | ||
|
||
@Test | ||
public void firstNavigationTest() { | ||
Matcher<RecyclerView.ViewHolder> categoryMatcher = ViewMatchers.withCategoryTitle("HATS"); | ||
|
||
onView(android.support.test.espresso.matcher.ViewMatchers.withId(R.id.categoryListView)).perform(scrollToHolder(categoryMatcher), actionOnHolderItem(categoryMatcher, click())); | ||
|
||
Matcher<RecyclerView.ViewHolder> productMatcher = ViewMatchers.withProductTitle("hat white"); | ||
|
||
onView(withId(R.id.productListView)).perform(scrollToHolder(productMatcher), actionOnHolderItem(productMatcher, click())); | ||
|
||
onView(withId(R.id.productDetailDescription)).check(matches(isDisplayed())); | ||
|
||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package com.novoda.androidstoreexample.tests | ||
|
||
import android.support.test.rule.ActivityTestRule | ||
import com.novoda.androidstoreexample.activities.MainActivity | ||
import com.novoda.androidstoreexample.pageobjects.MainActivityPageObject | ||
import com.novoda.androidstoreexample.pageobjects.ProductDetailsPageObject | ||
import com.novoda.androidstoreexample.pageobjects.ProductListPageObject | ||
import org.junit.Rule | ||
import org.junit.Test | ||
|
||
class EspressoTestExample { | ||
|
||
private val mainActivityPageObject = MainActivityPageObject() | ||
private val productListPageObject = ProductListPageObject() | ||
private val productDetailsPageObject = ProductDetailsPageObject() | ||
|
||
private val activityTestRule = ActivityTestRule<MainActivity>(MainActivity::class.java) | ||
|
||
@get:Rule | ||
var activityRule: ActivityTestRule<MainActivity> = activityTestRule | ||
|
||
@Test | ||
fun firstNavigationTest() { | ||
|
||
mainActivityPageObject.navigateToProductList() | ||
|
||
productListPageObject.navigateToProductDetails() | ||
|
||
productDetailsPageObject.assertProductDetailsDisplayed() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package com.novoda.androidstoreexample.tests | ||
|
||
import android.support.test.rule.ActivityTestRule | ||
import com.novoda.androidstoreexample.activities.MainActivity | ||
import com.novoda.androidstoreexample.pageobjects.MainActivityPageObject | ||
import com.novoda.androidstoreexample.pageobjects.ProductDetailsPageObject | ||
import com.novoda.androidstoreexample.pageobjects.ProductListPageObject | ||
import org.junit.Rule | ||
import org.junit.Test | ||
|
||
class EspressoUserFlowTests { | ||
|
||
private val mainActivityPageObject = MainActivityPageObject() | ||
private val productListPageObject = ProductListPageObject() | ||
private val productDetailsPageObject = ProductDetailsPageObject() | ||
|
||
private val activityTestRule = ActivityTestRule<MainActivity>(MainActivity::class.java) | ||
|
||
@get:Rule | ||
var activityRule: ActivityTestRule<MainActivity> = activityTestRule | ||
|
||
@Test | ||
fun navigateToProductDetailsTest() { | ||
|
||
mainActivityPageObject.navigateToProductList() | ||
|
||
productListPageObject.navigateToProductDetails() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd extract to which product you're navigating to a parameter (cause it shouldn't be the responsibility of a page object to know which one), same for the assertions There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These will be rewritten in https://novoda.atlassian.net/browse/PT-789 |
||
|
||
productDetailsPageObject.assertProductDetailsDisplayed() | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I saw some page objects before, how come this test uses directly espresso?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just replied in the previous comment before seeing this one 😅