美文网首页文艺的安卓君移动开发狂热者(299402133)IT圈
Android单元测试框架Robolectric3.0介绍(二)

Android单元测试框架Robolectric3.0介绍(二)

作者: geniusmart | 来源:发表于2016-03-22 23:14 被阅读8605次
    此生无缘,愿你在另一个时空永远幸运

    文章中的所有代码在此:https://github.com/geniusmart/LoveUT ,由于 Robolectric 3.0 和 3.1 版本(包括后续3.x版本)差异不小,该工程中包含这两个版本对应的测试用例 Demo 。

    一 闲话单元测试

    我们经常讲“前人种树,后人乘凉”,然而在软件开发中,往往呈现出来的却是截然相反的景象,我们在绩效和指标的驱使下,主动或被动的留下来大量坏味道的代码,在短时间内顺利的完成项目,此后却花了数倍于开发的时间来维护此项目,可谓“前人砍树,后人遭殃”,讽刺的是,砍树的人往往因为优秀的绩效,此时已经步步高升,而遭殃的往往是意气风发,步入职场的年轻人,如此不断轮回。所以,为了打破轮回,从一点一滴做起吧,“树”的种类众多,作为任意一名普通的软件工程师,种好单元测试这棵树,便是撒下一片荫凉。

    关于单元测试,很多人心中会有以下几个疑问:
    (1)为什么要写?
    (2)这不是QA人员该做的吗?
    (3)需求天天变,功能都来不及完成了,还要同时维护代码和UT,四不四傻啊?
    (4)我要怎么写UT(特别是Android单元测试)?

    1. 关于第一个问题,首先我们反问自己几个问题:
    • (1)我们在学习任何一个技术框架,比如 retofit2Dagger2 时,是不是第一时间先打开官方文档(或者任意文档),然后查阅api如何调用的代码,而官方文档往往都会在最醒目的地方,用最简洁的代码向我们说明了api如何使用?

      其实,当我们在写单元测试时,为了测试某个功能或某个api,首先得调用相关的代码,因此我们留下来的便是一段如何调用的代码。这些代码的价值在于为以后接手维护/重构/优化功能的人,留下一份程序猿最愿意去阅读的文档。

    • (2)当你写单元测试的时候,是不是发现很多代码无法测试?撇开对UT测试框架不熟悉的因素之外,是不是因为你的代码里一个方法做了太多事情,或者代码的封装性不够好,或者一个方法需要有其他很多依赖才能测试(高耦合),而此时,为了让你的代码可测试,你是不是会主动去优化一下代码

    • (3)是不是对重构没信心?这个话题太老生常谈了,配备有价值的、高覆盖率的单元测试可解决此问题。

    • (4)当你在写Android代码(比如网络请求和DB操作)的时候,是如何测试的?跑起来整个App,点了好几步操作后,终于到达要测试的功能,然后巨慢无比的Debug?如果你写UT,并使用Robolectric这样的框架,你不仅可以脱离Android环境对代码进行调试,还可以很快速的定位和Debug你想要调试的代码,大大的提升了开发效率。

    以上,便是写好单元测试的意义。

    1. 关于第二个问题,己所不欲勿施于人
      我始终觉得让QA写UT,是一种傻叉的行为。单元测试是一种白盒测试,本来就是开发分内之事,难道让QA去阅读你恶心的充满坏味道的代码,然后硬着头皮写出UT?试想一下,你的产品经理让你画原型写需求文档,你的领导让你去市场部辅助吹嘘产品,促进销售,你会不会有种吃了翔味巧克力的感觉?所以,己所不欲勿施于人。

    2. 这个问题有点头疼,总之,尽量提高我们的代码设计和写UT的速度,以便应对各种不合理的需求和项目。

    3. 前面三个问题,或多或少是心态的问题,调整好心态,认可UT的优点,尝试走第一步看看。而第四个问题,如何写?则是笔者这系列文章的核心内容,在我的第一篇《Robolectric3.0(一)》中已经介绍了这个框架的特点,环境搭建,三大组件(Activity、Bordercast、Service)的测试,以及Shadow的使用,这篇文章,主要介绍网络请求和数据库相关的功能如何测试。

    二 日志输出

    Robolectric对日志输出的支持其实非常简单,为什么把它单独列一个条目来讲解?因为往往我们在写UT的过程,其实也是在调试代码,而日志输出对于代码调试起到极大的作用。我们只需要在每个TestCase的setUp()里执行ShadowLog.stream = System.out即可,如:

    @Before
    public void setUp() throws URISyntaxException {
        //输出日志
        ShadowLog.stream = System.out;
    }
    

    此时,无论是功能代码还是测试代码中的 Log.i()之类的相关日志都将输出在控制面板中,调试起功能来,简直爽得不要不要的。

    三 网络请求篇

    关于网络请求,笔者采用的是retrofit2的2.0.0-beta4版本,api调用有很大的变化,详情请参考官方文档。Robolectic支持发送真实的网络请求,通过对响应结果进行测试,可大大的提升我们与服务端的联调效率。

    以github api为例,网络请求的代码如下:

    public interface GithubService {
    
        String BASE_URL = "https://api.github.com/";
    
        @GET("users/{username}/repos")
        Call<List<Repository>> publicRepositories(@Path("username") String username);
    
        @GET("users/{username}/following")
        Call<List<User>> followingUser(@Path("username") String username);
    
        @GET("users/{username}")
        Call<User> user(@Path("username") String username);
    
    
        class Factory {
            public static GithubService create() {
                Retrofit retrofit = new Retrofit.Builder()
                        .baseUrl(BASE_URL)
                        .addConverterFactory(GsonConverterFactory.create())
                        .build();
                return retrofit.create(GithubService.class);
            }
        }
    }
    

    1. 测试真实的网络请求

    @Test
    public void publicRepositories() throws IOException {
        Call<List<Repository>> call = githubService.publicRepositories("geniusmart");
        Response<List<Repository>> execute = call.execute();
    
        List<Repository> list = execute.body();
        //可输出完整的响应结果,帮助我们调试代码
        Log.i(TAG,new Gson().toJson(list));
        assertTrue(list.size()>0);
        assertNotNull(list.get(0).name);
    }
    

    这类测试的意义在于:

    • (1)检验网络接口的稳定性
    • (2)检验部分响应结果数据的完整性(如非空验证)
    • (3)方便开发阶段的联调(通过UT联调的效率远高于run app后联调)

    2. 模拟网络请求

    对于网络请求的测试,我们需要知道确切的响应结果值,才可进行一系列相关的业务功能的断言(比如请求成功/失败后的异步回调函数里的逻辑),而发送真实的网络请求时,其返回结果往往是不可控的,因此对网络请求和响应结果进行模拟显得特别必要。

    那么如何模拟?其原理很简单,okhttp提供了拦截器 Interceptors ,通过该api,我们可以拦截网络请求,根据请求路径,不进行请求的发送,而直接返回我们自定义好的相应的response json字符串。

    首先,自定义Interceptors的代码如下:

    public class MockInterceptor implements Interceptor {
    
        @Override
        public Response intercept(Interceptor.Chain chain) throws IOException {
    
            String responseString = createResponseBody(chain);
    
            Response response = new Response.Builder()
                    .code(200)
                    .message(responseString)
                    .request(chain.request())
                    .protocol(Protocol.HTTP_1_0)
                    .body(ResponseBody.create(MediaType.parse("application/json"), responseString.getBytes()))
                    .addHeader("content-type", "application/json")
                    .build();
            return response;
        }
    
        /**
         * 读文件获取json字符串,生成ResponseBody
         *
         * @param chain
         * @return
         */
        private String createResponseBody(Chain chain) {
    
            String responseString = null;
    
            HttpUrl uri = chain.request().url();
            String path = uri.url().getPath();
    
            if (path.matches("^(/users/)+[^/]*+(/repos)$")) {//匹配/users/{username}/repos
                responseString = getResponseString("users_repos.json");
            } else if (path.matches("^(/users/)+[^/]+(/following)$")) {//匹配/users/{username}/following
                responseString = getResponseString("users_following.json");
            } else if (path.matches("^(/users/)+[^/]*+$")) {//匹配/users/{username}
                responseString = getResponseString("users.json");
            }
            return responseString;
        }
    }
    

    相应的resonse json的文件可以存放在test/resources/json/下,如下图


    response的json数据文件

    再次,定义Http Client,并添加拦截器:

    //获取测试json文件地址
    jsonFullPath = getClass().getResource(JSON_ROOT_PATH).toURI().getPath();
    //定义Http Client,并添加拦截器
    OkHttpClient okHttpClient = new OkHttpClient.Builder()
            .addInterceptor(new MockInterceptor(jsonFullPath))
            .build();
    //设置Http Client
    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(GithubService.BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .client(okHttpClient)
            .build();
    mockGithubService = retrofit.create(GithubService.class);
    

    最后,就可以使用mockGithubService进行随心所欲的断言了:

    @Test
    public void mockPublicRepositories() throws Exception {
        Response<List<Repository>> repositoryResponse = mockGithubService.publicRepositories("geniusmart").execute();
        assertEquals(repositoryResponse.body().get(5).name, "LoveUT");
    }
    

    这种做法不仅仅可以在写UT的过程中使用,在开发过程中也可以使用,当服务端的接口开发滞后于客户端的进度时,可以先约定好数据格式,客户端采用模拟网络请求的方式进行开发,此时两个端可以做到不互相依赖。

    3. 网络请求的异步回调如何进行测试

    关于网络请求之后的回调函数如何测试,笔者暂时也没有什么自己觉得满意的解决方案,这里提供一种做法,权当抛砖引玉,希望有此经验的人提供更多的思路。

    由于网络请求和回调函数是在子线程和UI主线程两个线程中进行的,且后者要等待前者执行完毕,这种情况要在一个TestCase中测试并不容易。因此我们要做的就是想办法让两件事情同步的在一个TestCase中执行,类似于这样的代码:

    //此为Retrofit2的新api,代表同步执行
    //异步执行的api为githubService.followingUser("geniusmart").enqueue(callback);
    githubService.publicRepositories("geniusmart").execute();
    callback.onResponse(call,response);
    //对执行回调后影响的数据做断言
    some assert...
    

    这里我列举一个场景,并进行相应的单元测试:一个Activity中有个ListView,经过网络请求后,在异步回调函数里加载ListView的数据,点击每一个item后,吐司其对应的标题。

    public class CallbackActivity extends Activity {
    
        //省略一些全局变量声明的代码
        /**
         * 定义一个全局的callback对象,并暴露出get方法供UT调用
         */
        private Callback<List<User>> callback;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            //省略一些初始化UI组件的代码
            listView.setOnItemClickListener(new AdapterView.OnItemClickListener(){
                @Override
                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                    Toast.makeText(CallbackActivity.this,datas.get(position),Toast.LENGTH_SHORT).show();
                }
            });
            //加载数据
            loadData();
        }
    
        public void loadData() {
            progressBar.setVisibility(View.VISIBLE);
            datas = new ArrayList<>();
            //初始化回调函数对象
            callback = new Callback<List<User>>() {
                @Override
                public void onResponse(Call<List<User>> call, Response<List<User>> response) {
                    for(User user : response.body()){
                        datas.add(user.login);
                    }
    
                    ArrayAdapter<String> adapter = new ArrayAdapter<>(CallbackActivity.this,
                            android.R.layout.simple_list_item_1, datas);
                    listView.setAdapter(adapter);
                    progressBar.setVisibility(View.GONE);
                }
    
                @Override
                public void onFailure(Call<List<User>> call, Throwable t) {
                    progressBar.setVisibility(View.GONE);
                }
            };
            GithubService githubService = GithubService.Factory.create();
            githubService.followingUser("geniusmart").enqueue(callback);
        }
    
        public Callback<List<User>> getCallback(){
            return callback;
        }
    }
    

    相应的测试代码如下:

    @Test
    public void callback() throws IOException {
        CallbackActivity callbackActivity = Robolectric.setupActivity(CallbackActivity.class);
        ListView listView = (ListView) callbackActivity.findViewById(R.id.listView);
        Response<List<User>> users = mockGithubService.followingUser("geniusmart").execute();
        //结合模拟的响应数据,执行回调函数
        callbackActivity.getCallback().onResponse(null, users);
        ListAdapter listAdapter = listView.getAdapter();
        //对ListView的item进行断言
        assertEquals(listAdapter.getItem(0).toString(), "JakeWharton");
        assertEquals(listAdapter.getItem(1).toString(), "Trinea");
    
        ShadowListView shadowListView = Shadows.shadowOf(listView);
    
        //测试点击ListView的第3~5个Item后,吐司的文本
        shadowListView.performItemClick(2);
        assertEquals(ShadowToast.getTextOfLatestToast(), "daimajia");
        shadowListView.performItemClick(3);
        assertEquals(ShadowToast.getTextOfLatestToast(), "liaohuqiu");
        shadowListView.performItemClick(4);
        assertEquals(ShadowToast.getTextOfLatestToast(), "stormzhang");
    }
    

    这样做的话要改变一些编码习惯,比如回调函数不能写成匿名内部类对象,需要定义一个全局变量,并破坏其封装性,即提供一个get方法,供UT调用。

    注:经过后续研究,使用Mockito的Capture才是解决异步测试的最佳方案,后面考虑出专门文章来说明。

    四 数据库篇

    Robolectric从2.2开始,就已经可以对真正的DB进行测试,从3.0开始测试DB变得更加便利,通过UT来调试DB简直不能更爽。这一节将介绍不使用任何框架的DB测试,ORMLite测试以及ContentProvider测试。

    1. 不使用任何框架的DB测试(SQLiteOpenHelper)

    如果没有使用框架,采用Android的SQLiteOpenHelper对数据库进行操作,通常我们会封装好各个Dao,并实例化一个SQLiteOpenHelper的单例对象,测试代码如下:

    @Test
    public void query(){
        AccountDao.save(AccountUtil.createAccount("3"));
        AccountDao.save(AccountUtil.createAccount("4"));
        AccountDao.save(AccountUtil.createAccount("5"));
        AccountDao.save(AccountUtil.createAccount("5"));
    
        List<Account> accountList = AccountDao.query();
        assertEquals(accountList.size(), 3);
    }
    

    另外有一点要注意的是,当我们测试多个test时,会抛出一个类似于这样的异常:
    java.lang.RuntimeException: java.lang.IllegalStateException: Illegal connection pointer 37. Current pointers for thread Thread[pool-1-thread-1,5,main] []
    解决方式便是每次执行一个test之后,就将SQLiteOpenHelper实例对象重置为null,如下:

    @After
    public void tearDown(){
        AccountUtil.resetSingleton(AccountDBHelper.class, "mAccountDBHelper");
    }
    
    public static void resetSingleton(Class clazz, String fieldName) {
        Field instance;
        try {
            instance = clazz.getDeclaredField(fieldName);
            instance.setAccessible(true);
            instance.set(null, null);
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }
    

    2. OrmLite测试

    使用OrmLite对数据操作的测试与上述方法并无区别,同样也要注意每次测试完后,要重置OrmLiteSqliteOpenHelper实例。

    @After
    public void tearDown(){
        DatabaseHelper.releaseHelper();
    }
    
    @Test
    public void save() throws SQLException {
    
        long millis = System.currentTimeMillis();
        dao.create(new SimpleData(millis));
        dao.create(new SimpleData(millis + 1));
        dao.create(new SimpleData(millis + 2));
    
        assertEquals(dao.countOf(), 3);
    
        List<SimpleData> simpleDatas = dao.queryForAll();
        assertEquals(simpleDatas.get(0).millis, millis);
        assertEquals(simpleDatas.get(1).string, ((millis + 1) % 1000) + "ms");
        assertEquals(simpleDatas.get(2).millis, millis + 2);
    }
    

    3. ContentProvider测试

    一旦你的App里有ContentProvider,此时配备完善和严谨的单元测试用例是非常有必要的,毕竟你的ContentProvider是对外提供使用的,一定要保证代码的质量和稳定性。

    对ContentProvider的测试,需要借助影子对象ShadowContentResolver,关于Shadow,我在上文中已经有介绍过,此处的Shadow可以丰富ContentResolver的行为,帮助我们进行测试,代码如下:

    @RunWith(RobolectricGradleTestRunner.class)
    @Config(constants = BuildConfig.class)
    public class AccountProviderTest {
    
        private ContentResolver mContentResolver;
        private ShadowContentResolver mShadowContentResolver;
        private AccountProvider mProvider;
        private String AUTHORITY = "com.geniusmart.loveut.AccountProvider";
        private Uri URI_PERSONAL_INFO = Uri.parse("content://" + AUTHORITY + "/" + AccountTable.TABLE_NAME);
    
        @Before
        public void setUp() {
            ShadowLog.stream = System.out;
    
            mProvider = new AccountProvider();
            mContentResolver = RuntimeEnvironment.application.getContentResolver();
            //创建ContentResolver的Shadow对象
            mShadowContentResolver = Shadows.shadowOf(mContentResolver);
    
            mProvider.onCreate();
            //注册ContentProvider对象和对应的AUTHORITY
            ShadowContentResolver.registerProvider(AUTHORITY, mProvider);
        }
    
        @After
        public void tearDown() {
            AccountUtil.resetSingleton(AccountDBHelper.class, "mAccountDBHelper");
        }
    
    
        @Test
        public void query() {
            ContentValues contentValues1 = AccountUtil.getContentValues("1");
            ContentValues contentValues2 = AccountUtil.getContentValues("2");
    
            mShadowContentResolver.insert(URI_PERSONAL_INFO, contentValues1);
            mShadowContentResolver.insert(URI_PERSONAL_INFO, contentValues2);
    
            //查询所有数据
            Cursor cursor1 = mShadowContentResolver.query(URI_PERSONAL_INFO, null, null, null, null);
            assertEquals(cursor1.getCount(), 2);
    
            //查询id为2的数据
            Uri uri = ContentUris.withAppendedId(URI_PERSONAL_INFO, 2);
            Cursor cursor2 = mShadowContentResolver.query(uri, null, null, null, null);
            assertEquals(cursor2.getCount(), 1);
        }
    
        @Test
        public void queryNoMatch() {
            Uri noMathchUri = Uri.parse("content://com.geniusmart.loveut.AccountProvider/tabel/");
            Cursor cursor = mShadowContentResolver.query(noMathchUri, null, null, null, null);
            assertNull(cursor);
        }
    
        @Test
        public void insert() {
            ContentValues contentValues1 = AccountUtil.getContentValues("1");
            mShadowContentResolver.insert(URI_PERSONAL_INFO, contentValues1);
            Cursor cursor = mShadowContentResolver.query(URI_PERSONAL_INFO, null, AccountTable.ACCOUNT_ID + "=?", new String[]{"1"}, null);
            assertEquals(cursor.getCount(), 1);
            cursor.close();
        }
    
        @Test
        public void update() {
            ContentValues contentValues = AccountUtil.getContentValues("2");
            Uri uri = mShadowContentResolver.insert(URI_PERSONAL_INFO, contentValues);
    
            contentValues.put(AccountTable.ACCOUNT_NAME, "geniusmart_update");
            int update = mShadowContentResolver.update(uri, contentValues, null, null);
            assertEquals(update, 1);
    
            Cursor cursor = mShadowContentResolver.query(URI_PERSONAL_INFO, null, AccountTable.ACCOUNT_ID + "=?", new String[]{"2"}, null);
            cursor.moveToFirst();
            String accountName = cursor.getString(cursor.getColumnIndex(AccountTable.ACCOUNT_NAME));
            assertEquals(accountName, "geniusmart_update");
            cursor.close();
        }
    
        @Test
        public void delete() {
            try {
                mShadowContentResolver.delete(URI_PERSONAL_INFO, null, null);
                fail("Exception not thrown");
            } catch (Exception e) {
                assertEquals(e.getMessage(), "Delete not supported");
            }
        }
    
    }
    

    五 Love UT

    写UT是一种非常好的编程习惯,但是UT虽好,切忌贪杯,作为一名技术领导者,切忌拿测试覆盖率作为指标,如此一来会滋生开发者的抵触心理,导致乱写一通。作为开发者,应该时刻思考什么才是有价值的UT,什么逻辑没必要写(比如set和get),这样才不会疲于奔命且觉得乏味。其实很多事情都是因果关系,开发人员不写,所以leader强制写,而leader强制写,开发人员会抵触而乱写。所以,让各自做好,一起来享受UT带来的高质量的代码以及为了可测试而去思考代码设计的编程乐趣。

    本文的所有代码仍然放在LoveUT这个工程里:
    https://github.com/geniusmart/LoveUT

    参考文章

    http://square.github.io/retrofit/
    https://github.com/square/okhttp/wiki/Interceptors
    http://stackoverflow.com/questions/17544751/square-retrofit-server-mock-for-testing
    https://github.com/robolectric/robolectric/issues/1890

    最后,行此文时,悲痛欲绝,越长大越不会表达自己的情感,此文送给肚中远去的小小猴子,此生无缘。无论你在哪个时空,作为一个技术从业者,将保持纯良,求真,但行好事,希望能带给你幸运。愿此坎之后,此生无坎。

    相关文章

      网友评论

      • Victor123:请问下 getClass().getResource(JSON_ROOT_PATH) 中 JSON_ROOT_PATH怎么写? 绝对路径吗?
      • Victor123:请问设置完 @Before
        public void setup() throws URISyntaxException {
        //输出日志
        ShadowLog.stream = System.out;
        Log.d(TAG, "setup: ");
        }
        后在哪可以看日志呢? Logcat 还是gradle console? 我都没找到我打印的日志
        快乐石头111:@Victor123 在那个Run里就可以看到了
        Victor123:@geniusmart 单元测试自己的log面板是哪个? 我用的Android studio :joy:
        geniusmart:@Victor123 在单元测试自己的log面板
      • Victor123:楼主你好 你说的 【经过后续研究,使用Mockito的Capture才是解决异步测试的最佳方案,后面考虑出专门文章来说明。】是什么意思? 是使用Mockito来mock数据吗? 还是别的解决方案,请指教,谢谢
        geniusmart:没时间写,参考下我另外一篇讲mvp测试的,那篇更有大局观
      • 李云龙_:楼主你好,为什么网络请求 enque 不回调?只能用 call.execute();
        李云龙_:@geniusmart 搜嘎,谢谢楼主
        geniusmart:异步无法测,把异步变同步是测多线程场景的思路之一
      • 李云龙_:最后一段没看懂,此文送给肚中远去的小小猴子,此生无缘..,什么意思?
      • xp1979:你好,我用了自建Maven库,楼主的工程,跑测试用例提示连接oss超时。请问怎么解决? @geniusmart
      • Andrew玩Android:使用真实网络请求测试 publicRepositories 会报错,很明显是是因为SSL安全校验异常,楼主怎么解决?

        `javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path validation failed: java.security.cert.CertPathValidatorException: Algorithm constraints check failed on signature algorithm: SHA256WithRSAEncryption`


      • 王月半子:最近也在使用robolectric 遇到了些问题 在使用Robolectric框架中,由于被测代码中自定义的BaseApplication中还有一些三方类库的初始化,所以测试代码中额外自定义了一个application同时在config中配置,但是在测试过程中又不得不使用到被测代码中的BaseApplication的getString/getResource等方法,请问这种情况怎么处理呢?
      • richy_:看最后一段忽然感觉好心痛,愿未来一切安好
        geniusmart:@richy_ 谢谢,现在小鸡仔已经出来了:smile::smile:,一切安好
      • _夏兮:你好请问 service 要如何测试?
        geniusmart:@_夏兮 例子里面有
      • 离人歌:大神用这个
        @Before
        public void setUp() throws URISyntaxException {
        //输出日志
        ShadowLog.stream = System.out;
        }
        有报错过么,我这边报错:
        E/Typeface: Error opening /system/etc/fonts.xml
        java.io.FileNotFoundException: /system/etc/fonts.xml (No such file or directory)
        好奇怪
      • 谢三弟:可是这个不支持 22 以上啊
      • 5ea4beb37efa:同学你好 我是小米工程师 最近也在学习 单元测试 可以加微信好友不 shaoxy1992 我的微信 注明简书单元测试。
      • 少年阿洁:楼主,是否可以留个联系方式,交流下,或是加我的,我的Q:474280917
      • d5962134cef8:大哥你好. 在运行真实的网络测试和mock的网络测试时, 都会断言失败,报错为
        java.lang.AssertionError
        at okhttp3.internal.AndroidPlatform.isCleartextTrafficPermitted(AndroidPlatform.java:143)
        at okhttp3.OkHttpClient.<clinit>(OkHttpClient.java:73)
        at retrofit2.Retrofit$Builder.build(Retrofit.java:551)
        at com.geniusmart.loveut.net.GithubService$Factory.create(GithubService.java:31)
        at com.geniusmart.loveut.net.GithubServiceTest.setUp(GithubServiceTest.java:36)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        .......
        不知道你有没有遇到这样的情况呢? 我是直接编译的LoveUT项目
        快乐石头111:@dazeSimpleBook @Config(sdk = 21)换换这个sdk版本试试
        李云龙_:我也遇到了,不过是应用到项目中遇到的,
      • 红颜疯子:我使用的是3.1.2的版本,里面RobolectricGradleTestRunner已经是过时的类,而且我运行测试方法的时候,一直去下载一些东西,然后就卡死,进行不下去了,请教下?
        geniusmart:@红颜疯子 已经回复啦
        红颜疯子: @geniusmar 我在你第一篇文章里下的demo,也有问题,给你留言了,能帮忙看下吗?
        geniusmart:@红颜疯子 3.1的貌似bug很多,如果初次接触,建议先把3.0玩透了
      • JokAr_:“用Mockito的Capture才是解决异步测试”的文章在哪里?
      • 53d03bdc21b4:好文,感谢楼主分享。。。。。
      • 舞影凌风: :smiley: 很棒,支持一下
      • lovexiaov:赞一个,讲的非常好

      本文标题:Android单元测试框架Robolectric3.0介绍(二)

      本文链接:https://www.haomeiwen.com/subject/njltlttx.html