Albert Gao Albert Gao - 15 days ago 6
Python Question

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

The application is written by kivy.
I want to test a function via pytest, but in order to test that function, I need to initalize the object first, but the object needs something from the UI when initalizing, but I am at testing phase, so don't know how to retrieve something from the UI.

This is the class which has an error and has been handled

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


This is the helper class which generates the ServerMsg object used by the former class.

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


This is my tests code:

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


I can't initialize the object, it will throw an AttributeError when initialzing since it needs some value from UI.

So I get stuck.

I tried to mock the object then patch the function to the original one, but won't work either since the function itself has has logic related to UI.

How to solve it? Thanks

Answer

I made an article about testing Kivy apps together with a simple runner - KivyUnitTest. It works with unittest, not with pytest, but it shouldn't be hard to rewrite it, so that it fits your needs. In the article I explain how to "penetrate" the main loop of UI and this way you can happily go and do with button this:

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

and many more. Basically you can do anything with such a test and you don't need to test each function independently. I mean... it's a good practice, but sometimes (mainly when testing UI), you can't just rip the thing out and put it into a nice 50-line test.

This way you can do exactly the same thing as a casual user would do when using your app and therefore you can even catch issues you'd have trouble with when testing the casual way e.g. some weird/unexpected user behavior.

Here's the skeleton:

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

However, as Tomas said, you should separate UI and logic when possible, or better said, when it's an efficient thing to do. You don't want to mock your whole big application just to test a single function that requires communication with UI.

Comments