378 lines
12 KiB
Markdown
378 lines
12 KiB
Markdown
|
|
# Getting started with Mobly
|
|||
|
|
|
|||
|
|
This tutorial shows how to write and execute simple Mobly test cases. We are
|
|||
|
|
using Android devices here since they are pretty accessible. Mobly supports
|
|||
|
|
various devices and you can also use your own custom hardware/equipment.
|
|||
|
|
|
|||
|
|
## Setup Requirements
|
|||
|
|
|
|||
|
|
* A computer with at least 2 USB ports.
|
|||
|
|
* Mobly package and its system dependencies installed on the computer.
|
|||
|
|
* One or two Android devices with the [Mobly Bundled Snippets](
|
|||
|
|
https://github.com/google/mobly-bundled-snippets) (MBS) installed. We will
|
|||
|
|
use MBS to trigger actions on the Android devices.
|
|||
|
|
* A working adb setup. To check, connect one Android device to the computer
|
|||
|
|
and make sure it has "USB debugging" enabled. Make sure the device shows up
|
|||
|
|
in the list printed by `adb devices`.
|
|||
|
|
|
|||
|
|
## Example 1: Hello World!
|
|||
|
|
|
|||
|
|
Let's start with the simple example of posting "Hello World" on the Android
|
|||
|
|
device's screen. Create the following files:
|
|||
|
|
|
|||
|
|
**sample_config.yml**
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
TestBeds:
|
|||
|
|
# A test bed where adb will find Android devices.
|
|||
|
|
- Name: SampleTestBed
|
|||
|
|
Controllers:
|
|||
|
|
AndroidDevice: '*'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**hello_world_test.py**
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from mobly import base_test
|
|||
|
|
from mobly import test_runner
|
|||
|
|
from mobly.controllers import android_device
|
|||
|
|
|
|||
|
|
class HelloWorldTest(base_test.BaseTestClass):
|
|||
|
|
|
|||
|
|
def setup_class(self):
|
|||
|
|
# Registering android_device controller module declares the test's
|
|||
|
|
# dependency on Android device hardware. By default, we expect at least one
|
|||
|
|
# object is created from this.
|
|||
|
|
self.ads = self.register_controller(android_device)
|
|||
|
|
self.dut = self.ads[0]
|
|||
|
|
# Start Mobly Bundled Snippets (MBS).
|
|||
|
|
self.dut.load_snippet('mbs', android_device.MBS_PACKAGE)
|
|||
|
|
|
|||
|
|
def test_hello(self):
|
|||
|
|
self.dut.mbs.makeToast('Hello World!')
|
|||
|
|
|
|||
|
|
if __name__ == '__main__':
|
|||
|
|
test_runner.main()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
To execute:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
$ python hello_world_test.py -c sample_config.yml
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
*Expect*:
|
|||
|
|
|
|||
|
|
A "Hello World!" toast notification appears on your device's screen.
|
|||
|
|
|
|||
|
|
Within SampleTestBed's `Controllers` section, we used `AndroidDevice: '*'` to tell
|
|||
|
|
the test runner to automatically find all connected Android devices. You can also
|
|||
|
|
specify particular devices by serial number and attach extra attributes to the object:
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
AndroidDevice:
|
|||
|
|
- serial: xyz
|
|||
|
|
phone_number: 123456
|
|||
|
|
- serial: abc
|
|||
|
|
label: golden_device
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Example 2: Invoking specific test case
|
|||
|
|
|
|||
|
|
We have multiple tests written in a test script, and we only want to execute
|
|||
|
|
a subset of them.
|
|||
|
|
|
|||
|
|
**hello_world_test.py**
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from mobly import base_test
|
|||
|
|
from mobly import test_runner
|
|||
|
|
from mobly.controllers import android_device
|
|||
|
|
|
|||
|
|
class HelloWorldTest(base_test.BaseTestClass):
|
|||
|
|
|
|||
|
|
def setup_class(self):
|
|||
|
|
self.ads = self.register_controller(android_device)
|
|||
|
|
self.dut = self.ads[0]
|
|||
|
|
self.dut.load_snippet('mbs', android_device.MBS_PACKAGE)
|
|||
|
|
|
|||
|
|
def test_hello(self):
|
|||
|
|
self.dut.mbs.makeToast('Hello World!')
|
|||
|
|
|
|||
|
|
def test_bye(self):
|
|||
|
|
self.dut.mbs.makeToast('Goodbye!')
|
|||
|
|
|
|||
|
|
if __name__ == '__main__':
|
|||
|
|
test_runner.main()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
*To execute:*
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
$ python hello_world_test.py -c sample_config.yml --test_case test_bye
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
*Expect*:
|
|||
|
|
|
|||
|
|
A "Goodbye!" toast notification appears on your device's screen.
|
|||
|
|
|
|||
|
|
You can dictate what test cases to execute within a test script and their
|
|||
|
|
execution order, for example:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
$ python hello_world_test.py -c sample_config.yml --test_case test_bye test_hello test_bye
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
*Expect*:
|
|||
|
|
|
|||
|
|
Toast notifications appear on your device's screen in the following order:
|
|||
|
|
"Goodbye!", "Hello World!", "Goodbye!".
|
|||
|
|
|
|||
|
|
## Example 3: User parameters
|
|||
|
|
|
|||
|
|
You could specify user parameters to be passed into your test class in the
|
|||
|
|
config file.
|
|||
|
|
|
|||
|
|
In the following config, we added a parameter `favorite_food` to be used in the test case.
|
|||
|
|
|
|||
|
|
**sample_config.yml**
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
TestBeds:
|
|||
|
|
- Name: SampleTestBed
|
|||
|
|
Controllers:
|
|||
|
|
AndroidDevice: '*'
|
|||
|
|
TestParams:
|
|||
|
|
favorite_food: Green eggs and ham.
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
In the test script, you could access the user parameter:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def test_favorite_food(self):
|
|||
|
|
food = self.user_params.get('favorite_food')
|
|||
|
|
if food:
|
|||
|
|
self.dut.mbs.makeToast("I'd like to eat %s." % food)
|
|||
|
|
else:
|
|||
|
|
self.dut.mbs.makeToast("I'm not hungry.")
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Example 4: Multiple Test Beds and Default Test Parameters
|
|||
|
|
|
|||
|
|
Multiple test beds can be configured in one configuration file.
|
|||
|
|
|
|||
|
|
**sample_config.yaml**
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
# DefaultParams is optional here. It uses yaml's anchor feature to easily share
|
|||
|
|
# a set of parameters between multiple test bed configs
|
|||
|
|
DefaultParams: &DefaultParams
|
|||
|
|
favorite_food: green eggs and ham.
|
|||
|
|
|
|||
|
|
TestBeds:
|
|||
|
|
- Name: XyzTestBed
|
|||
|
|
Controllers:
|
|||
|
|
AndroidDevice:
|
|||
|
|
- serial: xyz
|
|||
|
|
phone_number: 123456
|
|||
|
|
TestParams:
|
|||
|
|
<<: *DefaultParams
|
|||
|
|
- Name: AbcTestBed
|
|||
|
|
Controllers:
|
|||
|
|
AndroidDevice:
|
|||
|
|
- serial: abc
|
|||
|
|
label: golden_device
|
|||
|
|
TestParams:
|
|||
|
|
<<: *DefaultParams
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
You can choose which one to execute on with the command line argument
|
|||
|
|
`--test_bed`:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
$ python hello_world_test.py -c sample_config.yml --test_bed AbcTestBed
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
*Expect*:
|
|||
|
|
|
|||
|
|
A "Hello World!" and a "Goodbye!" toast notification appear on your device's
|
|||
|
|
screen.
|
|||
|
|
|
|||
|
|
|
|||
|
|
## Example 5: Test with Multiple Android devices
|
|||
|
|
|
|||
|
|
In this example, we use one Android device to discover another Android device
|
|||
|
|
via bluetooth. This test demonstrates several essential elements in test
|
|||
|
|
writing, like asserts, device debug tag, and general logging vs logging with device tag.
|
|||
|
|
|
|||
|
|
**sample_config.yml**
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
TestBeds:
|
|||
|
|
- Name: TwoDeviceTestBed
|
|||
|
|
Controllers:
|
|||
|
|
AndroidDevice:
|
|||
|
|
- serial: xyz
|
|||
|
|
label: target
|
|||
|
|
- serial: abc
|
|||
|
|
label: discoverer
|
|||
|
|
TestParams:
|
|||
|
|
bluetooth_name: MagicBluetooth
|
|||
|
|
bluetooth_timeout: 5
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**sample_test.py**
|
|||
|
|
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
import logging
|
|||
|
|
import pprint
|
|||
|
|
|
|||
|
|
from mobly import asserts
|
|||
|
|
from mobly import base_test
|
|||
|
|
from mobly import test_runner
|
|||
|
|
from mobly.controllers import android_device
|
|||
|
|
|
|||
|
|
# Number of seconds for the target to stay discoverable on Bluetooth.
|
|||
|
|
DISCOVERABLE_TIME = 60
|
|||
|
|
|
|||
|
|
|
|||
|
|
class HelloWorldTest(base_test.BaseTestClass):
|
|||
|
|
def setup_class(self):
|
|||
|
|
# Registering android_device controller module, and declaring that the test
|
|||
|
|
# requires at least two Android devices.
|
|||
|
|
self.ads = self.register_controller(android_device, min_number=2)
|
|||
|
|
# The device used to discover Bluetooth devices.
|
|||
|
|
self.discoverer = android_device.get_device(
|
|||
|
|
self.ads, label='discoverer')
|
|||
|
|
# Sets the tag that represents this device in logs.
|
|||
|
|
self.discoverer.debug_tag = 'discoverer'
|
|||
|
|
# The device that is expected to be discovered
|
|||
|
|
self.target = android_device.get_device(self.ads, label='target')
|
|||
|
|
self.target.debug_tag = 'target'
|
|||
|
|
self.target.load_snippet('mbs', android_device.MBS_PACKAGE)
|
|||
|
|
self.discoverer.load_snippet('mbs', android_device.MBS_PACKAGE)
|
|||
|
|
|
|||
|
|
def setup_test(self):
|
|||
|
|
# Make sure bluetooth is on.
|
|||
|
|
self.target.mbs.btEnable()
|
|||
|
|
self.discoverer.mbs.btEnable()
|
|||
|
|
# Set Bluetooth name on target device.
|
|||
|
|
self.target.mbs.btSetName('LookForMe!')
|
|||
|
|
|
|||
|
|
def test_bluetooth_discovery(self):
|
|||
|
|
target_name = self.target.mbs.btGetName()
|
|||
|
|
self.target.log.info('Become discoverable with name "%s" for %ds.',
|
|||
|
|
target_name, DISCOVERABLE_TIME)
|
|||
|
|
self.target.mbs.btBecomeDiscoverable(DISCOVERABLE_TIME)
|
|||
|
|
self.discoverer.log.info('Looking for Bluetooth devices.')
|
|||
|
|
discovered_devices = self.discoverer.mbs.btDiscoverAndGetResults()
|
|||
|
|
self.discoverer.log.debug('Found Bluetooth devices: %s',
|
|||
|
|
pprint.pformat(discovered_devices, indent=2))
|
|||
|
|
discovered_names = [device['Name'] for device in discovered_devices]
|
|||
|
|
logging.info('Verifying the target is discovered by the discoverer.')
|
|||
|
|
asserts.assert_true(
|
|||
|
|
target_name in discovered_names,
|
|||
|
|
'Failed to discover the target device %s over Bluetooth.' %
|
|||
|
|
target_name)
|
|||
|
|
|
|||
|
|
def teardown_test(self):
|
|||
|
|
# Turn Bluetooth off on both devices after test finishes.
|
|||
|
|
self.target.mbs.btDisable()
|
|||
|
|
self.discoverer.mbs.btDisable()
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == '__main__':
|
|||
|
|
test_runner.main()
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
There's potentially a lot more we could do in this test, e.g. check
|
|||
|
|
the hardware address, see whether we can pair devices, transfer files, etc.
|
|||
|
|
|
|||
|
|
To learn more about the features included in MBS, go to [MBS repo](
|
|||
|
|
https://github.com/google/mobly-bundled-snippets) to see how to check its help
|
|||
|
|
menu.
|
|||
|
|
|
|||
|
|
To learn more about Mobly Snippet Lib, including features like Espresso support
|
|||
|
|
and asynchronous calls, see the [snippet lib examples](
|
|||
|
|
https://github.com/google/mobly-snippet-lib/tree/master/examples).
|
|||
|
|
|
|||
|
|
|
|||
|
|
## Example 6: Generated Tests
|
|||
|
|
|
|||
|
|
A common use case in writing tests is to execute the same test logic multiple
|
|||
|
|
times, each time with a different set of parameters. Instead of duplicating the
|
|||
|
|
same test case with minor tweaks, you could use the **Generated tests** in
|
|||
|
|
Mobly.
|
|||
|
|
|
|||
|
|
Mobly could generate test cases for you based on a list of parameters and a
|
|||
|
|
function that contains the test logic. Each generated test case is equivalent
|
|||
|
|
to an actual test case written in the class in terms of execution, procedure
|
|||
|
|
functions (setup/teardown/on_fail), and result collection. You could also
|
|||
|
|
select generated test cases via the `--test_case` cli arg as well.
|
|||
|
|
|
|||
|
|
|
|||
|
|
Here's an example of generated tests in action. We will reuse the "Example 1:
|
|||
|
|
Hello World!". Instead of making one toast of "Hello World", we will generate
|
|||
|
|
several test cases and toast a different message in each one of them.
|
|||
|
|
|
|||
|
|
You could reuse the config file from Example 1.
|
|||
|
|
|
|||
|
|
The test class would look like:
|
|||
|
|
|
|||
|
|
|
|||
|
|
**many_greetings_test.py**
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from mobly import base_test
|
|||
|
|
from mobly import test_runner
|
|||
|
|
from mobly.controllers import android_device
|
|||
|
|
|
|||
|
|
|
|||
|
|
class ManyGreetingsTest(base_test.BaseTestClass):
|
|||
|
|
|
|||
|
|
# When a test run starts, Mobly calls this function to figure out what
|
|||
|
|
# tests need to be generated. So you need to specify what tests to generate
|
|||
|
|
# in this function.
|
|||
|
|
def setup_generated_tests(self):
|
|||
|
|
messages = [('Hello', 'World'), ('Aloha', 'Obama'),
|
|||
|
|
('konichiwa', 'Satoshi')]
|
|||
|
|
# Call `generate_tests` function to specify the tests to generate. This
|
|||
|
|
# function can only be called within `setup_generated_tests`. You could
|
|||
|
|
# call this function multiple times to generate multiple groups of
|
|||
|
|
# tests.
|
|||
|
|
self.generate_tests(
|
|||
|
|
# Specify the function that has the common logic shared by these
|
|||
|
|
# generated tests.
|
|||
|
|
test_logic=self.make_toast_logic,
|
|||
|
|
# Specify a function that creates the name of each test.
|
|||
|
|
name_func=self.make_toast_name_function,
|
|||
|
|
# A list of tuples, where each tuple is a set of arguments to be
|
|||
|
|
# passed to the test logic and name function.
|
|||
|
|
arg_sets=messages)
|
|||
|
|
|
|||
|
|
def setup_class(self):
|
|||
|
|
self.ads = self.register_controller(android_device)
|
|||
|
|
self.dut = self.ads[0]
|
|||
|
|
self.dut.load_snippet('mbs', android_device.MBS_PACKAGE)
|
|||
|
|
|
|||
|
|
# The common logic shared by a group of generated tests.
|
|||
|
|
def make_toast_logic(self, greeting, name):
|
|||
|
|
self.dut.mbs.makeToast('%s, %s!' % (greeting, name))
|
|||
|
|
|
|||
|
|
# The function that generates the names of each test case based on each
|
|||
|
|
# argument set. The name function should have the same signature as the
|
|||
|
|
# actual test logic function.
|
|||
|
|
def make_toast_name_function(self, greeting, name):
|
|||
|
|
return 'test_greeting_say_%s_to_%s' % (greeting, name)
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == '__main__':
|
|||
|
|
test_runner.main()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Three test cases will be executed even though we did not "physically" define
|
|||
|
|
any "test_xx" function in the test class.
|