【问题标题】:Unit Testing Android app that relies on Robolectric / RxJava / RxAndroid依赖 Robolectric / RxJava / RxAndroid 的 Android 应用程序单元测试
【发布时间】:2015-04-15 18:35:36
【问题描述】:

我正在使用 Robolectric 3.0 快照。

我有一个测试:

@Test
public void my_test() throws Exception {
        when(testReferenceManager.getUserServiceMock().checkUsernameAvailability(anyString())).thenReturn(Observable.just(Arrays.asList(new User("test@email.com", "password"))));
        when(testReferenceManager.getUserServiceMock().checkAuthStatus(anyString())).thenReturn(Observable.just(Arrays.asList(new User("test@email.com", "password"))));

        EditText emailText = (EditText)activity.findViewById(R.id.text_email);
        EditText passwordText = (EditText)activity.findViewById(R.id.text_password);
        Button signInButton = (Button)activity.findViewById(R.id.sign_in_button);


        emailText.setText("test@email.com");
        passwordText.setText("password");

        Robolectric.flushBackgroundScheduler();
        Robolectric.flushForegroundScheduler();    

        assertThat(signInButton.getText()).isEqualTo(App.R.getString(R.string.button_login));
}

这里的关键是,如果 API 报告用户存在(它从上面的模拟中执行),则登录按钮文本应该与名为 R.string.button_login 的字符串资源的值相同。

更改按钮状态的设置在我的 Activity 中完成,如下所示:

        ReactiveEditText.textObservableForTextView(emailText)
            .startWith(emailText.getText().toString())
            .switchMap(new Func1<String, Observable<Boolean>>() {
                @Override
                public Observable<Boolean> call(String username) {
                    return webServices.usernameAvailable(username);
                }
            })
            .subscribe(new EndlessObserver<Boolean>() {
                @Override
                public void onNext(Boolean available) {
                    if (available) {
                        signInButton.setText(getString(R.string.button_signup));
                    } else {
                        signInButton.setText(getString(R.string.button_login));
                    }
                }
            });

ReactiveEditText.textObservableForTextView 只是以反应方式包装了 textChangedListener 接口:

public static Observable<String> textObservableForTextView(final TextView tv) {
        Observable<String> textObservable = Observable.create(new Observable.OnSubscribe<String>() {
            @Override
            public void call(final Subscriber<? super String> subscriber) {
                tv.addTextChangedListener(new TextWatcher() {
                    @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
                    @Override public void afterTextChanged(Editable s) { }

                    @Override
                    public void onTextChanged(CharSequence s, int start, int before, int count) {
                        subscriber.onNext(s.toString());
                    }
                });
            }
        });

        return ViewObservable.bindView(tv, textObservable);
}

usernameAvailable 接收到上面的模拟数据,看起来像这样:

    // If the incoming array is size 0 the username is available, otherwise it already exists. 

public Observable<Boolean> usernameAvailable(final String username) {
    return userService.checkUsernameAvailability(username)
            .map(new Func1<List<User>, Boolean>() {
                @Override
                public Boolean call(List<User> usersMatchingUsername) {
                    return usersMatchingUsername.size() == 0;
                }
            });
}

请注意,我正在使用“即时”调度程序来监听 EditText 的更改。我的困惑是:单元测试总是失败,在我看来(当我跳入调试器时)测试中的 assertThat 语句在观察者看到 EditText 的变化之前就被触发了。我确实在调试器中看到 Observer 最终会触发,并且在 signInButton 上正确设置了文本。我认为在调用 setText 之后,使用即时调度程序会让一切都按照我的预期直接发生。

【问题讨论】:

  • 如果我在测试中的最后一个 setText 之后添加 Thread.sleep(4000) ,它就会通过。另外,如果我返回 Observable.just(true);在 usernameAvailable 中它通过了。
  • 看起来你在某处有异步执行。使用 robolectric 时,所有东西都应该是同步的,以避免在睡眠中进行测试。你如何创建异步作业?也许你的风格没有被 robolectric 覆盖。
  • 我使用 Retrofit 来执行异步请求,尽管我模拟了响应(在这种情况下相关的模拟是测试的第一行)。我确实认为你是对的 - 这显然是一个异步问题。我认为调用 flushBackground/flushForeground 会给系统时间来完成请求,然后在测试继续之前更新主线程。
  • 我没有改装经验。但是我对androidannotations有同样的问题。解决方案是隐藏在不同线程上执行异步任务的类,并使用 robolectric 执行它们。这是我的 androidannotations 示例:github.com/nenick/AndroidAppDevelopment/blob/master/…

标签: android unit-testing robolectric retrofit rx-android


【解决方案1】:

在断言之前添加以下内容。它对我有用。

    // Flush all worker tasks out of queue and force them to execute.
    Robolectric.flushBackgroundThreadScheduler();
    Robolectric.getBackgroundThreadScheduler().idleConstantly(true);

    // A latch used to lock UI Thread.
    final CountDownLatch lock = new CountDownLatch(1);

    try
    {
        lock.await(millisecond, TimeUnit.MILLISECONDS);
    }
    catch (InterruptedException e)
    {
        lock.notifyAll();
    }

    // Flush all UI tasks out of queue and force them to execute.
    Robolectric.flushForegroundThreadScheduler();
    Robolectric.getForegroundThreadScheduler().idleConstantly(true);

【讨论】:

    【解决方案2】:

    我在使用 Observables 时遇到了同样的问题。我绕过的方法是使用

    usernameAvailable("username").toBlocking().first();
    

    这将为您提供可观察的第一个结果,在测试中同步。

    【讨论】:

    • 我不能直接这样做的原因是测试并不真正了解或关心 observables,该代码是从我的 Activity 调用的。
    • @WoogieNoogie 您是否尝试过使用即时调度程序?
    • 我们确实尝试了立即调度程序和测试调度程序,但在这两种情况下,测试都没有真正进入 Observable 就完成了。
    • 看起来这将解决我们的问题:github.com/ReactiveX/RxAndroid/pull/135
    • @powerj1984,您能否详细说明您的答案?我是 RxJava 新手,我正在尝试做同样的事情,但这似乎很痛苦。
    猜你喜欢
    • 1970-01-01
    • 2014-10-22
    • 1970-01-01
    • 2015-07-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多