Как взаимодействовать с пользовательским интерфейсом при тестировании приложения, написанного kivy?

Приложение написано kivy. Я хочу протестировать функцию через pytest, но для проверки этой функции мне нужно сначала инициализировать объект, но при инициализации объекту нужно что-то из пользовательского интерфейса, но я нахожусь на этапе тестирования, поэтому не знаю, как получить что-то из пользовательского интерфейса.

Это класс, который имеет ошибку и был обработан

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()

Это вспомогательный класс, который генерирует объект 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 при инициализации, так как ему нужно какое-то значение из пользовательского интерфейса.

Поэтому я застреваю.

Я попытался смоделировать объект, а затем исправить функцию на исходную, но это тоже не сработает, поскольку сама функция имеет логику, связанную с пользовательским интерфейсом.

Как это решить? Спасибо


person Albert Gao    schedule 17.11.2016    source источник
comment
Звучит как недостаток конструкции. Логика не должна зависеть от пользовательского интерфейса. Одна из причин этого заключается в том, что вы можете протестировать его изолированно.   -  person Tomasz Nocoń    schedule 18.11.2016


Ответы (2)


Я сделал статью о тестировании приложений Kivy вместе с простым раннером — KivyUnitTest. Он работает с unittest, а не с pytest, но его несложно переписать так, чтобы он соответствовал вашим потребностям. В статье я объясняю, как «проникнуть» в основной цикл пользовательского интерфейса, и таким образом вы можете с радостью пойти и сделать с помощью кнопки следующее:

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

и многое другое. По сути, с таким тестом можно делать что угодно, и вам не нужно тестировать каждую функцию отдельно. Я имею в виду... это хорошая практика, но иногда (в основном при тестировании пользовательского интерфейса) вы не можете просто вырвать это и поместить в хороший 50-строчный тест.

Таким образом, вы можете делать то же самое, что и обычный пользователь при использовании вашего приложения, и, следовательно, вы даже можете обнаружить проблемы, с которыми у вас возникнут проблемы при обычном тестировании, например. какое-то странное/неожиданное поведение пользователя.

Вот скелет:

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()

Однако, как сказал Томас, по возможности следует разделять пользовательский интерфейс и логику, или, лучше сказать, когда это эффективно. Вы не хотите издеваться над всем своим большим приложением только для того, чтобы протестировать одну функцию, которая требует связи с пользовательским интерфейсом.

person Peter Badida    schedule 18.11.2016
comment
Как вы wait_for отображаете элемент? Просто опросить дерево виджетов? Это будет неэффективно - person Janus Troelsen; 30.06.2018

Наконец-то сделал это, просто делай дела, я думаю, должно быть более элегантное решение. Идея проста, учитывая тот факт, что все строки представляют собой простое присвоение значений, кроме оператора s.send().

Затем мы просто имитируем исходный объект, каждый раз, когда на этапе тестирования появляются какие-то ошибки (поскольку в объекте отсутствуют некоторые значения из пользовательского интерфейса), мы имитируем его, повторяем этот шаг до тех пор, пока метод тестирования не сможет, наконец, проверить, может ли функция обрабатывать HTTPError или нет.

В этом примере нам нужно только смоделировать класс PhoneNumber, который нам повезло, но иногда нам может понадобиться обработать больше, поэтому очевидно, что ответ @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")
person Albert Gao    schedule 20.11.2016