Rxjava2 + Retrofit + MVP 单元测试

714 阅读4分钟

参考文章:

搭建项目

以下demo来源于参考文章中。但是,与参考文章不同的是,参考文章采用的是Rxjava1,在本文,我们将采用Rxjava2,并且我会按照google官方给的MVP-TODO-MVP模式来写,所以跟参考文章可能会有所不同。

  • build.gradle
    implementation "io.reactivex.rxjava2:rxjava:2.1.11"
    implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
    implementation 'com.squareup.okhttp3:okhttp:3.10.0'
    implementation group: 'com.squareup.okhttp3', name: 'logging-interceptor', version: '3.4.0'
    implementation ('com.squareup.retrofit2:retrofit:2.4.0'){
        exclude module: 'okhttp'
    }
    implementation "com.squareup.retrofit2:converter-gson:2.4.0"
    implementation ("com.squareup.retrofit2:adapter-rxjava2:2.4.0"){
        exclude module: 'rxjava'
    }
  • Model类:User
public class User extends RealmObject {
    @PrimaryKey
    private int uid;
    private String name;


    public int getUid() {
        return uid;
    }

    public void setUid(int uid) {
        this.uid = uid;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof User) {
            User user = (User) obj;
            if (user.uid == uid){
                return true;
            }
        }
        return false;
    }
}
  • DataSource类:UserDataSource
public interface UserDataSource {
    /**
     * 加载
     * @param uid
     * @return
     */
    Flowable<User> loadUser(@Query("uid") int uid);

    void saveUser(User user);
}
  • 本地来源LocalUserDataSource
/**
 * 数据库缓存的用户信息
 */

public class LocalUserDataSource implements UserDataSource {
    @Override
    public Flowable<User> loadUser(int uid) {
       return Flowable.create(emitter -> {
           Realm realm = Realm.getDefaultInstance();
           RealmResults<User> allAsync = realm.where(User.class)
                   .equalTo("uid",uid).findAll();
           List<User> users = realm.copyFromRealm(allAsync);
           if (users != null && users.size() > 0){
               emitter.onNext(users.get(0));
               emitter.onComplete();
           }else {
               emitter.onComplete();
           }
       }, BackpressureStrategy.BUFFER);
    }

    @Override
    public void saveUser(User user) {
        Realm realm = Realm.getDefaultInstance();
        realm.beginTransaction();
        realm.copyToRealmOrUpdate(user);
        realm.commitTransaction();
        realm.close();
    }
}
  • 服务器来源RemoteUserDataSource
public class RemoteUserDataSource implements UserDataSource {

    private  UserApi mUserApi;
    /**
     * 测试用
     * @param userApi
     */
    public RemoteUserDataSource(UserApi userApi){
       this.mUserApi = userApi;
    }

    public RemoteUserDataSource(){
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://www.baidu.com/")
                .client(getOkHttpClient())
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .build();
        mUserApi = retrofit.create(UserApi.class);
    }

    private static OkHttpClient getOkHttpClient(){
        return new OkHttpClient.Builder()
                .build();
    }

    @Override
    public Flowable<User> loadUser(int uid) {
        return mUserApi.loadUser(uid).toFlowable(BackpressureStrategy.BUFFER);
    }

    @Override
    public void saveUser(User user) {
        //do nothing
    }
}
  • 数据源仓库UserRepository
public class UserRepository implements UserDataSource{
    private UserDataSource mRemote;
    private UserDataSource mLocal;

    public UserRepository(UserDataSource remote,UserDataSource local){
        this.mRemote = remote;
        this.mLocal = local;
    }


    @Override
    public Flowable<User> loadUser(int uid) {
        return Flowable.concat(mLocal.loadUser(uid),loadFromNet(uid))
                .filter(user -> user != null)
                .firstOrError()
                .toFlowable();
    }

    private Flowable<User> loadFromNet(int uid){
       return mRemote.loadUser(uid)
                .doOnNext(user -> mLocal.saveUser(user));
    }

    @Override
    public void saveUser(User user) {
        mLocal.saveUser(user);
    }
}
  • View类以及Presenter协议类:UserContract
public interface UserContract {

    interface View{
        void showUser();
    }
    interface Presenter{
        void loadUser(int uid);
    }
}
  • presenter的实现类:
public class PresenterImpl implements UserContract.Presenter {

    UserDataSource mUserRepository;
    UserContract.View mUserView;


    public PresenterImpl(UserDataSource userService , UserContract.View userView){
        this.mUserRepository = userService;
        this.mUserView = userView;
    }


    @Override
    public void loadUser(int uid) {
        mUserRepository.loadUser(uid)
                .subscribe(new Consumer<User>() {
                    @Override
                    public void accept(User user) throws Exception {
                        mUserView.onUserLoaded(user);
                    }
                });
    }
}

基本项目搭建完成了,接下来可以愉快的玩耍了。

引入测试库

在项目中引入测试中所需要用到的库:

testImplementation 'junit:junit:4.12'
    testImplementation "org.robolectric:robolectric:3.3.2"
    testImplementation 'org.mockito:mockito-core:2.16.0'
    testImplementation "org.robolectric:robolectric:3.3.2"
    testImplementation 'org.robolectric:shadows-support-v4:3.4-rc2'
    testImplementation 'com.squareup.okhttp3:mockwebserver:3.9.1'
    androidTestImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.0'
    androidTestImplementation 'com.android.support.test:rules:1.0.0'

如果要使用context,记得 还要引入:

testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'

位置如图:

image.png 不然会报错:

Android IllegalStateException No instrumentation registered! Must run under a registering instrumentation

先看一下各个类在项目中的位置:

image.png

presenter层的测试

public class PresenterImplTest extends TestCase {
    @Mock
    UserContract.View mUserView;
    @Mock
    UserDataSource mUserRepository;

    UserContract.Presenter mUserPresenter;

    @Before
    public void setUp() throws Exception{
        MockitoAnnotations.initMocks(this);
//        mUserView = mock(UserContract.View.class);
//        mUserRepository = mock(UserDataSource.class);
        mUserPresenter = new PresenterImpl(mUserRepository,mUserView);
    }


    @Test
    public void testLoadUser() throws Exception{
        User user = new User();
        user.setUid(1);
        user.setName("kkmike999");
        //如果调用了mUserService的loadUser方法,那么返回Observable.just(user)
        when(mUserRepository.loadUser(anyInt()))
                .thenReturn(Flowable.just(user));
        //调用loadUser方法
        mUserPresenter.loadUser(1);

        ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);

        verify(mUserRepository).loadUser(1);
        verify(mUserView).onUserLoaded(captor.capture());

        User result = captor.getValue();
        Assert.assertEquals(result.getUid(),1);
    // 返回参数是否符合预期    Assert.assertEquals(result.getName(),"kkmike999");
    }
}

ArgumentCaptor介绍

  • 使用场景:在某些场景中,不光要对方法的返回值和调用进行验证,同时需要验证一系列逻辑之后所传入方法的参数是否符合期望。此时,我们就可以使用ArgumentCaptor。
  • 使用:通过ArgumentCaptor对象的forClass(Class clazz)方法来构建ArgumentCaptor对象。然后便可在验证时对方法的参数进行捕获,最后验证捕获的参数值。如果方法有多个参数都要捕获验证,那就需要创建多个ArgumentCaptor对象处理。
  • ArgumentCaptor的Api :
    • argument.capture() 捕获方法参数
    • argument.getValue() 获取方法参数值,如果方法进行了多次调用,它将返回最后一个参数值
    • argument.getAllValues() 方法进行多次调用后,返回多个参数值

参考博客:学习Mocktio

UserRepository的测试

public class UserRepositoryTest extends TestCase {


    @Mock
    UserDataSource mRemote;

    @Mock
    UserDataSource mLocal;

    UserRepository mRepository;

    @Before
    public void setUp(){
        MockitoAnnotations.initMocks(this);
        mRepository = new UserRepository(mRemote,mLocal);
    }


    @Test
    public void testLoadFromLocalSuccess(){
        int uid = 1;
        User user = new User();
        user.setUid(uid);
        user.setName("kkmike999");
        when(mLocal.loadUser(uid)).thenReturn(Flowable.just(user));
        when(mRemote.loadUser(uid)).thenReturn(Flowable.error(new Throwable("")));
        TestSubscriber<User> testObserver = new TestSubscriber<>();
        mRepository.loadUser(uid).subscribe(testObserver);
        //判断返回的User跟 user是一样的
        testObserver.assertValue(user);
    }

}

LocalDataSource的测试

@RunWith(AndroidJUnit4.class)
@LargeTest
public class LocalUserDataSourceTest extends TestCase {

    Context mContext;

    LocalUserDataSource localUserDataSource ;

    @Before
    public void setUp(){
        mContext = InstrumentationRegistry.getTargetContext();
        Realm.init(mContext);
        localUserDataSource = new LocalUserDataSource();
    }

    @Test
    public void testSave_retrievedUser(){
        User user = new User();
        user.setUid(1);
        user.setName("kkmike999");
        localUserDataSource.saveUser(user);

        //get
        TestSubscriber<User> subscriber = new TestSubscriber<>();
        localUserDataSource.loadUser(1).subscribe(subscriber);
        subscriber.assertValue(user);
    }
}

RemoteUserDataSource测试

/**
 * 作用:把异步变成同步
 */
public class RxjavaTestRule implements TestRule {
    @Override
    public Statement apply(Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                RxJavaPlugins.reset();
                RxJavaPlugins.setIoSchedulerHandler(new Function<Scheduler, Scheduler>() {
                    @Override
                    public Scheduler apply(Scheduler scheduler) throws Exception {
                        return Schedulers.trampoline();
                    }
                });
                RxAndroidPlugins.reset();
                RxAndroidPlugins.setMainThreadSchedulerHandler(new Function<Scheduler, Scheduler>() {
                    @Override
                    public Scheduler apply(Scheduler scheduler) throws Exception {
                        return Schedulers.trampoline();
                    }
                });

                base.evaluate();
            }
        };
    }
}

RxjavaTestRule是在test文件夹下的。

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 23)
public class RemoteUserDataSourceTest extends TestCase {

    private MockWebServer server;

    @Rule
    public RxjavaTestRule rule = new RxjavaTestRule();
    private RemoteUserDataSource mRemoteUserDataSource;

    @Before
    public void setUp() {
        ShadowLog.stream = System.out;

        // 创建一个 MockWebServer
        server = new MockWebServer();

        //设置响应,默认返回http code是 200
        MockResponse mockResponse = new MockResponse()
                .addHeader("Content-Type", "application/json;charset=utf-8")
                .addHeader("Cache-Control", "no-cache")
                .setBody("{\\n\" +\n" +
                        "            \"    \\\"name\\\": \\\"kkmex\\\",\\n\" +\n" +
                        "            \"    \\\"uid\\\": 12\\n\" +\n" +
                        "            \"}\"");

//        MockResponse mockResponse1 = new MockResponse()
//                .addHeader("Content-Type", "application/json;charset=utf-8")
//                .setResponseCode(404)
//                .throttleBody(5, 1, TimeUnit.SECONDS) //一秒传递5个字节,模拟网速慢的情况
//                .setBody("{\"error\": \"网络异常\"}");

        server.enqueue(mockResponse); //成功响应
//        server.enqueue(mockResponse1);//失败响应

        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .addInterceptor(new HttpLoggingInterceptor())
                .build();

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("http://" + server.getHostName() + ":" + server.getPort() + "/") //设置对应的Host与端口号
                .client(okHttpClient)
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                .build();

        UserApi userApi = retrofit.create(UserApi.class);
        mRemoteUserDataSource = new RemoteUserDataSource(userApi);

    }

    @Test
    public void getUserTest() throws Exception {
        //请求不变
        mRemoteUserDataSource.loadUser(1)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Subscriber<User>() {

                    @Override
                    public void onSubscribe(Subscription s) {

                    }

                    @Override
                    public void onNext(User user) {
                        assertEquals("kkmex", user.getName());
                        assertEquals(12, user.getUid());
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.e("Test", e.toString());
                    }

                    @Override
                    public void onComplete() {
                    }
                });

        //验证我们的请求客户端是否按预期生成了请求
        RecordedRequest request = server.takeRequest();
        assertEquals("GET /getUser?uid=1 HTTP/1.1", request.getRequestLine());
        assertEquals("okhttp/3.10.0", request.getHeader("User-Agent"));

        // 关闭服务
        server.shutdown();
    }
}

参考文章: Android单元测试(五):网络接口测试 好了,接下里就剩下View部分的单元测试了。这部分只能先占坑了~~~

提醒:上面使用Flowable是不太合适的,一般情况下,对于接收和处理速度差不多的场景,用Observable就可以了。对于从网络获取数据这种,一般一次就返回了所有数据,使用Maybe就可以了,没有必要使用Flowable

其他

在使用Rxjava2进行测试时,有下面一些技巧

  • 当返回Flowable,采用TestSubscriber;当返回Observable时,采用TestObserver
  • TestObserver如何取到OnNext中的值?答: values()方法
TestObserver<List<User>> testObserver = new TestObserver<>();
testObserver.values();