1# Getting started with Mobly
2
3This tutorial shows how to write and execute simple Mobly test cases. We are
4using Android devices here since they are pretty accessible. Mobly supports
5various devices and you can also use your own custom hardware/equipment.
6
7## Setup Requirements
8
9*   A computer with at least 2 USB ports.
10*   Mobly package and its system dependencies installed on the computer.
11*   One or two Android devices with the [Mobly Bundled Snippets](
12    https://github.com/google/mobly-bundled-snippets) (MBS) installed. We will
13    use MBS to trigger actions on the Android devices.
14*   A working adb setup. To check, connect one Android device to the computer
15    and make sure it has "USB debugging" enabled. Make sure the device shows up
16    in the list printed by `adb devices`.
17
18## Example 1: Hello World!
19 
20Let's start with the simple example of posting "Hello World" on the Android
21device's screen. Create the following files:
22 
23**sample_config.yml**
24 
25```yaml
26TestBeds:
27  # A test bed where adb will find Android devices.
28  - Name: SampleTestBed
29    Controllers:
30        AndroidDevice: '*'
31```
32 
33**hello_world_test.py**
34 
35```python
36from mobly import base_test
37from mobly import test_runner
38from mobly.controllers import android_device
39 
40class HelloWorldTest(base_test.BaseTestClass):
41 
42  def setup_class(self):
43    # Registering android_device controller module declares the test's
44    # dependency on Android device hardware. By default, we expect at least one
45    # object is created from this.
46    self.ads = self.register_controller(android_device)
47    self.dut = self.ads[0]
48    # Start Mobly Bundled Snippets (MBS).
49    self.dut.load_snippet('mbs', android_device.MBS_PACKAGE)
50 
51  def test_hello(self):
52    self.dut.mbs.makeToast('Hello World!')
53 
54if __name__ == '__main__':
55  test_runner.main()
56```
57 
58To execute:
59
60```
61$ python hello_world_test.py -c sample_config.yml
62```
63
64*Expect*:
65
66A "Hello World!" toast notification appears on your device's screen.
67 
68Within SampleTestBed's `Controllers` section, we used `AndroidDevice: '*'` to tell
69the test runner to automatically find all connected Android devices. You can also
70specify particular devices by serial number and attach extra attributes to the object:
71 
72```yaml
73AndroidDevice:
74  - serial: xyz
75    phone_number: 123456
76  - serial: abc
77    label: golden_device
78```
79 
80## Example 2: Invoking specific test case
81 
82We have multiple tests written in a test script, and we only want to execute
83a subset of them.
84 
85**hello_world_test.py**
86 
87```python
88from mobly import base_test
89from mobly import test_runner
90from mobly.controllers import android_device
91 
92class HelloWorldTest(base_test.BaseTestClass):
93 
94  def setup_class(self):
95    self.ads = self.register_controller(android_device)
96    self.dut = self.ads[0]
97    self.dut.load_snippet('mbs', android_device.MBS_PACKAGE)
98 
99  def test_hello(self):
100    self.dut.mbs.makeToast('Hello World!')
101 
102  def test_bye(self):
103    self.dut.mbs.makeToast('Goodbye!')
104 
105if __name__ == '__main__':
106  test_runner.main()
107```
108 
109*To execute:*
110
111```
112$ python hello_world_test.py -c sample_config.yml --test_case test_bye
113```
114 
115*Expect*:
116
117A "Goodbye!" toast notification appears on your device's screen.
118 
119You can dictate what test cases to execute within a test script and their
120execution order, for example:
121
122```
123$ python hello_world_test.py -c sample_config.yml --test_case test_bye test_hello test_bye
124```
125
126*Expect*:
127
128Toast notifications appear on your device's screen in the following order:
129"Goodbye!", "Hello World!", "Goodbye!".
130 
131## Example 3: User parameters
132 
133You could specify user parameters to be passed into your test class in the
134config file.
135 
136In the following config, we added a parameter `favorite_food` to be used in the test case.
137 
138**sample_config.yml**
139 
140```yaml
141TestBeds:
142  - Name: SampleTestBed
143    Controllers:
144        AndroidDevice: '*'
145    TestParams:
146        favorite_food: Green eggs and ham.
147```
148 
149In the test script, you could access the user parameter:
150 
151```python
152  def test_favorite_food(self):
153    food = self.user_params.get('favorite_food')
154    if food:
155      self.dut.mbs.makeToast("I'd like to eat %s." % food)
156    else:
157      self.dut.mbs.makeToast("I'm not hungry.")
158```
159 
160## Example 4: Multiple Test Beds and Default Test Parameters
161 
162Multiple test beds can be configured in one configuration file.
163 
164**sample_config.yaml**
165 
166```yaml
167# DefaultParams is optional here. It uses yaml's anchor feature to easily share
168# a set of parameters between multiple test bed configs
169DefaultParams: &DefaultParams
170    favorite_food: green eggs and ham.
171 
172TestBeds:
173  - Name: XyzTestBed
174    Controllers:
175        AndroidDevice:
176          - serial: xyz
177            phone_number: 123456
178    TestParams:
179        <<: *DefaultParams
180  - Name: AbcTestBed
181    Controllers:
182        AndroidDevice:
183          - serial: abc
184            label: golden_device
185    TestParams:
186        <<: *DefaultParams
187```
188 
189You can choose which one to execute on with the command line argument
190`--test_bed`:
191
192```
193$ python hello_world_test.py -c sample_config.yml --test_bed AbcTestBed
194```
195
196*Expect*:
197
198A "Hello World!" and a "Goodbye!" toast notification appear on your device's
199screen.
200 
201 
202## Example 5: Test with Multiple Android devices
203 
204In this example, we use one Android device to discover another Android device
205via bluetooth. This test demonstrates several essential elements in test
206writing, like asserts, device debug tag, and general logging vs logging with device tag.
207 
208**sample_config.yml**
209 
210```yaml
211TestBeds:
212  - Name: TwoDeviceTestBed
213    Controllers:
214        AndroidDevice:
215          - serial: xyz
216            label: target
217          - serial: abc
218            label: discoverer
219    TestParams:
220        bluetooth_name: MagicBluetooth
221        bluetooth_timeout: 5
222
223```
224 
225**sample_test.py**
226 
227 
228```python
229import logging
230import pprint
231
232from mobly import asserts
233from mobly import base_test
234from mobly import test_runner
235from mobly.controllers import android_device
236
237# Number of seconds for the target to stay discoverable on Bluetooth.
238DISCOVERABLE_TIME = 60
239
240
241class HelloWorldTest(base_test.BaseTestClass):
242    def setup_class(self):
243        # Registering android_device controller module, and declaring that the test
244        # requires at least two Android devices.
245        self.ads = self.register_controller(android_device, min_number=2)
246        # The device used to discover Bluetooth devices.
247        self.discoverer = android_device.get_device(
248            self.ads, label='discoverer')
249        # Sets the tag that represents this device in logs.
250        self.discoverer.debug_tag = 'discoverer'
251        # The device that is expected to be discovered
252        self.target = android_device.get_device(self.ads, label='target')
253        self.target.debug_tag = 'target'
254        self.target.load_snippet('mbs', android_device.MBS_PACKAGE)
255        self.discoverer.load_snippet('mbs', android_device.MBS_PACKAGE)
256
257    def setup_test(self):
258        # Make sure bluetooth is on.
259        self.target.mbs.btEnable()
260        self.discoverer.mbs.btEnable()
261        # Set Bluetooth name on target device.
262        self.target.mbs.btSetName('LookForMe!')
263
264    def test_bluetooth_discovery(self):
265        target_name = self.target.mbs.btGetName()
266        self.target.log.info('Become discoverable with name "%s" for %ds.',
267                             target_name, DISCOVERABLE_TIME)
268        self.target.mbs.btBecomeDiscoverable(DISCOVERABLE_TIME)
269        self.discoverer.log.info('Looking for Bluetooth devices.')
270        discovered_devices = self.discoverer.mbs.btDiscoverAndGetResults()
271        self.discoverer.log.debug('Found Bluetooth devices: %s',
272                                  pprint.pformat(discovered_devices, indent=2))
273        discovered_names = [device['Name'] for device in discovered_devices]
274        logging.info('Verifying the target is discovered by the discoverer.')
275        asserts.assert_true(
276            target_name in discovered_names,
277            'Failed to discover the target device %s over Bluetooth.' %
278            target_name)
279
280    def teardown_test(self):
281        # Turn Bluetooth off on both devices after test finishes.
282        self.target.mbs.btDisable()
283        self.discoverer.mbs.btDisable()
284
285
286if __name__ == '__main__':
287    test_runner.main()
288
289```
290
291There's potentially a lot more we could do in this test, e.g. check
292the hardware address, see whether we can pair devices, transfer files, etc.
293
294To learn more about the features included in MBS, go to [MBS repo](
295https://github.com/google/mobly-bundled-snippets) to see how to check its help
296menu.
297
298To learn more about Mobly Snippet Lib, including features like Espresso support
299and asynchronous calls, see the [snippet lib examples](
300https://github.com/google/mobly-snippet-lib/tree/master/examples).
301
302
303## Example 6: Generated Tests
304
305A common use case in writing tests is to execute the same test logic multiple
306times, each time with a different set of parameters. Instead of duplicating the
307same test case with minor tweaks, you could use the **Generated tests** in
308Mobly.
309
310Mobly could generate test cases for you based on a list of parameters and a
311function that contains the test logic. Each generated test case is equivalent
312to an actual test case written in the class in terms of execution, procedure
313functions (setup/teardown/on_fail), and result collection. You could also
314select generated test cases via the `--test_case` cli arg as well.
315
316
317Here's an example of generated tests in action. We will reuse the "Example 1:
318Hello World!". Instead of making one toast of "Hello World", we will generate
319several test cases and toast a different message in each one of them.
320
321You could reuse the config file from Example 1.
322
323The test class would look like:
324
325 
326**many_greetings_test.py**
327 
328```python
329from mobly import base_test
330from mobly import test_runner
331from mobly.controllers import android_device
332
333
334class ManyGreetingsTest(base_test.BaseTestClass):
335
336    # When a test run starts, Mobly calls this function to figure out what
337    # tests need to be generated. So you need to specify what tests to generate
338    # in this function.
339    def setup_generated_tests(self):
340        messages = [('Hello', 'World'), ('Aloha', 'Obama'),
341                    ('konichiwa', 'Satoshi')]
342        # Call `generate_tests` function to specify the tests to generate. This
343        # function can only be called within `setup_generated_tests`. You could
344        # call this function multiple times to generate multiple groups of
345        # tests.
346        self.generate_tests(
347            # Specify the function that has the common logic shared by these
348            # generated tests.
349            test_logic=self.make_toast_logic,
350            # Specify a function that creates the name of each test.
351            name_func=self.make_toast_name_function,
352            # A list of tuples, where each tuple is a set of arguments to be
353            # passed to the test logic and name function.
354            arg_sets=messages)
355
356    def setup_class(self):
357        self.ads = self.register_controller(android_device)
358        self.dut = self.ads[0]
359        self.dut.load_snippet('mbs', android_device.MBS_PACKAGE)
360
361    # The common logic shared by a group of generated tests.
362    def make_toast_logic(self, greeting, name):
363        self.dut.mbs.makeToast('%s, %s!' % (greeting, name))
364
365    # The function that generates the names of each test case based on each
366    # argument set. The name function should have the same signature as the
367    # actual test logic function.
368    def make_toast_name_function(self, greeting, name):
369        return 'test_greeting_say_%s_to_%s' % (greeting, name)
370
371
372if __name__ == '__main__':
373    test_runner.main()
374```
375
376Three test cases will be executed even though we did not "physically" define
377any "test_xx" function in the test class.
378