# 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.