GithubHelp home page GithubHelp logo

azimolabs / conditionwatcher Goto Github PK

View Code? Open in Web Editor NEW
218.0 8.0 25.0 164 KB

Android tool which helps to synchronise application behaviours with test thread in automation tests.

Java 100.00%
espresso android hacktoberfest

conditionwatcher's Introduction

ConditionWatcher

Article explaining

Simple class created in order to make Android automation testing easier, faster, cleaner and more intuitive. It synchronizes operations that might occur on any thread - with test thread. ConditionWatcher can be used as a replacement to Espresso's IdlingResources or it can work in parallel with them.

When we started our adventure with Android Espresso, we came across with various problems connected to IdlingResources. Before we were able to understand how Espresso works on lower layer and explain behaviour of IdlingResources, we created our own tool beforehand and based tests on it. As we can see now principle of operation is very similar, yet we would like present to you perks of ConditionWatcher as they might become useful to you.

Related article: Wait for it… IdlingResource and ConditionWatcher

Quick Start

If you want to check how ConditionWatcher works by yourself, you can run tests included in sample project.

  1. Pull ConditionWatcher project from GitHub

  2. Connect Android device or start AVD.

  3. Run test examples included in sample project by:

    a) Android Studio - locate test files: ConditionWatcherExampleTests.java and IdlingResourceExampleTests.java. Select test class that you want to launch. Choose Run task from menu that appears after right-clicking it.

    TestPackage

    b) Command line - open terminal and change directory to the root of the ConditionWatcher project. Execute ./gradlew sample:connectedCheck to run whole test suite.

Usage

ConditionWatcher doesn't need any setup. After classes are added to your project you are ready to go.

ConditionWatcher.java - performs wait

Singleton which is automatically created after call to any of it's methods. It is meant to be destroyed along with test process. To make test code wait, you need to call waitForCondition(Instruction instruction) method. ConditionWatcher will check if the expected state is achieved with 250ms interval and throw timeout exception after 60 seconds. Those values can be easly changed and each wait case can be handled separately.

Instruction.java - informs what to wait for

It provides ConditionWatcher with information what should be scanned and when conditions are met. ConditionWatcher will keep calling Instrucion's checkCondition() method with interval until it returns true. Furthermore Instruction contains getDescription() method where you can place additional logs (for example dump of elements connected to your current wait).

ConditionWatcher in test code looks like that:

@Test
public void shouldDisplayServerDetails_conditionWatcher() throws Exception {
    List<Server> servers = DataProvider.generateServerList();
    Server thirdServer = servers.get(2);

    // SplashActivity
    ConditionWatcher.waitForCondition(new BtnStartAnimationInstruction());
    onView(withId(R.id.btnStart)).perform(click());

    // ListActivity
    ConditionWatcher.waitForCondition(new ServerListLoadingInstruction());
    onData(anything()).inAdapterView(withId(R.id.lvList)).atPosition(2).perform(click());

    // DetailsActivity
    ConditionWatcher.waitForCondition(new LoadingDialogInstruction());
    onView(withText(thirdServer.getName())).check(matches(isDisplayed()));
    onView(withText(thirdServer.getAddress())).check(matches(isDisplayed()));
    onView(withText(thirdServer.getPort())).check(matches(isDisplayed()));
}
    

Example of one instruction which waits until loading dialog disappear from view hierarchy:

public class LoadingDialogInstruction extends Instruction {
    @Override
    public String getDescription() {
        return "Loading dialog shouldn't be in view hierarchy";
    }

    @Override
    public boolean checkCondition() {
        Activity activity = ((TestApplication)
                InstrumentationRegistry.getTargetContext().getApplicationContext()).getCurrentActivity();
        if (activity == null) return false;

        DialogFragment f = 
            (DialogFragment) activity.getFragmentManager().findFragmentByTag(LoadingDialog.TAG);
        return f == null;
    }
}

Full code can be found in provided sample project. It also contains the same test created with usage of IdlingResources with exactly the same logic to pinpoint the differences.

Why ConditionWatcher?

- It is fast! - save time on synchronization

You are provided with setWatchInterval() method which allows you to change interval with which ConditionWatcher checks if all conditions are met and if test can continue. In comparison to IdlingResource if method isIdleNow() returns false, framework waits 5 seconds before it checks your condition again. It doesn't only limits your options but also forces test to wait without real need.

- It is clean! - reduce number of lines in your code

ConditionWatcher's wait Instruction objects are reusable. Consequently you don't need to unregister them before next usage. IdlingResource is added to List stored in IdlingResourceRegistry class. That List can't store two objects with the same name. Furthermore once IdlingResource is idled, it becomes unusable until you unregister it and register again.

- Less restrictions! - you are the master of your own test

IdlingResource after registered within IdlingResourceRegistry starts to wait for app's idle state only after Espresso method was called within test. So you have to use either perform() or check() to wait for idle. ConditionWatcher is not connected to Espresso framework. It uses simple Thread.sleep() method which makes your test thread to wait until condition is met. You can decide what to wait for, how and when.

- It is easy! - be creative, modify the way you like

ConditionWatcher consists of 51 lines of code. It is very easy to understand and you can adjust it to your needs. Each app is different after all.

- It is intuitive! - there is no black box part

You don't have to be worried of unexpected behaviours. ConditionWatcher will stop waiting for your condition instantly after method checkCondition() returns true. If you haven't noticed it yet, IdlingResource's method isIdleNow() is still being called a few times (with ~1ms interval) after it returns true.

Download

Library dependency

dependencies {
  androidTestImplementation 'com.azimolabs.conditionwatcher:conditionwatcher:0.2'
}

Java code

If you don't want to add another dependency to your project, just copy ConditionWatcher.java and Instruction.java classes to your source directory.

License

Copyright (C) 2016 Azimo

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Towards financial services available to all

We’re working throughout the company to create faster, cheaper, and more available financial services all over the world, and here are some of the techniques that we’re utilizing. There’s still a long way ahead of us, and if you’d like to be part of that journey, check out our careers page.

conditionwatcher's People

Contributors

fisherkk avatar frogermcs avatar janstoltman avatar phillab avatar vbauer 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  avatar  avatar  avatar

conditionwatcher's Issues

Proposal: Include shorthand fluent functions to more closely match idiomatic Espresso

Thanks for ConditionWatcher! It really helped get me away from littering production code with references to Idling Resources.

In our project (which uses Kotlin), we've added some helper functions to allow us to tie directly into Espresso's functions so that we can mimic the withId() and .check(matches(...)) syntax. This allows us to do this:

waitForView(withId(R.id.searchfield_clearbutton)).toMatch(allOf(isDisplayed(), isEnabled()))

This eliminates the need for having to deal with activity or test context, and the implementation is fairly trivial (again, this is Kotlin):

/**
 * Waits for the view specified by the matcher meats the specified condition
 */
fun waitForView(viewMatcher: Matcher<View>) = ViewMatcherWaiter(viewMatcher)

/**
 * Used by the waitForView() shorthand fluent function
 */
class ViewMatcherWaiter constructor(val viewMatcher: Matcher<View>) {
    /**
     * Specify the Espresso matches which will satisfy the condition
     */
    fun toMatch(viewChecker: Matcher<View>) = waitForCondition(object : Instruction() {
        override fun getDescription(): String {
            val desc = StringDescription()
            desc.appendText("Wait for view ")
            viewMatcher.describeTo(desc)
            desc.appendText(" to match ")
            viewChecker.describeTo(desc)
            return desc.toString()
        }

        override fun checkCondition(): Boolean {
            return try {
                onView(viewMatcher).check(matches(viewChecker))
                true
            } catch (x: AssertionError) {
                false
            }
        }
    })
}

It also produces nice error messages on timeout:

java.lang.Exception: Wait for view with id: your.package.here.mock:id/searchfield_clearbutton to match (is displayed on the screen to the user and is enabled) - took more than 5 seconds. Test stopped.

This feels very natural to people familiar with Espresso testing and I think it would make a nice addition to your library. Your matchers can get as complex as Espresso allows without having to write custom Instruction classes.

Make minsdk smaller

Thank you for this nice approach to ui testing.

Could you please make minsdk smaller? 16 is too strict.

Wait for button Visibility To matches

Guys, I've added your tool to my test, I need my test waits and checks Visibility of 2 buttons then finishes the test.

In my test:

ConditionWatcher.waitForCondition(new BtnSendVerificationInstruction());
onView(withId(R.id.btnSendVerification)).perform(click());`

The Instruction:

public class BtnSendVerificationInstruction extends Instruction {
@Override
public String getDescription() {
    return "BtnSendVerification should be moved to center of activity";
}

@Override
public boolean checkCondition() {
    Activity activity = ((TestApplication)
            InstrumentationRegistry.getTargetContext().getApplicationContext()).getCurrentActivity();
    if (activity == null) return false;

    Button btnSendVerification = (Button) activity.findViewById(R.id.btnSendVerification);
    boolean isDisplayed = btnSendVerification.isShown();
    return btnSendVerification != null && isDisplayed;
}
}

Any idea?
Thanks

checkCondition call after return stub response

Adnroid Studio 3.3.

In my app I use Retrofit to do http request. Also I use MockWebServer to return stub response.
On activity when I click button I start async (call.enqueue(callback);) http request (by Retrofit 2) and wait callback method to return response. Here Espresso's test:

@RunWith(AndroidJUnit4::class)
class AddTraderActivityTest {
@get:Rule
    var addTraderIntentTestRule: IntentsTestRule<AddTraderActivity> = IntentsTestRule(AddTraderActivity::class.java)

private val baseEditText = viewWithId(baseTextInputEditText)
    private val quoteEditText = viewWithId(quoteTextInputEditText)
    private val buttonStart = viewWithId(startButton)

    @Before
    fun setup() {
        mockServer = MockWebServer()
        mockServer.start(8081)
        Debug.d(TAG, "SUCCCESS_START_MOCKWEBSRVER")
    }

  @Test
fun buttonStart_click_longResponse() {
    // stub response
    mockServer.enqueue(MockResponse()
            .setResponseCode(200)
            .setBody(FileUtil.getStringFromFile(context, "add_trader_success_200.json"))
            .setBodyDelay(5000, TimeUnit.MILLISECONDS))

    onView(withId(R.id.baseTextInputEditText))
            .perform(typeText(BASE_TEST))
    onView(withId(R.id.quoteTextInputEditText))
            .perform(typeText(QUOTE_TEST))
    onView(withId(R.id.startButton))
            .perform(click())
    onView(withText(R.id.containerProgressBarLayout))
            .check(matches(isDisplayed()))
}

But problem is when execute perform(click()) the method check is not call until not get stub response (after 5 seconds).
But I need call method check immediately after perform(click()) method. Because I need to check is containerProgressBarLayout is isDisplayed() while not return stub response. I need to check my view DURING loading data.
So to do this I try this:

   @Test
    fun buttonStart_click_longResponse() {
        // stub response
        mockServer.enqueue(MockResponse()
                .setResponseCode(200)
                .setBody(FileUtil.getStringFromFile(context, "add_trader_success_200.json"))
                .setBodyDelay(5000, TimeUnit.MILLISECONDS))

        baseEditText.type(BASE_TEST)
        quoteEditText.type(QUOTE_TEST)
        buttonStart.click()
        Debug.d(TAG, "AFTER_CLICK")
        ConditionWatcher.waitForCondition(object : Instruction() {
            override fun getDescription(): String {
                return "test description"
            }

            override fun checkCondition(): Boolean {
                Debug.d(TAG, "checkCondition")
                return true
            }
        })
        onView(withText(containerProgressBarLayout))
                .check(matches(isDisplayed()))
		}

		

But log AFTER_CLICK and method checkCondition() call AFTER 5 SECONDS.

When call click then call this production code:

TransportService.executeTraderOperation(Trader.Operation.CREATE, base.trim(), quote.trim(), new DefaultRestClientCallback<Void>() {
            @Override
            public void onSuccess(Response<Void> response) {
            }

            @Override
            public void onError(ErrorResponse errorResponse) {
             
            }
        });
    }

and this:

public static void executeTraderOperation(Trader.Operation traderOperation, String base, String quote, Callback<Void> callback) {
        TraderMonitorRestClient traderMonitorRestClient = RestClientFactory.createRestClient(TraderMonitorRestClient.class);
        String sender = BuildConfig.APPLICATION_ID + "_" + BuildConfig.VERSION_NAME;
        String key = DateUtil.getDateAsString(new Date(), "mmHHddMMyyyy");
        Call<Void> call = traderMonitorRestClient.executeTraderOperation(traderOperation.toString().toLowerCase(), base, quote, sender, key);
        // asynchronously
        call.enqueue(callback);
    }

fun ViewInteraction.click(): ViewInteraction = perform(ViewActions.click())

fun viewWithId(@IdRes resId: Int): ViewInteraction {
    return onView(withId(resId))
}

Make it easy to get the latest activity

First, thanks for this tool! IdlingResource is a total mess.

Now to business, from the documentation I was not able to understand how to get the latest activity.

However, I found a different way, which follows:

    //ref: http://stackoverflow.com/a/38990078/689223
    static private Activity getCurrentActivity(){
        Collection<Activity> resumedActivity = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED);

        for(Activity act : resumedActivity){
            return act;
        }
        return null;
    }

    static private Activity getCurrentActivitySafe(){
        Callable<Activity> callable = new Callable<Activity>() {
            @Override
            public Activity call() throws Exception {
                return getCurrentActivity();
            }
        };

        FutureTask<Activity> task = new FutureTask<>(callable);
        getInstrumentation().runOnMainSync(task);

        try {
            return task.get(); // Blocks
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

Example on how to use:

@Override
public boolean checkCondition() {
     Activity currentActivity = getCurrentActivitySafe();
    (...)
}

getCurrentActivitySafe() has to be called instead getCurrentActivity() because checkCondition can run outside of the UI thread, causing an error.

My suggestion is to make getCurrentActivitySafe() an utility function, or to rename it to getCurrentActivity() and make it a method of Instruction.
Another alternative is to make checkCondition a method that receives the current activity as an argument, like this checkCondition(Activity currentActivity)

Replace sleep with delay

The better approach will be to convert in kotlin and use delay so it does not block the main thread and also we could do parallel testing as well

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.