{"id":15015493,"url":"https://github.com/keyweeusr/kivyunittest","last_synced_at":"2025-04-12T09:19:03.736Z","repository":{"id":62574312,"uuid":"64707582","full_name":"KeyWeeUsr/KivyUnitTest","owner":"KeyWeeUsr","description":":bee: Test more, cry less!","archived":false,"fork":false,"pushed_at":"2017-09-13T21:17:48.000Z","size":30,"stargazers_count":26,"open_issues_count":1,"forks_count":5,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-04-12T09:18:49.321Z","etag":null,"topics":["kivy","python","tutorial","unit-testing"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/KeyWeeUsr.png","metadata":{"files":{"readme":"README.rst","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-08-01T23:07:19.000Z","updated_at":"2022-04-18T17:30:27.000Z","dependencies_parsed_at":"2022-11-03T18:46:41.434Z","dependency_job_id":null,"html_url":"https://github.com/KeyWeeUsr/KivyUnitTest","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KeyWeeUsr%2FKivyUnitTest","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KeyWeeUsr%2FKivyUnitTest/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KeyWeeUsr%2FKivyUnitTest/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/KeyWeeUsr%2FKivyUnitTest/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/KeyWeeUsr","download_url":"https://codeload.github.com/KeyWeeUsr/KivyUnitTest/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248543836,"owners_count":21121838,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["kivy","python","tutorial","unit-testing"],"created_at":"2024-09-24T19:47:32.690Z","updated_at":"2025-04-12T09:19:03.710Z","avatar_url":"https://github.com/KeyWeeUsr.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"KivyUnitTest\n============\n\n.. image:: https://img.shields.io/pypi/pyversions/kivyunittest.svg\n   :target: https://pypi.python.org/pypi/kivyunittest\n\n.. image:: https://img.shields.io/pypi/v/kivyunittest.svg\n   :target: https://pypi.python.org/pypi/kivyunittest\n\n*Test more, cry less!*\n\nThis script is meant to launch a folder of your tests which will behave as one\nbig test suite. It's done this way because of necessity having a fresh ``python``\ninterpreter for each Kivy application test to run without mistakes (otherwise\nmess from previous ``App().run()`` interferes).\n\nEach unittest file in a folder consisting of tests must start with ``test_``\nprefix and end with ``.py``\n\nRun from console:\n\n.. code::\n\n    python -m kivyunittest --folder \"FOLDER\" --pythonpath \"FOLDER\"\n\nWithout ``--folder`` flag the file assumes it's placed into a folder full of\ntests presumably as ``__init__.py``. It makes a list of files, filters\neverything not starting with ``test_`` and ending with ``.py`` and runs each test.\n\nFlag ``--pythonpath`` appends a folder to the ``sys.path`` automatically,\ntherefore it's not necessary to include it in each test manually.\n\nErrors\n------\n\nIf there is an error of whatever kind that unittest recognizes as failure,\nKivyUnitTest will save the name of the test and its log. When the testing ends\nall error logs are put together into console divided by pretty headers with\ntest's name.\n\nWriting Unit Test for Kivy application\n--------------------------------------\n\nBasic tests\n~~~~~~~~~~~\n\nWhen the Kivy application starts, it creates a loop and until the loop is\nthere, nothing will execute after ``App().run()`` line. That's why we need to\nprobe the loop.\n\nThis can be achieved by a simple ``time.sleep()`` as you've surely noticed\nsooner when trying to pause the app for a while. That's exactly what a custom\nunittest for Kivy does - pauses the main loop as much as possible\nas a scheduled interval and executes the testing ``run_test`` function.\n\nExample:\n\n.. code::\n\n    import unittest\n\n    import os\n    import sys\n    import time\n    import os.path as op\n    from functools import partial\n    from kivy.clock import Clock\n\nFirst we need to set up importing of the application set ``main_path``\nto be the folder of ``main.py`` e.g. when you have tests in\n``\u003capp dir\u003e/tests/test_example.py``. Or choose the folder with the\n``--pythonpath`` flag.\n\n.. code::\n\n    main_path = op.dirname(op.dirname(op.abspath(__file__)))\n    sys.path.append(main_path)\n\nImport your main class that inherits from App (``class My(App):``) or even\nadditional stuff that's not connected with App class or its children.\n\n.. code::\n\n    from main import My\n\n\n    class Test(unittest.TestCase):\n        # sleep function that catches ``dt`` from Clock\n        def pause(*args):\n            time.sleep(0.000001)\n\n        # main test function\n        def run_test(self, app, *args):\n            Clock.schedule_interval(self.pause, 0.000001)\n\n            # Do something\n\n            # Comment out if you are editing the test, it'll leave the\n            # Window opened.\n            app.stop()\n\nCreate an instance of your application, put it as a parameter into partial\n(so that you could access it later), schedule main function with Clock and\nlaunch the application (working Window will appear).\n\n.. code::\n\n        # same named function as the filename(!)\n        def test_example(self):\n            app = My()\n            p = partial(self.run_test, app)\n            Clock.schedule_once(p, 0.000001)\n            app.run()\n\n    if __name__ == '__main__':\n        unittest.main()\n\nIntermediate tests\n~~~~~~~~~~~~~~~~~~\n\nThis kind of tests is used directly for testing in the Kivy core and might\nnot be easy and/or suitable enough for your needs, however it brings a way\nextended control over the testing environment. In this test you can render\neach frame manually, move ``Clock`` each tick on your own and dispatch raw\ninput from (mocked) providers through ``MotionEvent``.\n\nThere is a class with all necessary stuff prepared in the background, so\nthat it launches a Kivy window, but waits for you to move it further e.g.\nif you decide to ``render()`` a widget.\n\n.. code::\n\n    from kivy.tests.common import GraphicUnitTest\n\n    from kivy.input.motionevent import MotionEvent\n    from kivy.graphics import Color, Point\n    from kivy.uix.widget import Widget\n    from kivy.base import EventLoop\n    from math import sqrt\n\nAfter you import ``MotionEvent``, you can create own class that inherits\nfrom it and use it later as a mocked input. We will use ``sx`` and ``sy``\nwhich are just positions on X and Y axis in 0 - 1 range (percents, if\nyou will). This class will dispatch a ``touch``.\n\n.. code::\n\n    class UTMotionEvent(MotionEvent):\n        def depack(self, args):\n            self.is_touch = True\n            self.sx = args['sx']\n            self.sy = args['sy']\n            self.profile = ['pos']\n            super(UTMotionEvent, self).depack(args)\n\nIf we know how to assemble a class to create a touch input, we might draw\nsomething with it as well. Kivy includes a nice demo, Touchtracer, for\nshowcasing multitouch. We fetch ``calculate_points`` from that example.\nIt basically returns a new set of points we'll input to a drawing function.\n\n.. code::\n\n    # taken from Kivy's Touchtracer\n    def calculate_points(x1, y1, x2, y2):\n        dx = x2 - x1\n        dy = y2 - y1\n        dist = sqrt(dx * dx + dy * dy)\n        o = []\n        m = dist\n        for i in range(1, int(m)):\n            mi = i / m\n            o.extend([\n                x1 + dx * mi,\n                y1 + dy * mi\n            ])\n        return o\n\nFor drawing we'll use a very similar thing to the one used in the Touchtracer.\nLet's draw a ``Point`` on ``on_touch_down`` event. Then, if we move the touch\nappend new points along the line between an old and a new point and draw them.\n\n.. code::\n\n    # core taken from Kivy's Touchtracer\n    class WidgetCanvasDraw(Widget):\n        def on_touch_down(self, touch):\n            win = self.get_parent_window()\n            ud = touch.ud\n\n            with self.canvas:\n                Color(1, 0, 0, 1)\n                ud['lines'] = Point(points=(\n                    touch.x, touch.y\n                ))\n\n            touch.grab(self)\n            return True\n\n        def on_touch_move(self, touch):\n            if touch.grab_current is not self:\n                return\n            ud = touch.ud\n\n            points = ud['lines'].points\n            oldx, oldy = points[-2], points[-1]\n\n            points = calculate_points(oldx, oldy, touch.x, touch.y)\n\n            if not points:\n                return\n\n            add_point = ud['lines'].add_point\n            for idx in range(0, len(points), 2):\n                add_point(\n                    points[idx],\n                    points[idx + 1]\n                )\n\n        def on_touch_up(self, touch):\n            if touch.grab_current is not self:\n                return\n            touch.ungrab(self)\n\nWe have input, drawing behavior, let's set up a test. You might want to\nget used to this \"template\" if you intend to use the ``GraphicsUnitTest``\nclass. It's not that scary though. Set a class attribute ``framecount``\nto zero, prepare some debugging behavior (``setUp`` prepares a new Window,\n``tearDown`` purges it). After overriding them with empty functions, such\nactions won't happen.\n\n.. code::\n\n    class WidgetDrawTestCase(GraphicUnitTest):\n        framecount = 0\n\n        # debug test with / stop destroying window\n        # def tearDown(self, *_): pass\n        # def setUp(self, *_): pass\n\nWe make sure the Window is available to us with ``EventLoop``, prepare\nall out widgets and then call ``EventLoop.idle()`` which makes a lot of\ninternals ready for an application to show like you are used to it. More\nor less.\n\n.. code::\n\n        def test_touch_draw(self):\n            # get Window instance for creating visible\n            # widget tree and for calculating coordinates\n            EventLoop.ensure_window()\n            win = EventLoop.window\n\n            # add widget for testing\n            child = WidgetCanvasDraw()\n            win.add_widget(child)\n\n            # get widgets ready\n            EventLoop.idle()\n\nYou can happily start testing now.\n\nThe little bit problematic part comes now, because you have to be sure\nwhere you want your touch to go and do it in 0 - 1 range, so that the\ntest works even after Window resizing. Absolute values are not the way\nyou want to go. Always try to generalise the movement and find a way\nhow to simplify them into a small list.\n\n.. code::\n\n            # default \"cursor\" position in the middle\n            pos = [win.width / 2.0, win.height / 2.0]\n\n            # default pos, new pos\n            points = [\n                [pos[0] - 5, pos[1], pos[0] + 5, pos[1]],\n                [pos[0], pos[1] - 5, pos[0], pos[1] + 5]\n            ]\n\n            # general behavior for touch+move+release\n            for i, point in enumerate(points):\n                x, y, nx, ny = point\n\n                # create custom MotionEvent (touch) instance\n                touch = UTMotionEvent(\"unittest\", 1, {\n                    \"sx\": x / float(win.width),\n                    \"sy\": y / float(win.height),\n                })\n\nThe points and touch are ready. Let's dispatch the input in the test.\nFor that we use ``EventLoop`` again and its method\n``post_dispatch_input(event_type, motion_event)``.\n\n* touch down with ``begin`` event type\n* touch move with ``update`` event type\n* touch up with ``end`` event type\n\n.. code::\n\n                # dispatch the MotionEvent in EventLoop as\n                # touch/press/click, see Profiles for more info:\n                # https://kivy.org/docs/api-kivy.input.motionevent.html#profiles\n                EventLoop.post_dispatch_input(\"begin\", touch)\n\n                # the touch is dispatched and has ud['lines']\n                # available from on_touch_down\n                self.assertIn('lines', touch.ud)\n                self.assertTrue(isinstance(touch.ud['lines'], Point))\n\n                # move touch from current to the new position\n                touch.move({\n                    \"sx\": nx / float(win.width),\n                    \"sy\": ny / float(win.height)\n                })\n                # update the MotionEvent in EventLoop\n                EventLoop.post_dispatch_input(\"update\", touch)\n\n                # release the MotionEvent in EventLoop\n                EventLoop.post_dispatch_input(\"end\", touch)\n\n                # still available, but released\n                self.assertIn('lines', touch.ud)\n                self.assertTrue(isinstance(touch.ud['lines'], Point))\n\n                expected_points = [[\n                    x + 0, y, x + 1, y,\n                    x + 2, y, x + 3, y,\n                    x + 4, y, x + 5, y,\n                    x + 6, y, x + 7, y,\n                    x + 8, y, x + 9, y\n                ], [\n                    x, y + 0, x, y + 1,\n                    x, y + 2, x, y + 3,\n                    x, y + 4, x, y + 5,\n                    x, y + 6, x, y + 7,\n                    x, y + 8, x, y + 9\n                ]]\n\n                # check if the instruction points == expected ones\n                self.assertEqual(\n                    touch.ud['lines'].points,\n                    expected_points[i]\n                )\n\nThe less obvious part comes now, because we need to trigger the rendering\nof our graphics in the application. Fortunately that's easy to do with\nsimple ``GraphicUnitTest.render()``. You most likely want to put there the\nroot widget like when building an application with ``App.build()`` method.\n\n.. code::\n\n            # render the graphics\n            self.render(child)\n\nIt's quite useful to add ``unittest.main()`` at the end of your test,\nbecause if you only try to write a single test then you most likely don't\nwant to run the whole suite. Especially if the suite is large.\n\n.. code::\n\n    if __name__ == '__main__':\n        import unittest\n        unittest.main()\n\n\nTips for testing\n~~~~~~~~~~~~~~~~\n\nHandle class communication through App class via ``App.get_running_app()`` in\nyour application, put every needed widget inside App class like this:\n\n.. code::\n\n    class MyButton(Button):\n        def __init__(self, **kwargs):\n            super(\u003cclass name\u003e, self).__init__(**kwargs)\n            self.text = 'Hello Test'\n            app = App.get_running_app()\n            app.my_button = self\n\nand then access your widgets in test's ``run_test()`` function via ``app``\nparameter like this:\n\n.. code::\n\n    self.assertEqual('Hello Test', app.my_button.text)\n\nUse ``app.root`` to get instance of a class you pass in the ``build()``\nfunction in the App class.\n\nDispatch events through widgets e.g. ``\u003cwidget\u003e.dispatch('on_release')`` to\nexecute function bound to ``on_release``.\n\n.. |rec| replace:: Recorder module\n.. _rec: https://kivy.org/docs/api-kivy.input.recorder.html\n.. |ins| replace:: Inspector module\n.. _ins: https://kivy.org/docs/api-kivy.modules.inspector.html\n\nUse Kivy's |ins|_ as help to navigate down the path of App class and use ``ids``\nin ``kv language``, it'll make targeting a specific widget easier.\n\nTry even Kivy's |rec|_ to record steps and play them later instead of\ndispatching events manually. However, this way is heavy time-consuming as it\nplays the steps exactly as long as they were recorded.\n\nExample:\n\n.. code::\n\n    from kivy.input.recorder import Recorder\n\n    # place this inside ``run_test()``\n    rec = Recorder(filename='myrecorder.kvi')\n    rec.bind(on_stop=\u003cfunction\u003e)\n    rec.play = True\n\nThis will play all steps and then executes a function bound to ``on_stop``.\nMay be useful for testing touch gestures, swipes, dragging and other rather\nannoying to write manually stuff.\n\nThere's also possibility to change time the steps were recorded in in ``.kvi``\nfile (that long number), which will speed things up.\n\nAlso, there's a very interesting Python package made by Mathieu Virbel that\nallows you to go down the widget tree rabit hole in a more sane way than\nusing this:\n\n.. code::\n\n    my_widget.children[0].children[1].children[2]...\n\nwhich gets tedious and annoying the more you use it when you navigate the\ntree from the application's root widget itself through complex layouts.\nThis is where `Telenium \u003chttps://github.com/tito/telenium\u003e`_ might save you\na lot of minutes instead of typing the same thing over and over.\n\nLicense\n-------\n\nThe MIT License (MIT)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkeyweeusr%2Fkivyunittest","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkeyweeusr%2Fkivyunittest","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkeyweeusr%2Fkivyunittest/lists"}