Giter Site home page Giter Site logo

kappuccino's Introduction

codebeat badge bitrise badge Download

kappuccino

A framework to simplify the way you do instrumentation tests in your app, using Espresso and Kotlin.

Here is how you do instrumentation tests today, using simply Espresso:

@Test fun loginFieldsAreVisible() {
  onView(withId(R.id.username)).check(matches(isDisplayed())
  onView(withId(R.id.password)).check(matches(isDisplayed())
  onView(withId(R.id.login_button)).check(matches(isDisplayed())
}

This is just to check a simple login screen, and we are not even considering that we may need to scroll to one of these views, due to small screens support.

With scroll, our test will be something like this:

@Test fun loginFieldsAreVisible() {
  onView(withId(R.id.username)).perform(scrollTo()).check(matches(isDisplayed())
  onView(withId(R.id.password)).perform(scrollTo()).check(matches(isDisplayed())
  onView(withId(R.id.login_button)).perform(scrollTo()).check(matches(isDisplayed())
}

We have to repeat a lot of code, this makes the tests hard to read and understand at a first look. Also you may forget some scrollTo(), or mismatch the check function. At the end, all we want to do is check if the views with these ids are displayed.

So, this is how you do the same test with kappuccino library:

@Test fun loginFieldsAreVisible() {
  displayed {
    id(R.id.username)
    id(R.id.password)
    id(R.id.login_button)
  }
}

Cleaner, easier to write and understand. To scroll, all you have to do is pass a parameter to the function:

@Test fun loginFieldsAreVisible() {
  displayed(scroll = true) {
    id(R.id.username)
    id(R.id.password)
    id(R.id.login_button)
  }
}

Installation

1 - Setup kotlin in your project, see the instructions here

2 - Create a kotlin directory into 'src/androidTest/', check the sample code for reference.

3 - Set you sourceDataSet into your build.gradle file

sourceSets {
    androidTest.java.srcDirs = ['src/androidTest/kotlin']
  }

4 - Add library and dependencies into your build.gradle file and sync

androidTestImplementation 'br.com.concretesolutions:kappuccino:$latest.version'

5 - This library depends on the following libraries:

So, ensure those libraries are also in your dependencies.

androidTestImplementation "com.android.support.test.espresso:espresso-intents:$versions.espresso"
androidTestImplementation "com.android.support.test.espresso:espresso-core:$versions.espresso"
androidTestImplementation "com.android.support.test.espresso:espresso-contrib:$versions.espresso"
androidTestImplementation "com.android.support.test.uiautomator:uiautomator-v18:$versions.uiAutomator"

And you're ready to go!

If you have any module conflicts, try to exclude the conflicting module, for example:

androidTestImplementation('br.com.concretesolutions:kappuccino:$latest.version', {
        exclude group: 'com.android.support'
    })

Assertion methods

These are the methods to make view assertions

checked {}
notChecked {}

clickable {}
notClickable {}

selected {}
notSelected {}

displayed {}
notDisplayed {}

notExist {}

Action methods

These are methods to interact with views

click {}
doubleClick {}
longClick {}

typeText {}
clearText {}

Scroll

The scroll method is now a parameter for all the above methods, the default value is false, for example:

@Test fun scrollToButton_andClick() {
  click(scroll = true) {
    id(R.id.login_button)
  }
}

In this case, it will scroll to the view and click. If you don't provide a parameter, the scroll will not happen.

Combine matchers (Matchers.allOf)

To combine multiple matchers, use the allOf method:

@Test fun scrollToButton_andClick() {
  click(scroll = true) {
    allOf {
        id(R.id.login_button)
        text(R.string.login_button_text)
    }
  }
}

Hierarchy

There are two methods of hierarchy matchers: Parent and Descendant.

Parent

You can use Parent method with two different approaches: block matching or combining.

1 - Block matching:
For block matching, pass the parentId as method parameter.

Then, kappuccino will match all the views inside the block:

@Test fun matchParent_blockMatching_example() {
  displayed {
    parent(R.id.parent) {
        id(R.id.username)
        id(R.id.password)
        id(R.id.login_button)
    }
  }
}

Here, kappuccino will check if all the views (username, password and login_button) are descendant of the declared parent, and are displayed.

For better understanding, the code above is equivalent to the one below, using pure Espresso:

@Test fun matchParent_example() {
    onView(
        allOf(isDescendantOf(withId(R.id.parent)), withId(R.id.username)))
        .check(matches(isDisplayed()))
    onView(
        allOf(isDescendantOf(withId(R.id.parent)), withId(R.id.password)))
        .check(matches(isDisplayed()))
    onView(
        allOf(isDescendantOf(withId(R.id.parent)), withId(R.id.login_button)))
        .check(matches(isDisplayed()))
}

2 - Combination of matchers:
You can use the parent method as a combination of matchers:

@Test fun matchParent_combining_example() {
    displayed {
        allOf {
            parent {
                id(R.id.parent)
            }
            id(R.id.username)
        }
    }
}

Here, you will check if the view with id = R.id.username, and with parent with id = R.id.parent, is displayed

Descendant

It works just like the parent method, for both cases (block matching and combining matchers)

@Test fun descendant_block_example() {
    displayed {
        allOf {
            descendant {
                id(R.id.username)
            }
            id(R.id.parent)
        }
    }
}

Here, we'll check if the parent, with child R.id.username is displayed. Same use for block matching.

RecyclerView

To interact with the recycler view:

@Test fun recyclerView_example() {
    recyclerView(R.id.recycler_view) {
        sizeIs(10)
        atPosition(3) {
            displayed {
                id(R.id.item_description)
                text(R.string.description_text)
                text("Item header text")
            }
        }
    }
}

To type text in a RecyclerView item's EditText:

@Test fun recyclerView_textInput_example() {
    recyclerView(R.id.recycler_view) {
        atPosition(0) {
            typeText(R.id.editText, "Position 0")
        }

        atPosition(1) {
            typeText(R.id.editText, "Position 1")
        }
    }
}

To swipe a RecyclerView's item left or right:

@Test fun recyclerView_swipeLeft_example() {
    recyclerView(R.id.recycler_view() {
        atPosition(0) {
            swipeLeft()
        }

        atPosition(1) {
            swipeRight()
        }
    }
}

Menu and action bar

To interact with the options menu:

@Test
fun whenClickingOnItem1_shouldShowCorrectText() {
    menu {
        onItem(R.string.item_1) {
            click()
        }
    }

    displayed {
        text(R.string.item_1_selected)
    }
}

To interact with the action bar:

@Test
fun whenClickingOnActionBarItem_shouldClearText() {
    menu(openOptionsMenu = false) {
        onActionBarItem(R.id.item_clear) {
            click()
        }
    }

    notDisplayed {
        id(R.id.txt_menu)
    }

Matchers

You can use the following matchers:

fun id(@IdRes viewId: Int)
fun text(@StringRes textId: Int)
fun text(text: String)
fun contentDescription(@StringRes contentDescriptionId: Int)
fun contentDescription(contentDescription: String)
fun image(@DrawableRes imageId: Int)
fun textColor(@ColorRes colorId: Int)
fun parent(@IdRes parentId: Int)
fun descendant(@IdRes descendantId: Int)
fun custom(viewMatcher: Matcher<View>) // Here you can pass a custom matcher

TextInputLayout Matchers

You can match TextInputLayout now:

To check if TextInputLayout has an error text

@Test
fun textInputLayout_hasTextError_example() {
    textInputLayout(R.id.textInputLayout) {
        hasTextError()
    }
}

To check error text with text

@Test
fun textInputLayout_checkTextErrorWithText_example() {
    textInputLayout(R.id.textInputLayout) {
         withTextError("example text error")
    }
}

To check error text with an string resource

@Test
fun textInputLayout_checkTextErrorWithResource_example() {
    textInputLayout(R.id.textInputLayout) {
         withTextError(R.string.textError)
    }
}

Intent Matchers

You can match intents easily now:

@Test
fun intentMatcherTest() {
    val WHATS_PACKAGE_NAME = "com.whatsapp"
    val PLAY_STORE_URL = "https://play.google.com/store/apps/details?id="
    Intents.init()
    matchIntent {
        action(Intent.ACTION_VIEW)
        url(PLAY_STORE_URL + WHATS_PACKAGE_NAME)
        result {
           ok()
        }
    }

    click {
        id(R.id.btn_start_activity)
    }

    matchIntent {
        action(Intent.ACTION_VIEW)
        url(PLAY_STORE_URL + WHATS_PACKAGE_NAME)
    }

    Intents.release()
}

If you use some of the result methods (resultOk, resultCanceled, resultData) it's going to be like use the Espresso intending method. If you DON'T use any of the result methods, it's the same as use the Espresso intended method. The above code it will be something like this, without kappuccino

@Test
fun intentMatcherTest() {
    val WHATS_PACKAGE_NAME = "com.whatsapp"
    val PLAY_STORE_URL = "https://play.google.com/store/apps/details?id="
    Intents.init()

    val matcher = allOf(hasAction(Intent.ACTION_VIEW), hasData(Uri.parse(PLAY_STORE_URL + WHATS_PACKAGE_NAME)))
    val result = ActivityResult(Activity.RESULT_OK, null)
    intending(matcher).respondWith(result);

    click {
        id(R.id.btn_start_activity)
    }

    intended(matcher)

    Intents.release()
}

You can also use a custom intent matcher with the custom method

Runtime permissions

Easily handle runtime permissions

@Test
fun grantContactsPermission() {
    click {
        id(R.id.btn_request_permission)
    }

    runtimePermission(Manifest.permission.READ_CONTACTS) {
        allow()
    }

    displayed {
        text("PERMISSION GRANTED")
    }
}

Background matcher

Check view's background. The background must be VectorDrawable, BitmapDrawable or ColorDrawable

@Test
fun backgroundColorTest() {
    displayed {
        allOf {
            id(R.id.view_background)
            background(R.drawable.ic_android)
        }
    }

    displayed {
        background(R.color.colorAccent)
    }
}

For more examples, please check the sample code.

Wiki: coming soon.

Tip: this framework was based on Robots Pattern. It's a good idea to use this framework in combination with this pattern.

LICENSE

This project is available under Apache Public License version 2.0. See LICENSE.

kappuccino's People

Contributors

92alanc avatar alexsoaresdesiqueira avatar cs-alan-camargo avatar cs-bruno-silva avatar hcolangelo-concrete avatar heitorcolangelo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

kappuccino's Issues

Permission actions not working

The grant function is actually denying the permissions, while deny isn't doing anything at all.
This behaviour might have something to do with the index of the button in the dialogue.

In the code, the DENY button is at index 0 and the ALLOW button is at index 1, but when pressing the button at index 1, the test is pressing the first button (DENY), so I guess the permission dialogue doesn't work with zero-based indices.

My suggestion is to deprecate or even delete that permissionUtils file, because in the support library there is a very straightfoward rule that handles permissions. Here is an example:

@Rule
@JvmField
val cameraPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(CAMERA)

Using the support library, this is all that needs to be done in order to grant a permission.

Library package says concretesolutions

This is using the br.com.concretesolutions package. So, I'd like to discuss two points here:

  1. Move the library to github.com/concretesolutions
  2. Update the package to br.com.concrete (much like we are trying to do with canarinho and next is requestmatcher).

I'd like to know what are your thoughts on these points.

Sample code

It would be good if the sample code could me more organized and more complete.

Add Changelog

As the library releases updates quickly, sometimes I feel a bit unconfortable to update it sometimes, as I don't know if the update contains breaking changes or only bug fixes.

Some suggestions is to tag each release and add a changelog to Github's releases tab, or a simpler solution, just by adding a CHANGES.md file to the root folder.

Runtime Permissions

Not working! =(

doWait() runtimePermission(Manifest.permission.ACCESS_COARSE_LOCATION) { deny() } runtimePermission(Manifest.permission.ACCESS_FINE_LOCATION) { deny() } doWait()

logcat:

java.lang.NoClassDefFoundError: Failed resolution of: Landroid/support/test/uiautomator/UiDevice; at br.com.concretesolutions.kappuccino.utils.PermissionUtils.handlePermission(PermissionUtils.kt:41) at br.com.concretesolutions.kappuccino.utils.PermissionUtils.deny(PermissionUtils.kt:29) at br.com.concretesolutions.kappuccino.utils.PermissionUtilsKt.runtimePermission(PermissionUtils.kt:15)

Update intent matcher doc on README

The samples and explanation of how we use Kappuccino to match intents are not up-to-date.
Also, it can be improved, for example (not up-to-date yet, just refactoring):

@Test
fun intentMatcherTest() {
    val WHATS_PACKAGE_NAME = "com.whatsapp"
    val PLAY_STORE_URL = "https://play.google.com/store/apps/details?id="
    Intents.init()

    val matcher = allOf {
    	action(Intent.ACTION_VIEW)
        url(PLAY_STORE_URL + WHATS_PACKAGE_NAME)
    }

    matchIntent {
        matcher
        result {
           ok()
        }
    }

    click {
        id(R.id.btn_start_activity)
    }

    matchIntent {
        matcher
    }

    Intents.release()
}

Migration to mavenCentral

Hello everyone, I'm trying to remove jCenter from my project and its library is not located when I do this, I'd like to know if this migration is in progress or do you have any intentions to migrate?

README turning into a wiki

Our README rocks! It's very complete and clear in every sense, but I think it's a bit too complete.

At least in my opinion a README file should only summarise what the project is about, how to install it, give like 1 or 2 examples, who are the project administrators and how to contribute.

Since this file already has examples for each of the methods we've created so far (or most of them maybe), we could simply move all this detailed information into a wiki, which again in my opinion I think is more appropriate for having detailed info.

What are your thoughts?

textCompoundDrawable matcher is not working

I was updating the gradle to 3.1.2 and after running the test suit the ManipulateTextActivityTest failed.
Turns out that the drawables wasn't on the screen because it seems that the method setCompoundDrawables (ManipulateTextActivity line 18) does not work properly.
So I changed to setCompoundDrawablesWithIntrinsicBounds and then the drawables started to be shown.
But still, the tests keeps failing.

Differentiate between matchIntent methods

Current behaviour is to detect if a matchIntent is intending or intended by the contents of the clojure: whether it uses the result methods will decide upon its use.

I think this is a bit confusing and more implicit than explicit. I understand that both methods take a Matcher<Intent> parameter though they are meant for different things. Even the name match is not exactly what it does: it matches to either validate or stub (as per Espresso docs). So, in my opinion we could have a different DSL here. Something like:

// V1 proposal
// Validating
triggeredIntent { // or maybe sentIntent {}
    action(Intent.ACTION_VIEW)
}

// Stubbing
stubIntent {
    action(Intent.ACTION_VIEW)
} withResult {
    ok()
    intent().setAction(Intent.ACTION_VIEW)
}

// Slight variation
// Stubbing
stubIntent {
    action(Intent.ACTION_VIEW)
} respondWithOk { // or respondWithCanceled {} or respondWith(code) {}
    setAction(Intent.ACTION_VIEW)
    setData(/*...*/)
    // ...
}

// --------------

// V2 proposal
// Validating
 matchIntent {
    action(Intent.ACTION_VIEW)
    url(PLAY_STORE_URL + WHATS_PACKAGE_NAME)
} wasSent() // be explicit though a bit awkward

// Stubbing
 matchIntent {
    action(Intent.ACTION_VIEW)
    url(PLAY_STORE_URL + WHATS_PACKAGE_NAME)
} returnWith { // be explicit though a bit awkward
    ok()
    intent().setAction(Intent.ACTION_VIEW)
}

(I am not too sure how we could elegantly populate the intent. Perhaps follow the "anko" way here would be the best alternative...).

The proposals try to be closer to the espresso intentions and perhaps ease future additions to this API. I prefer V1 IMHO.

Please, feel free to say this makes no sense :)

Change CI tool

Is bitrise a real option?
Don't you think that would be better to know the code that is running on CI?

Reasons to consider a CI with a configuration file

People can understand what CI is doing by looking the file.
They can reproduce CI pipelines locally.
It's easy to migrate between services since there's no need to log in to understand what CI does, it's public and it's on our code.
Others can suggest improvements to CI pipelines.

Possible tools

  • Circle CI
  • Travis CI

Fix Bitrise badge

I think that it's been a while since the bitrise badge it's not displaying any useful data.
It may be necessary to also fix bitrise build workflow as well

Check drawable in a TextView

It would be good if there's a feature that enables to check a drawable (start, top, bottom or end) of TextView

DeteKT or Ktlint

A nice addition to the project would be to have a more standardized code base following either detekt or ktlint. I think detekt is easier to integrate currently than ktlint but that is just my opinion.

Problems with view's visibilities in RecyclerView (View.GONE)

I have a scenario of test where I need the check if a specific view of an item of RecyclerView.Adapter was setted with visibility = View.GONE, so this is my assert:

recyclerView(R.id.recycler_view_id) {
    atPosition(1) {
        notExist { id(R.id.label_id) }
    }
}

but when a run this code above the test fail. I made some asserts to verify the visibility of the R.id.label_id and always return the visibility = View.GONE.

I tried to use notDisplayed, but I got the same result.

The curious point is, when my RecyclerView has only one item and I used notExist { id(R.id.label_id) } without the recycler view wrapper (recyclerView(<id>) { ... }) it worked.

'No tests were found' when try to run all test suit

This is a know issue, that happens sometimes.
When trying to run all tests at once, this message is displayed

No tests found. This usually means that your test classes are not in the form that your test runner expects (e.g. don't inherit from TestCase or lack @test annotations).

But this does not happen when you run a single class, or a single test.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.