如何让自动化测试更加灵活简洁?

简化的架构对于自动化测试和主代码一样重要。冗余和不灵活性可能会导致一些问题:比如 UI 中的任何更改都需要更新多个文件,测试可能在功能上相互重复,并且支持新功能可能会变成一项耗时且有挑战性的工作来适应现有测试。

我的名字是Arseny Fedorov。我是卡巴斯基安全软件 Android 版团队的一名测试软件开发工程师。在本文中,我将展示一种不同的自动化测试开发方法和一些最佳实践,可以帮助大家避免上述问题。

页面对象模式如何理顺代码

在为应用程序编写测试时,我们需要在运行各种检查或操作时引用应用程序的视图元素。如果我们总是在编写的每个测试中明确说明元素 ID,这将使我们的代码容易受到 UI 更改的影响:我们必须在使用这些元素的每个测试中更新所有已更改的 ID。

页面对象模式有助于避免这种情况。页面对象模式的理念是将页面(应用程序屏幕)作为一个对象(测试抽象)来呈现,该对象会公布和初始化页面上的所有图形元素,并设置与它们的交互。有关该模式的详细信息可以在此处了解(https://kasperskylab.github.io/Kaspresso/en/Wiki/Page_object_in_Kaspresso/)。

本文中的所有示例都使用我们的开源测试自动化框架 Kaspresso。(https://github.com/KasperskyLab/Kaspresso)为什么不使用Espresso?

首先,Kaspresso使用声明式方法编写测试,这种方法依赖于Kakao,它是Espresso的Kotlin DSL封装器。下面是一个例子:

Espresso

@Test
fun testFirstFeature() {
    onView(withId(R.id.toFirstFeature))
        .check(ViewAssertions.matches(
               ViewMatchers.withEffectiveVisibility(
                       ViewMatchers.Visibility.VISIBLE)))
    onView(withId(R.id.toFirstFeature)).perform(click())
}

Kaspresso​​​​​​​

@Test
fun testFirstFeature() {
    MainScreen {
        toFirstFeatureButton {
            isVisible()
            click()
        }
    }
}

其次,在拦截器的帮助下,Kaspresso 避免了测试的不稳定性,从而提高了稳定性。这些拦截器在我们处理异步图形元素或列表时特别有用。

第三,Kaspresso集成了KAutomator,这是一个方便的Kotlin DSL封装器,可用于UI Automator,从而加快UI测试的速度。下面是标准版(右)和加速版(左)UI Automator之间的区别:

图片

除此之外,Kaspresso 允许将测试分解为步骤,类似于手动测试用例的完成方式,并记录每个步骤。如果测试崩溃,日志将帮助你立即查看哪些步骤成功完成,哪些步骤失败。除了日志之外,你还可以访问图形元素的层次结构以及视频、屏幕截图等。Kaspresso 内置的 Android 调试桥 (adb) 支持将帮助你直接使用 Android。Allure集成可清晰显示测试结果。

那么,让我们开始讨论正题。你可以通过下载项目源代码并运行它来重现下面描述的所有步骤。我们将描述 MainActivity 页面并自动化 LoginActivity 测试。结果以及测试可在TECH-tutorial-results分支中找到,因此你可以随时前往那里查看完成的代码。

MainActivity 看起来像这样:

图片

我们创建一个继承自 KScreen 的 MainScreen 对象:

object MainScreen : KScreen<MainScreen>() {
    override val layoutId: Int? = null
    override val viewClass: Class<*>? = null
}

KScreen 实现了页面对象模式,它描述了与测试交互的所有视图元素。

Kaspresso 中的页面对象实现以 layoutId 和 viewClass 变量而闻名,它们可以帮助开发人员立即识别哪个布局文件用于相关页面以及哪个类提供其功能。但手头的任务是讨论页面对象概念本身,因此我们现在将它们设置为 null。

我们使用 Android Studio 中的 UI Automator Viewer 或 Layout Inspector 来查找登录活动按钮的 ID。页面上其余视图元素的标识符可以类似地找到。

图片

主屏幕元素的描述如下所示:

object MainScreen : KScreen<MainScreen>() {
    override val layoutId: Int? = null
    override val viewClass: Class<*>? = null


    val titleTextView = KTextView { withId(R.id.title) }
    val simpleActivityButton = KButton { withId(R.id.simple_activity_btn) }
    val wifiActivityButton = KButton { withId(R.id.wifi_activity_btn) }
    val notificationActivityButton = KButton { withId(R.id.notification_activity_btn) }


    val loginActivityButton = KButton { withId(R.id.login_activity_btn) }
   
    val makeCallActivityButton = KButton { withId(R.id.make_call_activity_btn) }
    val flakyActivityButton = KButton { withId(R.id.flaky_activity_btn) }
    val listActivityButton = KButton { withId(R.id.list_activity_btn) }
}

现在,我们可以从我们创建的任何测试中引用 MainScreen 对象,并使用此页面的视图元素。

让我们编写第一个测试,它将检查页面上是否有“登录活动”按钮并单击它。

为此,我们创建一个继承自 TestCase 的 LoginActivityTest 类:

class LoginActivityTest : TestCase() {
    /**
     * activityScenarioRule is used to invoke MainActivity before running the test.
     * More details on activityScenarioRule are available here:
     * https://developer.android.com/reference/androidx/test/ext/junit/rules/ActivityScenarioRule
     */
    @get:Rule
    val activityRule = activityScenarioRule<MainActivity>()


    @Test
    fun test() {
        MainScreen {
            loginActivityButton {
                isVisible()
                click()
            }
        }
    }
}

正如你在测试代码中看到的,在创建 MainScreen 对象后,我们可以使用几行来引用页面元素,运行我们需要的检查,然后单击按钮。随着 LoginActivity 页面打开,测试结束。我们看看它的布局……

图片

...并创建登录屏幕:

object LoginScreen : KScreen<LoginScreen>() {
    override val layoutId: Int? = null
    override val viewClass: Class<*>? = null


    val usernameEditText = KEditText { withId(R.id.input_username) }
    val passwordEditText = KEditText { withId(R.id.input_password) }
    val loginButton = KButton { withId(R.id.login_btn) }
}

让我们修改 LoginActivityTest 并尝试使用登录名“123456”和密码“123456”获得授权:​​​​​​​

class LoginActivityTest : TestCase() {
    @get:Rule
    val activityRule = activityScenarioRule<MainActivity>()


    @Test
    fun test() {
        val username = "123456"
        val password = "123456"


        MainScreen {
            loginActivityButton {
                isVisible()
                click()
            }
        }
        LoginScreen {
            usernameEditText { replaceText(username) }
            passwordEditText { replaceText(password) }
            loginButton { click() }
        }
    }
}

授权后,我们会看到最后一个页面 AfterLoginActivity。

图片

Kaspresso 可以使用Device类从测试内部检查正在显示的活动。我们通过检查授权后设备屏幕上是否出现 AfterLoginActivity 来结束第一个测试:​​​​​​​

class LoginActivityTest : TestCase() {
    @get:Rule
    val activityRule = activityScenarioRule<MainActivity>()


    @Test
    fun test() {
        val username = "123456"
        val password = "123456"


        MainScreen {
            loginActivityButton {
                isVisible()
                click()
            }
        }
        LoginScreen {
            usernameEditText { replaceText(username) }
            passwordEditText { replaceText(password) }
            loginButton { click() }
        }


        device.activities.isCurrent(AfterLoginActivity::class.java)
    }
}

如果我们不使用页面对象模式,我们的代码将如下所示:

这种方法使得动态了解哪些测试字符串与哪些页面交互变得更加困难。添加新的检查和操作可能会使代码难以辨认。因此,我们建议使用页面对象来创建高质量的可扩展测试。

将测试分为几个步骤

任何测试,无论是自动测试还是手动测试,都要遵循一个测试用例--也就是说,测试人员要检查一连串的步骤,以确定页面是否功能齐全。在 step() 函数的帮助下,Kaspresso 将代码分解成多个步骤。步骤还有助于整理测试日志。

要使用步骤,需要在测试中调用 run{} 方法,并在大括号中列出测试中要运行的所有步骤。每个步骤都应在 step() 函数中调用。

让我们试一下:​​​​​​​

class LoginActivityTest : TestCase() {
    @get:Rule
    val activityRule = activityScenarioRule<MainActivity>()


    @Test
    fun test() {
        run {
            val username = "123456"
            val password = "123456"
           
            step("Open login screen") {
                MainScreen {
                    loginActivityButton {
                        isVisible()
                        click()
                    }
                }
            }


            step("Try to login") {
                LoginScreen {
                    usernameEditText { replaceText(username) }
                    passwordEditText { replaceText(password) }
                    loginButton { click() }
                }
            }


            step("Check current screen") {
                device.activities.isCurrent(AfterLoginActivity::class.java)
            }
        }
    }
}

通过这些步骤,标记为“KASPRESSO”的信息级日志如下所示:

图片

如果你对步骤仍有疑问,建议你阅读这些(https://kasperskylab.github.io/Kaspresso/en/Tutorial/Steps_and_sections/)。它还提供了你可能在日志中注意到的之前/之后部分的详细信息。

现在,让我们尝试实施负面测试用例,例如用户输入的登录名或密码少于最小字符数(6 个)。

在创建一组自动测试时,应遵循的规则是为每个测试用例设置一个单独的测试方法。换句话说,我们不会在同一个方法中测试输入无效登录名或密码时的行为,而是在 LoginActivityTest 类中创建单独的方法:​​​​​​​

@Test
fun loginUnsuccessfulIfUsernameIncorrect() {
    run {
        val username = "12"
        val password = "123456"


        step("Open login screen") {
            MainScreen {
                loginActivityButton {
                    isVisible()
                    click()
                }
            }
        }


        step("Try to login") {
            LoginScreen {
                usernameEditText { replaceText(username) }
                passwordEditText { replaceText(password) }
                loginButton { click() }
            }
        }


        step("Check current screen") {
            device.activities.isCurrent(LoginActivity::class.java)
        }
    }
}

另一个测试,使用有效的登录名和无效的密码:​​​​​​​

@Test
fun loginUnsuccessfulIfPasswordIncorrect() {
    run {
        val username = "123456"
        val password = "1234"


        step("Open login screen") {
            MainScreen {
                loginActivityButton {
                    isVisible()
                    click()
                }
            }
        }


        step("Try to login") {
            LoginScreen {
                usernameEditText { replaceText(username) }
                passwordEditText { replaceText(password) }
                loginButton { click() }
            }
        }


        step("Check current screen") {
            device.activities.isCurrent(LoginActivity::class.java)
        }
    }
}

我建议你在执行第一个测试时重命名它,以便其名称显示我们仅检查是否成功授权。​​​​​​​

@Test
fun test()

我们将其更改为:​​​​​​​

@Test
fun loginSuccessfulIfUsernameAndPasswordCorrect()

你可能已经注意到,在上面的自动化测试中,用于导航到 LoginActivity 页面并输入登录凭据的字符串会重复。如果能重复使用这些步骤就好了。

使用Scenario

Kaspresso 包含一个名为 Scenario 的工具,它允许将多个步骤组合成有序的操作序列。这在编写重复步骤的测试时非常有用。

让我们创建一个继承自 Scenario 的 LoginScenario 类。为了使其工作,我们需要重写 steps 属性以列出Scenario中的所有步骤。​​​​​​​

class LoginScenario : Scenario() {
    override val steps: TestContext<Unit>.() -> Unit = {
        step("Open login screen") {
            MainScreen {
                loginActivityButton {
                    isVisible()
                    click()
                }
            }
        }


        step("Try to login") {
            LoginScreen {
                usernameEditText { replaceText(username) }
                passwordEditText { replaceText(password) }
                loginButton { click() }
            }
        }
    }
}

这里的问题是我们没有初始化用户名和密码变量。我们可以通过将这些指定为 LoginScenario 类构造函数中的参数来解决此问题。然后,这部分代码:

class LoginScenario : Scenario()

更改为:​​​​​​​

class LoginScenario(
    private val username: String,
    private val password: String
) : Scenario()

这是生成的Scenario代码:​​​​​​​

class LoginScenario(
    private val username: String,
    private val password: String
) : Scenario() {


    override val steps: TestContext<Unit>.() -> Unit = {
        step("Open login screen") {
            MainScreen {
                loginActivityButton {
                    isVisible()
                    click()
                }
            }
        }


        step("Try to login") {
            LoginScreen {
                usernameEditText { replaceText(username) }
                passwordEditText { replaceText(password) }
                loginButton { click() }
            }
        }
    }
}

让我们在 LoginActivityTest 测试中使用此Scenario:​​​​​​​

class LoginActivityTest : TestCase() {
    @get:Rule
    val activityRule = activityScenarioRule<MainActivity>()


    @Test
    fun loginSuccessfulIfUsernameAndPasswordCorrect() {
        run {
            step("Try to login with correct username and password") {
                scenario(
                    LoginScenario(
                        username = "123456",
                        password = "123456",
                    )
                )
            }


            step("Check current screen") {
                device.activities.isCurrent(AfterLoginActivity::class.java)
            }
        }
    }


    @Test
    fun loginUnsuccessfulIfUsernameIncorrect() {
        run {
            step("Try to login with incorrect username") {
                scenario(
                    LoginScenario(
                        username = "12",
                        password = "123456",
                    )
                )
            }


            step("Check current screen") {
                device.activities.isCurrent(LoginActivity::class.java)
            }
        }
    }


    @Test
    fun loginUnsuccessfulIfPasswordIncorrect() {
        run {
            step("Try to login with incorrect password") {
                scenario(
                    LoginScenario(
                        username = "123456",
                        password = "1234",
                    )
                )
            }


            step("Check current screen") {
                device.activities.isCurrent(LoginActivity::class.java)
            }
        }
    }
}

我们研究了一种有利于使用Scenario的案例——在同一页面的不同测试中重复使用相同的步骤。然而,这并不是Scenario的唯一目的。

一个应用程序可以有多个页面,你只能以授权用户的身份访问这些页面。然后,你需要重新描述每个页面的授权步骤。但是,如果你使用Scenario,这将变得非常简单。

目前,AfterLoginActivity 页面在我们登录后打开。让我们为该屏幕编写一个测试。

首先我们创建一个页面对象:​​​​​​​

object AfterLoginScreen : KScreen<AfterLoginScreen>() {
    override val layoutId: Int? = null
    override val viewClass: Class<*>? = null


    val title = KTextView { withId(R.id.title) }
}

然后我们添加测试:​​​​​​​

class AfterLoginActivityTest : TestCase() {
    @get:Rule
    val activityRule = activityScenarioRule<MainActivity>()


    @Test
    fun test() {


    }
}

我们需要获得授权才能访问该页面。如果没有Scenario,我们将不得不再次重新运行所有步骤:打开主页,单击按钮,输入登录名和密码,然后再次单击按钮。整个过程现在简化为使用 LoginScenario:​​​​​​​

class AfterLoginActivityTest : TestCase() {
    @get:Rule
    val activityRule = activityScenarioRule<MainActivity>()


    @Test
    fun test() {
        run {
            step("Open AfterLogin screen") {
                scenario(
                    LoginScenario(
                        username = "123456",
                        password = "123456"
                    )
                )
            }


            step("Check title") {
                AfterLoginScreen {
                    title {
                        isVisible()
                    }
                }
            }
        }
    }
}

总而言之,使用Scenario使代码干净、清晰且可重用。如果你想要测试仅授权用户可以访问的页面,则无需再重复大量相同的步骤。重要的是,我们还实现了适当的测试可扩展性。如果 LoginActivity 页面上的 UI 元素的标识符发生更改,则不需要更新测试代码。要使测试再次正常工作,你所需要做的就是修复 LoginScreen。

作为对比,这里是没有以上最佳实践的测试代码。我希望你能像一场噩梦一样忘记这种写作风格。​​​​​​​

class LoginActivityTest : TestCase() {
    @get:Rule
    val activityRule = activityScenarioRule<MainActivity>()


    @Test
    fun test() {
        val loginActivityButton = KButton { withId(R.id.login_activity_btn) }
        loginActivityButton {
            isVisible()
            click()
        }
        val usernameEditText = KEditText { withId(R.id.input_username) }
        val passwordEditText = KEditText { withId(R.id.input_password) }
        val loginButton = KButton { withId(R.id.login_btn) }
        usernameEditText { replaceText("123456") }
        passwordEditText { replaceText("123456") }
        loginButton { click() }
        device.activities.isCurrent(AfterLoginActivity::class.java)
        pressBack()
        usernameEditText { replaceText("123456") }
        passwordEditText { replaceText("1234") }
        loginButton { click() }
        device.activities.isCurrent(LoginActivity::class.java)
        usernameEditText { replaceText("12") }
        passwordEditText { replaceText("123456") }
        loginButton { click() }
        device.activities.isCurrent(LoginActivity::class.java)
    }
}

Kaspresso 框架相关链接:

https://github.com/KasperskyLab/Kaspresso

https://kasperskylab.github.io/Kaspresso/en/

行动吧,在路上总比一直观望的要好,未来的你肯定会感谢现在拼搏的自己!如果想学习提升找不到资料,没人答疑解惑时,请及时加入群: 786229024,里面有各种测试开发资料和技术可以一起交流哦。

最后: 下方这份完整的软件测试视频教程已经整理上传完成,需要的朋友们可以自行领取【保证100%免费】在这里插入图片描述
软件测试面试文档
我们学习必然是为了找到高薪的工作,下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料,并且有字节大佬给出了权威的解答,刷完这一套面试资料相信大家都能找到满意的工作。在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

<think>嗯,用户问的是Python自动化测试的优势。首先,我需要回忆一下Python自动化测试中的常见应用和特点。Python的语法简洁,可能是一个优势,因为测试代码需要易读易写。另外,Python有丰富的测试框架,比如pytest、unittest,这些框架能提高效率。还有第三方库的支持,比如Selenium用于Web测试,Appium用于移动端,这些库可能也是优势之一。 跨平台特性应该也是一个点,因为Python可以在不同操作系统上运行,这对测试环境有帮助。社区支持和资源丰富,用户遇到问题容易找到解决方案。还有集成能力,比如与CI/CD工具集成,Jenkins、GitHub Actions这些,可能也是优势。 需要确认这些点是否正确,比如Python是否真的易于学习,第三方库是否丰富。另外,是否要提到执行速度?虽然Python本身可能不如Java或C++快,但自动化测试中效率可能更多取决于框架和并行处理能力,比如pytest的并行执行功能。或者资源消耗是否较低? 可能还需要比较其他语言,比如Java的TestNG,或者Ruby的Watir,但用户可能不需要对比,只要Python的优点。需要组织这些点,按重要性或逻辑顺序排列,确保结构清晰。比如先语法简单,再框架和库,然后跨平台,社区,集成,最后扩展性等。可能需要举例子,比如用pytest写测试用例更简洁,或者用Selenium做Web自动化的例子。 还要注意用户可能关心实际应用场景,比如Web、API、移动端测试,Python在这些领域的工具支持。另外,是否支持数据驱动测试、行为驱动开发(BDD)比如behave库,这些也是优势。还有报告生成,比如Allure框架集成。 需要确保每个优势点都有具体解释,避免太笼统。比如提到语法简洁时,可以对比其他语言,用代码示例说明。但用户要求回答用中文,所以可能不需要实际代码,但可以描述。比如用pytest写测试用例不需要类继承,更简单。 还要注意用户可能想知道为什么选择Python而不是其他语言,所以每个优势点要突出Python的独特之处。比如动态类型让测试脚本更灵活,但可能也要提到类型提示在Python3中的支持,不影响灵活性的同时增加可维护性。 总结下来,大概可以分为几个部分:语法简洁易学、丰富的测试框架和库、跨平台支持、强大的社区和文档、与其他工具集成方便、支持多种测试类型(功能、接口、性能等)、易于扩展和维护、高效的测试执行与管理(如参数化、并行测试)。每个部分需要简要说明,结构清晰,帮助用户逐步理解。</think>Python自动化测试的优势可以从以下几个方面逐步分析,结合其生态和语言特性: --- ### 一、**语法简洁,学习成本低** - **直观易读**:Python的语法接近自然语言,例如用`assert`直接验证结果,测试代码可读性高。 - **快速上手**:相比Java/C++,无需复杂类型声明,适合测试人员快速编写脚本。 **示例**: ```python def test_add(): assert 1 + 1 == 2 # 一行代码完成断言 ``` --- ### 二、**丰富的测试框架和工具链** 1. **主流框架支持** - **pytest**:插件化设计,支持参数化、夹具(Fixture)等高级功能。 ```python @pytest.mark.parametrize("a,b,expected", [(1,2,3), (4,5,9)]) def test_sum(a, b, expected): assert a + b == expected ``` - **unittest**:Python标准库,兼容xUnit风格。 - **Robot Framework**:关键字驱动,适合非技术人员协作。 2. **专项测试工具** - **Web自动化**:Selenium + WebDriver - **API测试**:Requests + Pytest - **移动端测试**:Appium - **性能测试**:Locust --- ### 三、**强大的第三方库生态** - **数据处理**:`pandas`分析测试结果,`numpy`处理性能数据。 - **报告生成**:`Allure`生成可视化报告,`pytest-html`快速导出结果。 - **Mock模拟**:`unittest.mock`隔离依赖项。 --- ### 四、**跨平台兼容性** - **多系统支持**:Windows/Linux/macOS无缝运行同一套脚本。 - **环境隔离**:通过`virtualenv`或`Docker`避免环境冲突。 --- ### 五、**高效的CI/CD集成** - **自动化流水线**:与Jenkins、GitHub Actions等工具无缝对接,触发测试任务。 - **并行执行**:`pytest-xdist`插件实现多进程加速。 --- ### 六、**灵活的扩展能力** - **自定义插件**:可基于业务需求开发专用测试工具。 - **混合编程**:通过`Cython`或调用C库优化关键路径性能。 --- ### 七、**社区与资源支持** - **活跃社区**:Stack Overflow、GitHub等平台问题响应快。 - **文档齐全**:主流工具均有详细官方指南和示例。 --- ### 总结 Python凭借**低门槛语法**、**全场景工具覆盖**和**高度可扩展性**,成为自动化测试的首选语言。无论是Web/API/移动端测试,还是集成到DevOps流程中,Python都能提供高效、稳定的解决方案。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值