测试kivy编写的应用程序时如何与UI交互?

How to interact with the UI when testing an application written by kivy?

应用由kivy编写。 我想通过 pytest 测试一个函数,但是为了测试那个函数,我需要先初始化对象,但是对象在初始化时需要 UI 中的东西,但我正处于测试阶段,所以不要不知道如何从 UI.

检索某些内容

这是class,有错误,已处理

class SaltConfig(GridLayout):
    def check_phone_number_on_first_contact(self, button):
        s = self.instanciate_ServerMsg(tt)

        try:
            s.send()
        except HTTPError as err:
            print("[HTTPError] : " + str(err.code))
            return

        # some code when running without error

    def instanciate_ServerMsg():
        return ServerMsg()

这是帮助程序 class,它生成前者 class 使用的 ServerMsg 对象。

class ServerMsg(OrderedDict):
    def send(self,answerCallback=None):
        #send something to server via urllib.urlopen

这是我的测试代码:

class TestSaltConfig:
    def test_check_phone_number_on_first_contact(self):
        myError = HTTPError(url="http://127.0.0.1", code=500,
                        msg="HTTP Error Occurs", hdrs="donotknow", fp=None)

    mockServerMsg = mock.Mock(spec=ServerMsg)
    mockServerMsg.send.side_effect = myError

    sc = SaltConfig(ds_config_file_missing.data_store)

    def mockreturn():
        return mockServerMsg

    monkeypatch.setattr(sc, 'instanciate_ServerMsg', mockreturn)
    sc.check_phone_number_on_first_contact()

我无法初始化该对象,它会在初始化时抛出一个 AttributeError,因为它需要来自 UI 的一些值。

所以我卡住了。

我尝试模拟对象然后将函数修补到原始对象,但也不起作用,因为函数本身具有与 UI.

相关的逻辑

如何解决?谢谢

我写了一篇关于使用简单的运行器测试 Kivy 应用程序的文章 - KivyUnitTest。它适用于 unittest,而不适用于 pytest,但重写它应该不难,以便满足您的需要。在文章中,我解释了如何 "penetrate" UI 的主循环,这样你就可以愉快地使用按钮:

button = <button you found in widget tree>
button.dispatch('on_release')

还有更多。基本上你可以用这样的测试做任何事情,你不需要单独测试每个功能。我的意思是......这是一个很好的做法,但有时(主要是在测试 UI 时),你不能把它撕掉然后放到一个不错的 50 行测试中。

通过这种方式,您可以执行与临时用户在使用您的应用时所做的完全相同的事情,因此您甚至可以发现在测试临时用户时遇到的问题,例如一些 weird/unexpected 用户行为。

这是骨架:

import unittest

import os
import sys
import time
import os.path as op
from functools import partial
from kivy.clock import Clock

# when you have a test in <root>/tests/test.py
main_path = op.dirname(op.dirname(op.abspath(__file__)))
sys.path.append(main_path)

from main import My


class Test(unittest.TestCase):
    def pause(*args):
        time.sleep(0.000001)

    # main test function
    def run_test(self, app, *args):
        Clock.schedule_interval(self.pause, 0.000001)

        # Do something

        # Comment out if you are editing the test, it'll leave the
        # Window opened.
        app.stop()

    def test_example(self):
        app = My()
        p = partial(self.run_test, app)
        Clock.schedule_once(p, 0.000001)
        app.run()

if __name__ == '__main__':
    unittest.main()

但是,正如 Tomas 所说,您应该尽可能将 UI 和逻辑分开,或者更好地说,当它是一件 高效 的事情时。您不想模拟整个大型应用程序只是为了测试需要与 UI.

通信的单个函数

终于搞定了,把事情搞定,我想一定有更优雅的解决方案。这个想法很简单,因为除了 s.send() 语句之外,所有行都只是简单的赋值。

然后我们只是模拟原始对象,每次在测试阶段弹出一些错误(因为对象缺少UI中的一些值),我们模拟它,我们重复这个步骤直到测试方法最终可以测试函数是否可以处理 HTTPError

在这个例子中,我们只需要mock一个PhoneNumber class这还算幸运,但有时我们可能需要处理更多,所以显然@KeyWeeUsr的答案更理想生产环境的选择。但我只是在这里为想要快速解决方案的人列出我的想法。

@pytest.fixture
def myHTTPError(request):
    """
    Generating HTTPError with the pass-in parameters 
    from pytest_generate_tests(metafunc)
    """
    httpError = HTTPError(url="http://127.0.0.1", code=request.param,
                          msg="HTTP Error Occurs", hdrs="donotknow", fp=None)
    return httpError

class TestSaltConfig:
    def test_check_phone_number( self, myHTTPError, ds_config_file_missing ):
        """
        Raise an HTTP 500 error, and invoke the original function with this error.
        Test to see if it could pass, if it can't handle, the test will fail.
        The function locates in configs.py, line 211
        This test will run 2 times with different HTTP status code, 404 and 500
        """

        # A setup class used to cover the runtime error
        # since Mock object can't fake properties which create via __init__()
        class PhoneNumber:
            text = "610274598038"

        # Mock the ServerMsg class, and apply the custom 
        # HTTPError to the send() method
        mockServerMsg = mock.Mock(spec=ServerMsg)
        mockServerMsg.send.side_effect = myHTTPError

        # Mock the SaltConfig class and change some of its 
        # members to our custom one
        mockSalt = mock.Mock(spec=SaltConfig)
        mockSalt.phoneNumber = PhoneNumber()
        mockSalt.instanciate_ServerMsg.return_value = mockServerMsg
        mockSalt.dataStore = ds_config_file_missing.data_store

        # Make the check_phone_number_on_first_contact() 
        # to refer the original function
        mockSalt.check_phone_number = SaltConfig.check_phone_number

        # Call the function to do the test
        mockSalt.check_phone_number_on_first_contact(mockSalt, "button")