安卓测试工具离线用例

17

我在进行仪器测试时使用了Robotium。大部分情况下,我可以测试一切,但是离线情况除外。

只要我禁用数据(使用adb、模拟器中的F8快捷键等...),测试就会断开连接。它在设备/模拟器中继续运行,但没有结果报告。

所以,我想到了一个想法,只将应用程序置于脱机模式,而不是整个设备。问题是我不知道怎么做...

使用iptablesApi需要对我的设备进行root。我已经阅读过Mobiwol应用程序使用某种VPN来限制应用程序的互联网访问,而无需对设备进行root。

问题 Mobiwol应用程序如何按应用程序阻止互联网连接?或者还有其他方法来离线测试APK吗?

编辑12/30/2014

我忘了说,我能够在离线状态下运行测试,但必须在设备处于离线状态时启动测试。目前,我将我的测试分为离线和联机两个部分。在运行ONLINEs之后,我执行著名的adb kill-serveradb start-server。然后我执行OFFLINEs。


1
你在离线场景中究竟想要做什么?你是想访问外部服务器吗? - user2511882
1
是的,我正在尝试连接外部服务器,然后处理异常,例如我有一个空列表修改,显示“没有互联网”连接或某些功能在应用程序离线时不应弹出等。 - Amio.io
下面出现了一个一模一样的答案,建议查看此答案以解决另一个问题。由于下面的复制可能会被删除,我将此建议保存在评论中。 - undefined
7个回答

11

由于这里似乎有不同的问题,我只是提出一些建议。 1)如果您只想在运行离线测试用例之前关闭数据,则可以尝试使用Robotium本身来实现。

例如: 对于WiFi:

WifiManager wifi=(WifiManager)solo.getCurrentActivity().getSystemService(Context.WIFI_SERVICE);
wifi.setWifiEnabled(false);

移动数据(使用反射):

ConnectivityManager dataManager=(ConnectivityManager)solo.getCurrentActivity().getSystemService(Context.CONNECTIVITY_SERVICE);

Method dataClass = ConnectivityManager.class.getDeclaredMethod(“setMobileDataEnabled”, boolean.class);
dataClass.setAccessible(true);
dataClass.invoke(dataManager, true);
你可以在 OFFLINE 测试套件中的 setup() 方法运行单个测试用例之前执行上述两个调用。当 OFFLINE 测试套件中的所有测试用例都完成后,可以在 teardown() 方法的最后启用 WiFi/DATA。
2)看着你在 OP 中发布的应用程序,它似乎基本上:
  • 根据操作系统版本使用 ipTables

  • 创建一个脚本标头,基于需要 WiFi/Data 的所有应用程序的 UID

  • 应该从包管理器中获取设备上安装的应用程序列表以及任何隐藏应用程序等。

  • 再次根据用户选择执行脚本并使用用户所需的规则覆盖 ipTable 中的现有规则。

很确定这些都很难编码...以项目符号的形式表达起来要容易得多。
希望这可以在一定程度上帮助您。
更新:确保您在应用程序清单中具有设置 WiFi/Data 开/关权限的必要权限。而不是测试 apk 清单。必须是应用程序清单本身。 有这个库可能会帮到你。它是 Solo 的扩展。 http://adventuresinqa.com/2014/02/17/extsolo-library-to-extend-your-robotium-test-automation/

嘿,伙计,很高兴你写了。我迫不及待地想检查一下,希望第一个解决方案能够奏效。我现在就去检查它! - Amio.io
很不幸,Robotium Solo禁用数据也会断开测试。至于第二点:我已经读到他们使用了某种VPN而非ipTables。因为对于ipTables,你需要对设备进行root操作。 - Amio.io
请检查答案的编辑。我发布了一个链接到extSolo,可能会对您有所帮助。 - user2511882
只需在真实设备上运行测试吗? - user2511882
3
自 Android 9 开始,未签名的应用程序无法更改 WiFi 状态,因此无法再使用此解决方案。另外,禁用移动数据的反射在 Android KitKat 版本之后已经失效(如我所记得的那样)。 - Lorenzo Vincenzi
显示剩余4条评论

7

在尝试了类似于user2511882的解决方案数小时后,我仍然遇到了一个异常,因为缺少权限(是的,“修改系统设置”权限已经被激活)。

最终我使用UI automator完成了它:

public static void setAirplaneMode(boolean enable)
{
    if ((enable ? 1 : 0) == Settings.System.getInt(getInstrumentation().getContext().getContentResolver(),
            Settings.Global.AIRPLANE_MODE_ON, 0))
    {
        return;
    }
    UiDevice device = UiDevice.getInstance(getInstrumentation());
    device.openQuickSettings();
    // Find the text of your language
    BySelector description = By.desc("Airplane mode");
    // Need to wait for the button, as the opening of quick settings is animated.
    device.wait(Until.hasObject(description), 500);
    device.findObject(description).click();
    getInstrumentation().getContext().sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
}

您需要在 androidTest 清单文件中添加 ACCESS_NETWORK_STATE:

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

测试结束后不要忘记禁用它。

如果您使用的语言不是英语,则需要将“飞行模式”更改为您所使用的语言的文本。由于我有多种翻译,因此我从资源字符串中读取它。


这绝对是我找到的最佳解决方案。谢谢。 - ullstrm
这个很好用。我只需要添加一些 Thread.sleep 就可以让测试通过了。 - pavi2410

3

LinkedIn的Test Butler有一个非常好用的库,你只需调用以下代码即可轻松启用或禁用WiFi和移动数据:

TestButler.setGsmState(false);
TestButler.setWifiState(false);

这个库的主要优点是不需要在清单文件中申请任何权限,更多详细信息请参考项目网站:

https://github.com/linkedin/test-butler


1
我认为TestButler只适用于模拟器测试。如果我要在真实设备上进行测试怎么办? - IgorGanapolsky

1
抱歉如果我过于简化了,但是只需将手机/模拟器设置为飞行模式怎么样?通过实际用户界面。这是我用来测试离线情况的方法。

3
那样做没有帮助。我想要完全自动化测试。我可以从Java中关闭互联网。问题是,当我关闭它(无论是按照您建议的方式还是其他方式),测试会断开连接并在此发生时立即停止。然后,尽管测试仍在断开连接的设备上运行,但我无法接收测试结果。如果我可以防止JUnits在设备/模拟器切换到飞行模式时停止,我就可以重新连接到设备并继续测试。 - Amio.io

1

这里有一个解决方案,它使用UiAutomator来从下拉状态栏启用或禁用“飞行模式”,如果启用,则关闭所有网络连接。它适用于大多数Android操作系统版本,并使用了用户Aorlinn(2019年10月12日)的答案的部分内容。

app/build.gradle

dependencies {
    androidTestImplementation "androidx.test.uiautomator:uiautomator:2.2.0"
}

AndroidManifest.xml

不需要额外的权限,甚至不需要 <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> 因为UiAutomator仅在状态栏UI上点击按钮。因此,应用程序不需要直接访问修改设备的Wifi或移动数据设置。

Kotlin

import android.os.Build
import android.provider.Settings
import androidx.test.espresso.matcher.ViewMatchers.assertThat
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObjectNotFoundException
import androidx.test.uiautomator.Until
import org.hamcrest.Matchers.`is`
import org.junit.Assert
import org.junit.Assume.assumeNoException
import org.junit.Assume.assumeThat
import java.io.IOException


private var airplaneModeOn = 0
private val OFF = 0
private val ON = 1
private var airplaneModeButtonPosition: Point? = null

/**
 * Turn off the Internet connectivity by switching "Aeroplane mode" (flight mode) on. From:
 * https://dev59.com/wl4c5IYBdhLWcg3wuMJ7
 */
@Test
fun checkAppWillWork_withNoInternetConnection() {
    try {
        airplaneModeOn = getCurrentAirplaneModeSetting()
    } catch (e: Exception) {
        e.printStackTrace()
        assumeNoException("Cannot retrieve device setting of 'Airplane Mode'. Aborting the test.", e)
    }

    // Check that Airplane mode is off
    assertThat(airplaneModeOn, `is`(OFF))

    // Press the "Aeroplane mode" button on the 'Quick Settings' panel
    toggleAeroplaneModeButton()

    // Verify Airplane mode is on
    assumeThat("Cannot change the 'Aeroplane Mode' setting. Test aborted.", airplaneModeOn, `is`(ON))
    assertThat(airplaneModeOn, `is`(ON))

    // Do some tests when the Internet is down
    // Note: Switch the Internet back on in cleanup()
}

/**
 * Turn the Internet connectivity on or off by pressing the "Aeroplane mode" button. It opens the
 * status bar at the top, then drags it down to reveal the buttons on the 'Quick Settings' panel.
 */
@Throws(UiObjectNotFoundException::class)
private fun toggleAeroplaneModeButton() {
    try {
        airplaneModeOn = getCurrentAirplaneModeSetting()
    } catch (e: SecurityException) {
        e.printStackTrace()
        Assert.fail()
    } catch (e: IOException) {
        e.printStackTrace()
        Assert.fail()
    }

    // Open the status bar at the top; drag it down to reveal the buttons
    val device = UiDevice.getInstance(getInstrumentation())
    device.openQuickSettings()

    // Wait for the button to be visible, because opening the Quick Settings is animated.
    // You can use any string here; You only need a time delay wait here.
    val description = By.desc("AeroplaneMode")
    device.wait(Until.hasObject(description), 2000)

    // Search for and click the button
    var buttonClicked = clickObjectIfFound(device,
            "Aeroplane mode", "Airplane mode", "機内モード", "Modo avión")
    if (!buttonClicked) {
        // Swipe the Quick Panel window to the LEFT, if possible
        val screenWidth = device.displayWidth
        val screenHeight = device.displayHeight
        device.swipe((screenWidth * 0.80).toInt(), (screenHeight * 0.30).toInt(),
                     (screenWidth * 0.20).toInt(), (screenHeight * 0.30).toInt(), 50)
        buttonClicked = clickObjectIfFound(device,
                "Aeroplane mode", "Airplane mode", "機内モード", "Modo avión")
    }
    if (!buttonClicked) {
        // Swipe the Quick Panel window to the RIGHT, if possible
        val screenWidth = device.displayWidth
        val screenHeight = device.displayHeight
        device.swipe((screenWidth * 0.20).toInt(), (screenHeight * 0.30).toInt(),
                     (screenWidth * 0.80).toInt(), (screenHeight * 0.30).toInt(), 50)
        clickObjectIfFound(device,
                "Aeroplane mode", "Airplane mode", "機内モード", "Modo avión")
    }

    // Wait for the Internet to disconnect or re-connect
    device.wait(Until.hasObject(description), 6000)

    // Close the Quick Settings panel
    getInstrumentation().context
            .sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))

    // Verify change in device settings
    try {
        airplaneModeOn = getCurrentAirplaneModeSetting()
    } catch (e: SecurityException) {
        e.printStackTrace()
        Assert.fail()
    } catch (e: IOException) {
        e.printStackTrace()
        Assert.fail()
    }
}

/**
 * On Android 8/9, use an 'adb shell' command to retrieve the "Airplane Mode" setting, like this:
 *
 *
 * `adb shell settings list global | grep airplane_mode_on   ==>   "airplane_mode_on=0"`
 *
 *
 * (But note that grep is not available on the Android shell. See guidance URLs below)
 *
 *
 *  * https://www.reddit.com/r/tasker/comments/fbi5ai/psa_you_can_use_adb_to_find_all_the_settings_that/
 *  * https://dev59.com/pVsX5IYBdhLWcg3wUuCa
 *
 *
 * On all other Android OS versions, use `Settings.System.getInt()` to retrieve the "Airplane Mode" setting.
 * It sets `airplaneModeOn = 1 (true)` or `airplaneModeOn = 0 (false)`
 *
 * @throws IOException if `executeShellCommand()` didn't work
 * @throws SecurityException if `Settings.System.getInt()` didn't work
 */
@Throws(IOException::class, SecurityException::class)
private fun getCurrentAirplaneModeSetting(): Int {
    if (Build.VERSION.SDK_INT in 26..28 /*Android 8-9*/) {
        val shellResponse = UiDevice
                .getInstance(getInstrumentation())
                .executeShellCommand("settings list global")

        airplaneModeOn = when {
            shellResponse.contains("airplane_mode_on=1") -> 1
            shellResponse.contains("airplane_mode_on=0") -> 0
            else -> throw IOException("Unsuitable response from adb shell command 'settings list global'")
        }

    } else {
        // Oddly this causes a SecurityException on Android 8,9 devices
        airplaneModeOn = Settings.System.getInt(
                getInstrumentation().context.contentResolver,
                Settings.Global.AIRPLANE_MODE_ON,
                0)
    }
    return airplaneModeOn
}

/**
 * Make UiAutomator search for and click a single button, based on its text label.
 *
 * Sometimes multiple buttons will match the required text label. For example,
 * when Airplane mode is switched on, "Mobile data Aeroplane mode" and "Aeroplane mode"
 * are 2 separate buttons on the Quick Settings panel, on Android 10+.
 * For Aeroplane mode, always click on the last matched item.
 *
 * @param textLabels You must supply the language variants of the button that you want to click,
 * for example "Cancel" (English), "Cancelar" (Spanish), "취소" (Korean)
 * @return True if a button was found and clicked, otherwise return false
 */
private fun clickObjectIfFound(device: UiDevice, vararg textLabels: String): Boolean {
    for (languageVariant in textLabels) {
        val availableButtons = device.findObjects(By.text(languageVariant))
        if (availableButtons.size >= 1) {
            if (airplaneModeButtonPosition == null) {
                availableButtons[availableButtons.size - 1].click()
                airplaneModeButtonPosition = availableButtons[availableButtons.size - 1].visibleCenter
                return true
            } else {
                // Use the stored position to avoid clicking on the wrong button
                for (button in availableButtons) {
                    if (button.visibleCenter == airplaneModeButtonPosition) {
                        button.click()
                        airplaneModeButtonPosition = null
                        return true
                    }
                }
            }
        }
    }
    return false
}

@After
fun cleanup() {
    // Switch the Internet connectivity back on, for other tests.
    if (airplaneModeOn == ON) {
        toggleAeroplaneModeButton()
        assertThat(airplaneModeOn, `is`(OFF))
    }
}

如果您的设备语言不是英语,您将需要调整字符串"Aeroplane mode"。例如,您可以在此处检查大约70种语言的翻译:https://github.com/aosp-mirror/platform_frameworks_base/search?q=global_actions_toggle_airplane_mode


就个人而言,我认为最好只启用ACCESS_NETWORK_STATE并使用Aorlinn的解决方案通过Settings.System在程序中进行编程检查,而不是尝试解析shell命令的输出。 - Adam Burley
你可能会注意到在getCurrentAirplaneModeSetting()函数中,我同时使用了Settings.System.getInt()executeShellCommand()来获取当前的飞行模式设置。之所以使用了两种不同的策略,是因为其中一种方法在所有的Android操作系统版本上都不起作用。 - undefined
你说有一个“SecurityException”,这是在你把“ACCESS_NETWORK_STATE”添加到了androidTest清单之后吗? - undefined

0
请使用@Illyct的解决方案(请给他们的答案点赞https://dev59.com/KFkS5IYBdhLWcg3wgXAQ#64765567)。
InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand("svc wifi disable")
InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand("svc data disable")

-1
由于@user2511882的回答,您可以在Android X Test中使用应用程序上下文而不是Activity,方法如下:
internal fun switchWifi(value: Boolean) {
val wifiManager = ApplicationProvider.getApplicationContext<YourApplicationClass>().getSystemService(Context.WIFI_SERVICE) as WifiManager
wifiManager.isWifiEnabled = value}

请注意,此方法仅适用于 API <= 28。还有其他方法,例如使用 UI AutomatorAPI > 28

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接