├── .gitignore ├── README.rst ├── doc └── img │ ├── chat_on_slack.png │ ├── discussion-qq-qr.png │ └── logo-simple-my-testflow.png ├── requirements.txt ├── res ├── app │ └── com.google.android.calculator.apk └── img │ ├── keyboard-digits-landscape.png │ └── keyboard-digits.png ├── setup.py └── testflow ├── __init__.py ├── lib ├── __init__.py ├── case │ ├── __init__.py │ └── android_app.py └── utils │ ├── __init__.py │ └── installation.py └── scripts ├── __init__.py ├── example.py ├── multitests_one_by_one.py └── multitests_together.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # Packages 4 | *.egg 5 | *.egg-info 6 | dist 7 | build 8 | eggs 9 | parts 10 | bin 11 | var 12 | sdist 13 | develop-eggs 14 | .installed.cfg 15 | lib64 16 | __pycache__ 17 | 18 | # Installer logs 19 | pip-log.txt 20 | 21 | # Unit test / coverage reports 22 | .coverage 23 | .tox 24 | nosetests.xml 25 | 26 | # Translations 27 | *.mo 28 | 29 | # Mr Developer 30 | .mr.developer.cfg 31 | .project 32 | .pydevproject 33 | .vs/ 34 | tmp/ 35 | *.log 36 | _site 37 | apps 38 | _build/ 39 | *.spec 40 | htmlcov/ 41 | cover/ 42 | .idea/ 43 | .DS_Store 44 | 45 | # test results 46 | log/ 47 | pocounit-results/ 48 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | .. image:: doc/img/logo-simple-my-testflow.png 3 | 4 | my-testflow 5 | =========== 6 | 7 | **Test automation standard engineering project template**. More and more features to be added in future. 8 | 9 | How to use? 10 | ----------- 11 | 12 | **1.** Clone this repo and run the following command. (Installing with ``-e`` flag is strongly recommended.) 13 | 14 | .. code-block:: bash 15 | 16 | git clone https://github.com/AirtestProject/my-testflow.git 17 | pip install -e my-testflow 18 | 19 | **2.** Run the example script after installation. 20 | 21 | .. code-block:: bash 22 | 23 | python testflow/scripts/example.py 24 | 25 | **3.** Check the test results in ``pocounit-results/`` with `TestResultPlayer`_. 26 | 27 | Project structure 28 | ----------------- 29 | 30 | .. code-block:: text 31 | 32 | ─ my-testflow/ 33 | ├─ testflow/ <------- rename this folder if you with (identifier only) 34 | | ├─ __init__.py 35 | | ├─ lib/ 36 | | | ├─ __init__.py 37 | | | └─ ... 38 | | └─ scripts/ 39 | | ├─ __init__.py 40 | | ├─ example.py <------- you can try running this script 41 | | └─ ... 42 | ├─ res/ <------- store any resource files 43 | | ├─ app/ 44 | | └─ img/ 45 | ├─ pocounit-results/ <------- test results will auto generated here 46 | ├─ setup.py 47 | ├─ requirements.txt 48 | └─ .gitignore 49 | 50 | 51 | How to get the test results? 52 | ---------------------------- 53 | 54 | Use our `TestResultPlayer`_ to replay the whole procedure of your tests! 55 | 56 | More info 57 | --------- 58 | 59 | This template is designed for engineering test flow and it works along with following frameworks. 60 | You can take a look at the API reference according to each framework. 61 | 62 | - `airtest`_ 63 | - `poco`_ 64 | - `pocounit`_ 65 | 66 | `see also `_ 67 | 68 | 69 | .. _TestResultPlayer: http://poco.readthedocs.io/en/latest/source/doc/about-test-result-player.html 70 | .. _airtest: http://airtest.readthedocs.io 71 | .. _poco: http://poco.readthedocs.io 72 | .. _pocounit: https://github.com/AirtestProject/PocoUnit 73 | 74 | Join to discuss! 75 | ---------------- 76 | 77 | |chat on slack| 78 | 79 | .. image:: doc/img/discussion-qq-qr.png 80 | 81 | .. |chat on slack| image:: doc/img/chat_on_slack.png 82 | :alt: chat on slack 83 | :scale: 100% 84 | :target: https://join.slack.com/t/airtestproject/shared_invite/enQtMzYwMjc2NjQzNDkzLTcyMmJlNjgyNjgzZTRkNWRiYmE1YWI1ZWE5ZmQwYmM1YmY3ODZlMDc0YjkwMTQ5NDYxYmEyZWU1ZTFlZjg3ZjI -------------------------------------------------------------------------------- /doc/img/chat_on_slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirtestProject/my-testflow/43bfe0a1416f8fa8f0355f36a111c5decf777741/doc/img/chat_on_slack.png -------------------------------------------------------------------------------- /doc/img/discussion-qq-qr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirtestProject/my-testflow/43bfe0a1416f8fa8f0355f36a111c5decf777741/doc/img/discussion-qq-qr.png -------------------------------------------------------------------------------- /doc/img/logo-simple-my-testflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirtestProject/my-testflow/43bfe0a1416f8fa8f0355f36a111c5decf777741/doc/img/logo-simple-my-testflow.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pocoui 2 | pocounit -------------------------------------------------------------------------------- /res/app/com.google.android.calculator.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirtestProject/my-testflow/43bfe0a1416f8fa8f0355f36a111c5decf777741/res/app/com.google.android.calculator.apk -------------------------------------------------------------------------------- /res/img/keyboard-digits-landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirtestProject/my-testflow/43bfe0a1416f8fa8f0355f36a111c5decf777741/res/img/keyboard-digits-landscape.png -------------------------------------------------------------------------------- /res/img/keyboard-digits.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirtestProject/my-testflow/43bfe0a1416f8fa8f0355f36a111c5decf777741/res/img/keyboard-digits.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import os 4 | from setuptools import setup, find_packages 5 | 6 | 7 | def parse_requirements(filename='requirements.txt'): 8 | """ load requirements from a pip requirements file. (replacing from pip.req import parse_requirements)""" 9 | lineiter = (line.strip() for line in open(filename)) 10 | return [line for line in lineiter if line and not line.startswith("#")] 11 | 12 | 13 | project_name = find_packages()[0] 14 | if '.' in project_name: 15 | project_name = project_name.split('.', 1)[0] 16 | 17 | if os.path.exists('requirements.txt'): 18 | reqs = parse_requirements() 19 | else: 20 | reqs = [] 21 | 22 | setup( 23 | name='testflow_' + project_name, 24 | version='1.0.2', 25 | description='A test automation project using poco and pocounit.', 26 | packages=find_packages(), 27 | include_package_data=True, 28 | install_requires=reqs, 29 | ) 30 | -------------------------------------------------------------------------------- /testflow/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirtestProject/my-testflow/43bfe0a1416f8fa8f0355f36a111c5decf777741/testflow/__init__.py -------------------------------------------------------------------------------- /testflow/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirtestProject/my-testflow/43bfe0a1416f8fa8f0355f36a111c5decf777741/testflow/lib/__init__.py -------------------------------------------------------------------------------- /testflow/lib/case/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirtestProject/my-testflow/43bfe0a1416f8fa8f0355f36a111c5decf777741/testflow/lib/case/__init__.py -------------------------------------------------------------------------------- /testflow/lib/case/android_app.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | from pocounit.case import PocoTestCase 4 | from pocounit.addons.poco.action_tracking import ActionTracker 5 | from pocounit.addons.poco.capturing import SiteCaptor 6 | from poco.drivers.android.uiautomation import AndroidUiautomationPoco 7 | 8 | from airtest.core.api import connect_device, device as current_device 9 | from airtest.core.helper import device_platform 10 | 11 | 12 | class AndroidAppCase(PocoTestCase): 13 | @classmethod 14 | def setUpClass(cls): 15 | super(AndroidAppCase, cls).setUpClass() 16 | if not current_device(): 17 | connect_device('Android:///') 18 | 19 | dev = current_device() 20 | meta_info_emitter = cls.get_result_emitter('metaInfo') 21 | if device_platform() == 'Android': 22 | meta_info_emitter.snapshot_device_info(dev.serialno, dev.adb.get_device_info()) 23 | 24 | cls.poco = AndroidUiautomationPoco(screenshot_each_action=False) 25 | 26 | action_tracker = ActionTracker(cls.poco) 27 | cls.register_addon(action_tracker) 28 | cls.site_capturer = SiteCaptor(cls.poco) 29 | cls.register_addon(cls.site_capturer) 30 | -------------------------------------------------------------------------------- /testflow/lib/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirtestProject/my-testflow/43bfe0a1416f8fa8f0355f36a111c5decf777741/testflow/lib/utils/__init__.py -------------------------------------------------------------------------------- /testflow/lib/utils/installation.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | __author__ = 'lnx3032' 3 | 4 | 5 | import re 6 | 7 | from airtest.utils.apkparser.apk import APK 8 | 9 | 10 | def install_android_app(adb_client, localpath, force_reinstall=False): 11 | apk_info = APK(localpath) 12 | package_name = apk_info.package 13 | 14 | def _get_installed_apk_version(package): 15 | package_info = adb_client.shell(['dumpsys', 'package', package]) 16 | matcher = re.search(r'versionCode=(\d+)', package_info) 17 | if matcher: 18 | return int(matcher.group(1)) 19 | return None 20 | 21 | try: 22 | apk_version = int(apk_info.androidversion_code) 23 | except (RuntimeError, ValueError): 24 | apk_version = 0 25 | installed_version = _get_installed_apk_version(package_name) 26 | print('installed version is {}, installer version is {}. force_reinstall={}'.format(installed_version, apk_version, force_reinstall)) 27 | if installed_version is None or apk_version > installed_version or force_reinstall: 28 | if installed_version is not None: 29 | force_reinstall = True 30 | if hasattr(adb_client, 'install_app'): 31 | adb_client.install_app(localpath, force_reinstall) 32 | else: 33 | adb_client.install(localpath, force_reinstall) 34 | return True 35 | return False 36 | 37 | 38 | def uninstall_android_app(adb_client, package): 39 | if hasattr(adb_client, 'uninstall_app'): 40 | adb_client.uninstall_app(package) 41 | else: 42 | adb_client.uninstall(package) 43 | -------------------------------------------------------------------------------- /testflow/scripts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AirtestProject/my-testflow/43bfe0a1416f8fa8f0355f36a111c5decf777741/testflow/scripts/__init__.py -------------------------------------------------------------------------------- /testflow/scripts/example.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import time 4 | 5 | # TODO: "from testflow.lib" should be renamed according to your actual package name 6 | from testflow.lib.case.android_app import AndroidAppCase 7 | from testflow.lib.utils.installation import install_android_app 8 | from airtest.core.api import start_app, stop_app, Template, exists 9 | from airtest.core.api import device as current_device 10 | 11 | 12 | class CalculatorPlus(AndroidAppCase): 13 | @classmethod 14 | def name(cls): 15 | return 'CalculatorPlus (customized name)' 16 | 17 | @classmethod 18 | def getMetaInfo(cls): 19 | return { 20 | "author": 'author name', 21 | "remark": 'any data', 22 | 'any-keys': 'any-values', 23 | } 24 | 25 | def setUp(self): 26 | self.package_name = 'com.google.android.calculator' 27 | apk_path = self.R('res/app/com.google.android.calculator.apk') 28 | install_android_app(current_device().adb, apk_path) 29 | start_app(self.package_name) 30 | 31 | def runTest(self): 32 | keyboard = Template(self.R('res/img/keyboard-digits.png')) 33 | keyboard_landscape = Template(self.R('res/img/keyboard-digits-landscape.png')) 34 | self.assertTrue(exists(keyboard) or exists(keyboard_landscape), 'App started.') 35 | 36 | self.poco('com.google.android.calculator:id/digit_1').click() 37 | self.poco('com.google.android.calculator:id/op_add').click() 38 | self.poco('com.google.android.calculator:id/digit_1').click() 39 | self.poco('com.google.android.calculator:id/eq').click() 40 | time.sleep(1) 41 | result = self.poco('com.google.android.calculator:id/formula').get_text() 42 | self.site_capturer.snapshot('check-point-1') 43 | self.assertEqual(result, '2', '1+1=2 ^^') 44 | 45 | def tearDown(self): 46 | stop_app(self.package_name) 47 | 48 | 49 | if __name__ == '__main__': 50 | import pocounit 51 | pocounit.main() 52 | -------------------------------------------------------------------------------- /testflow/scripts/multitests_one_by_one.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import time 4 | 5 | # TODO: "from testflow.lib" should be renamed according to your actual package name 6 | from testflow.lib.case.android_app import AndroidAppCase 7 | from testflow.lib.utils.installation import install_android_app 8 | from airtest.core.api import start_app, stop_app 9 | from airtest.core.api import device as current_device 10 | 11 | 12 | class CalculatorCase(AndroidAppCase): 13 | @classmethod 14 | def setUpClass(cls): 15 | super(CalculatorCase, cls).setUpClass() 16 | cls.package_name = 'com.google.android.calculator' 17 | apk_path = cls.R('res/app/com.google.android.calculator.apk') 18 | install_android_app(current_device().adb, apk_path) 19 | start_app(cls.package_name) 20 | 21 | @classmethod 22 | def tearDownClass(cls): 23 | stop_app(cls.package_name) 24 | super(CalculatorCase, cls).tearDownClass() 25 | 26 | def setUp(self): 27 | # clear previous result 28 | clr = self.poco('com.google.android.calculator:id/clr') 29 | if clr.exists(): 30 | clr.click() 31 | 32 | 33 | class CalculatorPlus(CalculatorCase): 34 | def runTest(self): 35 | self.poco('com.google.android.calculator:id/digit_1').click() 36 | self.poco('com.google.android.calculator:id/op_add').click() 37 | self.poco('com.google.android.calculator:id/digit_1').click() 38 | self.poco('com.google.android.calculator:id/eq').click() 39 | time.sleep(1) 40 | result = self.poco('com.google.android.calculator:id/formula').get_text() 41 | self.assertEqual(result, '2', '1+1=2 ^^') 42 | 43 | 44 | class CalculatorMinus(CalculatorCase): 45 | def runTest(self): 46 | self.poco('com.google.android.calculator:id/digit_1').click() 47 | self.poco('com.google.android.calculator:id/op_sub').click() 48 | self.poco('com.google.android.calculator:id/digit_1').click() 49 | self.poco('com.google.android.calculator:id/eq').click() 50 | time.sleep(1) 51 | result = self.poco('com.google.android.calculator:id/formula').get_text() 52 | self.assertEqual(result, '0', '1-1=0 ^^') 53 | 54 | 55 | if __name__ == '__main__': 56 | import pocounit 57 | pocounit.main() 58 | -------------------------------------------------------------------------------- /testflow/scripts/multitests_together.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | import time 4 | 5 | # TODO: "from testflow.lib" should be renamed according to your actual package name 6 | from testflow.lib.case.android_app import AndroidAppCase 7 | from testflow.lib.utils.installation import install_android_app 8 | from pocounit.suite import PocoTestSuite 9 | from airtest.core.api import device as current_device, connect_device 10 | from airtest.core.api import start_app, stop_app 11 | 12 | 13 | class CalculatorSuite(PocoTestSuite): 14 | def setUp(self): 15 | if not current_device(): 16 | connect_device('Android:///') 17 | 18 | self.package_name = 'com.google.android.calculator' 19 | apk_path = self.R('res/app/com.google.android.calculator.apk') 20 | install_android_app(current_device().adb, apk_path) 21 | start_app(self.package_name) 22 | 23 | def tearDown(self): 24 | stop_app(self.package_name) 25 | 26 | 27 | class CalculatorCase(AndroidAppCase): 28 | def setUp(self): 29 | # clear previous result 30 | clr = self.poco('com.google.android.calculator:id/clr') 31 | if clr.exists(): 32 | clr.click() 33 | 34 | 35 | class CalculatorPlus(CalculatorCase): 36 | def runTest(self): 37 | self.poco('com.google.android.calculator:id/digit_1').click() 38 | self.poco('com.google.android.calculator:id/op_add').click() 39 | self.poco('com.google.android.calculator:id/digit_1').click() 40 | self.poco('com.google.android.calculator:id/eq').click() 41 | time.sleep(1) 42 | result = self.poco('com.google.android.calculator:id/formula').get_text() 43 | self.assertEqual(result, '2', '1+1=2 ^^') 44 | 45 | 46 | class CalculatorMinus(CalculatorCase): 47 | def runTest(self): 48 | self.poco('com.google.android.calculator:id/digit_1').click() 49 | self.poco('com.google.android.calculator:id/op_sub').click() 50 | self.poco('com.google.android.calculator:id/digit_1').click() 51 | self.poco('com.google.android.calculator:id/eq').click() 52 | time.sleep(1) 53 | result = self.poco('com.google.android.calculator:id/formula').get_text() 54 | self.assertEqual(result, '0', '1-1=0 ^^') 55 | 56 | 57 | if __name__ == '__main__': 58 | suite = CalculatorSuite([ 59 | CalculatorPlus(), 60 | CalculatorMinus(), 61 | ]) 62 | import pocounit 63 | pocounit.run(suite) 64 | --------------------------------------------------------------------------------