【问题标题】:Mocking up WifiManager for Android Unit Testing为 Android 单元测试模拟 WifiManager
【发布时间】:2011-07-30 10:45:08
【问题描述】:

我正在尝试为依赖 WifiManager 和返回的 ScanResults 的几个类实现一些单元测试。我想做的是能够控制我收到的扫描结果,以便测试各种不同的条件。

不幸的是,我很难成功地模拟 WifiManager(尽管我想我可以在我的 MockWifiManager 中传递它的构造函数空引用)。这只是我的第一个问题,因为一旦我有一个 MockWifiManager 可以玩(如果这甚至可以工作!)我将不得不成功创建我的测试 ScanResults 没有公共构造函数(想象它是由某个工厂在某个地方创建的)。

问题: 如果它没有公共构造函数,我什至可以扩展它吗?

我做错了吗?我经常被问到有关如何完成特定任务的问题,但实际上他们试图以错误的方式解决不同的问题,也许这就是我在这里所做的?

我对 android 很陌生,所以不得不模拟所有这些功能至少可以说是。

感谢您的意见!

编辑: 我在实例化一个 MockWifiManager 时也很开心。 wifi 管理器的构造函数期望 IWifiManager 类型在 Android SDK 中似乎不存在。

【问题讨论】:

    标签: java android unit-testing junit mocking


    【解决方案1】:

    围绕 WifiManager 创建一个抽象。用它来嘲讽。嘲笑你不拥有的东西是艰难而脆弱的。如果操作正确,您应该能够切换内部结构,而且您最终会得到一个更好的可模拟 API。

    对于您的测试,您可以存根/伪造经理,让您心满意足。对于生产,您将传递一个具体实例。

    关于您关于更改代码只是为了使其可测试的观点是不正确的。首先,您应该模拟角色而不是下面论文中讨论的类型。谷歌了解更多信息。

    第二次围绕第三方代码创建抽象是一种最佳实践,正如 SOLID 中的依赖倒置原则所述。无论您是否进行单元测试,您都应该始终依赖抽象而不是具体实现。

    http://www.objectmentor.com/resources/articles/dip.pdf http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod

    【讨论】:

    • 围绕测试需求进行设计似乎很奇怪。包装我可能需要使用的每个系统 API 还有其他好处吗?
    • @Brian 是的。首先,您可以在一个地方交换 WifiManager。当版本 x 出现并且 API 有所不同时会发生什么?您必须更改代码库中的许多区域。我正在用更多信息更新我的答案。
    • 啊,是的,隔离可能发生变化的事情。除非我用这种抽象保护自己,否则我无法控制 Android API。好电话!
    【解决方案2】:

    您可以尝试通过使用反射来访问私有构造函数来创建 ScanResult 实例。代码可能如下所示:

            try {
                Constructor<ScanResult> ctor = ScanResult.class.getDeclaredConstructor(null);
                ctor.setAccessible(true);
                ScanResult sr = ctor.newInstance(null);
                sr.BSSID = "foo";
                sr.SSID = "bar";
                // etc... 
            } catch (SecurityException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IllegalArgumentException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (InstantiationException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
    

    对于其他测试方式,我大多数时候将来自 ScanResult 之类的实例的信息转换并仅将我需要的信息封装到我自己的对象中。这些我喂给做艰苦工作的方法。这使得测试更容易,因为您可以轻松构建这些中间对象,而无需依赖真正的 ScanResult 对象。

    【讨论】:

    • 每次我必须使用反射时,上帝都会杀死一只小狗。不幸的是,这可能是唯一的方法,感谢代码 sn-p
    【解决方案3】:

    我一直在努力构建ScanResult 对象。我已经成功地使用了上面的反射方法。

    如果有人正在寻找克隆ScanResult 对象(或任何其他实现Parcelable 接口的对象)的方法,您可以使用这种方法(我在单元测试中检查了它):

    @RunWith(RobolectricTestRunner.class)
    @Config(manifest=Config.NONE)
    public class MovingAverageQueueTests {
        @Test
        public void parcelTest() {
            Parcel parcel = Parcel.obtain();
    
            ScanResult sr = buildScanResult("01:02:03:04:05:06", 70);
    
            parcel.writeValue(sr);
            parcel.setDataPosition(0); // required after unmarshalling
            ScanResult clone = (ScanResult)parcel.readValue(ScanResult.class.getClassLoader());
            parcel.recycle();
    
            assertThat(clone.BSSID, is(equalTo(sr.BSSID)));
            assertThat(clone.level, is(equalTo(sr.level)));
            assertThat(clone, is(not(sameInstance(sr))));
        }
    
        private ScanResult buildScanResult(String mac, int level) {
            Constructor<ScanResult> ctor = null;
            ScanResult sr = null;
    
            try {
                ctor = ScanResult.class.getDeclaredConstructor(null);
                ctor.setAccessible(true);
                sr = ctor.newInstance(null);
    
                sr.BSSID = mac;
                sr.level = level;
    
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
    
            return sr;
        }
    }
    

    至于性能,这个幼稚的检查:

    @Test
    public void buildVsClonePerformanceTest() {
        ScanResult sr = null;
    
        long start = System.nanoTime();
        for (int i = 0; i < 1000000; i++) {
            sr = buildScanResult("01:02:03:04:05:06", 70);
        }
        long elapsedNanos = System.nanoTime() - start;
    
        LOGGER.info("buildScanResult: " + elapsedNanos);
    
        start = System.nanoTime();
        for (int i = 0; i < 1000000; i++) {
            sr = cloneScanResult(sr);
        }
        elapsedNanos = System.nanoTime() - start;
    
        LOGGER.info("cloneScanResult: " + elapsedNanos);
    }
    

    显示了这些结果:

    2016 年 10 月 26 日下午 3:25:19 com.example.neutrino.maze.MovingAverageQueueTests buildVsClonePerformanceTest 信息:buildScanResult:202072179 2016 年 10 月 26 日下午 3:25:21 com.example.neutrino.maze.MovingAverageQueueTests buildVsClonePerformanceTest 信息:cloneScanResult:2004391903

    因此,即使使用反射,这种方式的克隆效率也比创建实例的效率低 10 倍。我知道这个测试并不稳健,因为在编译时进行了优化......但是很难减轻十倍的影响。我也测试了 10K 次迭代,然后这个因子甚至是 100!仅供参考。

    附:让 Parcel.obtain() 和 parcel.recycle 脱离循环并没有帮助

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-10-07
      • 1970-01-01
      • 2013-02-22
      • 2011-04-11
      • 2020-05-04
      • 1970-01-01
      相关资源
      最近更新 更多