Authoring Tests
Authoring Tests
## Overview
There are two types of automated tests you can add for Azure CLI: [unit tests]
(https://en.wikipedia.org/wiki/Unit_testing) and [integration tests]
(https://en.wikipedia.org/wiki/Integration_testing).
For unit tests, we support unit tests written in the forms standard [unittest]
(https://docs.python.org/3/library/unittest.html).
Azure CLI translates user inputs into Azure Python SDK calls which communicate with
[Azure REST API](https://docs.microsoft.com/rest/api/). These HTTP communications
are captured and recorded so the integration tests can be replayed in an automation
environment without making actual HTTP calls. This ensures that the commands
actually work against the service when they are recorded (and then can be re-run
live to verify) and provides protection against regressions or breaking changes
when they are played back.
The scenario tests are run in replayable mode in the Travis CI during pull request
verification and branch merge. However, the tests will be run in live mode nightly
in internal test infrastructure. See [Test Policies](#test-policies) for more
details.
1) The live scenario tests actually verify the end to end scenario.
2) The live scenario tests ensure the credibility of the tested scenario.
3) The test recording tends to go stale. The sample it captures will eventually
deviate from the actual traffic samples.
4) The tests in playback mode does not verify the request body and it doesn't
ensure the correct requests sequence.
5) The unhealthy set of live tests prevent the Azure CLI team from rebaselining
tests rapidly.
6) Neglecting the live tests will reduce the quality and the credibility of the
test bed.
It is a requirement for the command owner to maintain their test in live mode.
## Authoring Tests
## Recording Tests
### Preparation
It is a good practice to add recording file to the local git cache, which makes it
easy to diff the different versions of recording to detect issues or changes.
Once the recording file is generated, execute the test again. This time the test
will run in playback mode. The execution is offline, and will not act on the Azure
subscription.
If the replay passes, you can commit the tests as well as recordings and submit
them as part of your PR.
When a recording file is missing, the test framework will execute the test in live
mode. You can also force tests to run live either by setting the environment
variable `AZURE_TEST_RUN_LIVE` or by using the `--live` flag with the `azdev test`
command.
Also, you can author tests which are for live test only by deriving the test class
from `LiveScenarioTest`. Since these tests will not be run as part of the CI, you
lose much of the automatic regression protection by doing this. However, for
certain tests that cannot be re-recorded, cannot be replayed, or fail due to
service issues, this is a viable approach.
Recorded tests are run on TravisCI as part of all pull request validations.
Therefore it is important to keep the recordings up-to-date, and to ensure that
they can be re-recorded.
Live tests run nightly in a separate system and are not tied to pull requests.
Here are some issues that may occur when authoring tests that you should be aware
of.
* **Non-deterministic results**: If you find that a test will pass on some
playbacks but fail on others, there are a couple possible things to check:
1. check if your command makes use of concurrency.
2. check your parameter aliasing (particularly if it complains that a required
parameter is missing that you know is there)
If your command makes use of concurrency, consider using unit tests,
LiveScenarioTest, or, if practical, forcing the test to operate on a single thread
for recording and playback.
* **Paths**: When including paths in your tests as parameter values, always wrap
them in double quotes. While this isn't necessary when running from the command
line (depending on your shell environment), it will likely cause issues with the
test framework.
* **Defaults**: Ensure you don't have any defaults configured with `az configure`
prior to running tests. Defaults can interfere with the expected test flow.
```python
from azure.cli.testsdk import ScenarioTest
class StorageAccountTests(ScenarioTest):
def test_list_storage_account(self):
self.cmd('az storage account list')
```
Notes:
1. When the test is run and no recording file is available, the test will be run in
live mode. A recording file will be created at `recording/<test_method_name>.yaml`.
2. Wrap the command in the `self.cmd` method. It will assert that the exit code of
the command is zero.
3. All the functions and classes you need for writing tests are included in the
`azure.cli.testsdk` namespace. It is recommended __not__ to import from a sub-
namespace to avoid breaking changes.
```python
class StorageAccountTests(ScenarioTest):
def test_list_storage_account(self):
accounts_list = self.cmd('az storage account list').get_output_in_json()
assert len(accounts_list) > 0
```
Notes:
```python
from azure.cli.testsdk import ScenarioTest
class StorageAccountTests(ScenarioTest):
def test_list_storage_account(self):
self.cmd('az account list-locations', checks=[
self.check("[?name=='westus'].displayName | [0]", 'West US')
])
```
Notes:
```python
from azure.cli.testsdk import ScenarioTest, ResourceGroupPreparer
class StorageAccountTests(ScenarioTest):
@ResourceGroupPreparer()
def test_create_storage_account(self, resource_group):
self.cmd('az group show -n {rg}', checks=[
self.check('name', '{rg}'),
self.check('properties.provisioningState', 'Succeeded')
])
```
Notes:
1. The preparers are executed before each test in the test class when `setUp` is
executed. Any resources created in this way will be cleaned up after testing.
Unless you specify that a preparer use an existing resource via its associated
environment variable. See [Test-Related Environment Variables](#test-related-
environment-variables).
2. The resource group name is injected into the test method as a parameter. By
default `ResourceGroupPreparer` passes the value as the `resource_group` parameter.
The target parameter can be customized (see following samples).
3. The resource group will be deleted asynchronously for performance reason.
4. The resource group will automatically be registered into the tests keyword
arguments (`self.kwargs`) with the key default key of `rg`. This can then be
directly plugged into the command string in `cmd` and into the verification step of
the `check` method. The test infrastructure will automatically replace the values.
```python
class StorageAccountTests(ScenarioTest):
@ResourceGroupPreparer(parameter_name='group_name',
parameter_name_for_location='group_location')
def test_create_storage_account(self, group_name, group_location):
self.kwargs.update({
'loc': group_location
})
self.cmd('az group show -n {rg}', checks=[
self.check('name', '{rg}'),
self.check('location', '{loc}'),
self.check('properties.provisioningState', 'Succeeded')
])
```
Notes:
1. In addition to the name, the location of the resource group can be also injected
into the test method.
2. Both parameters' names can be customized.
3. You can add the location to the test's kwargs using the `self.kwargs.update`
method. This allows you to take advantage of the automatic kwarg replacement in
your command strings and checks.
```python
class StorageAccountTests(ScenarioTest):
@ResourceGroupPreparer(parameter_name_for_location='location')
def test_create_storage_account(self, resource_group, location):
self.kwargs.update({
'name': self.create_random_name(prefix='cli', length=24),
'loc': location,
'sku': 'Standard_LRS',
'kind': 'Storage'
})
self.cmd('az storage account create -n {name} -g {rg} --sku {sku} -l
{loc}')
self.cmd('az storage account show -n {name} -g {rg}', checks=[
self.check('name', '{name}'),
self.check('location', '{loc}'),
self.check('sku.name', '{sku}'),
self.check('kind', '{kind}')
])
```
Note:
```python
from azure.cli.testsdk import ScenarioTest, ResourceGroupPreparer,
StorageAccountPreparer
class StorageAccountTests(ScenarioTest):
@ResourceGroupPreparer()
@StorageAccountPreparer()
def test_list_storage_accounts(self, storage_account):
accounts = self.cmd('az storage account list').get_output_in_json()
search = [account for account in accounts if account['name'] ==
storage_account]
assert len(search) == 1
```
Note:
1. Like `ResourceGroupPreparer`, you can use `StorageAccountPreparer` to prepare a
disposable storage account for the test. The account is deleted along with the
resource group during test teardown.
2. Creation of a storage account requires a resource group. Therefore
`ResourceGroupPrepare` must be placed above `StorageAccountPreparer`, since
preparers are designed to be executed from top to bottom. (The core preparer
implementation is in the [AbstractPreparer](https://github.com/Azure/azure-python-
devtools/blob/master/src/azure_devtools/scenario_tests/preparers.py) class in the
[azure-devtools](https://pypi.python.org/pypi/azure-devtools) package.)
3. The preparers communicate among themselves by adding values to the `kwargs` of
the decorated methods. Therefore the `StorageAccountPreparer` uses the resource
group created in the preceding `ResourceGroupPreparer`.
4. The `StorageAccountPreparer` can be further customized to modify the parameters
of the created storage account:
```python
@StorageAccountPreparer(sku='Standard_LRS', location='southcentralus',
parameter_name='storage')
```
```python
class StorageAccountTests(ScenarioTest):
@ResourceGroupPreparer()
@StorageAccountPreparer(parameter_name='account_1')
@StorageAccountPreparer(parameter_name='account_2')
def test_list_storage_accounts(self, account_1, account_2):
accounts_list = self.cmd('az storage account list').get_output_in_json()
assert len(accounts_list) >= 2
assert next(acc for acc in accounts_list if acc['name'] == account_1)
assert next(acc for acc in accounts_list if acc['name'] == account_2)
```
Note:
<!--
Note: This document's source uses
[semantic linefeeds](http://rhodesmill.org/brandon/2012/one-sentence-per-line/)
to make diffs and updates clearer.
-->
```python
with self.assertRaisesRegexp(CLIError, "usage error: --vnet NAME --subnet NAME |
--vnet ID --subnet NAME | --subnet ID"):
self.cmd('container create -g {rg} -n {container_group_name} --image
nginx --vnet {vnet_name}')
```
The above syntax is the recommended way to test that a specific error occurs. You
must pass the type of the error as well as a string used to match the error
message. If the error is encountered, the text will be validated and, if matching,
the command will be deemed a success (for testing purposes) and execution will
continue. If the command does not yield the expected error, the test will fail.
There are a few environment variables that modify the behaviour of the CLI's test
infrastructure.
However there are other environment variables that alter the behavior of resource
preparers such as the `ResourceGroupPreparer`, such as:
1. **`AZURE_CLI_TEST_DEV_RESOURCE_GROUP_NAME` and
`AZURE_CLI_TEST_DEV_RESOURCE_GROUP_LOCATION`** : If set, the first environment
variable specifies an existing resource group to be returned by the
`ResourceGroupPreparer`. The specified resource group will not be deleted after the
test. The second specifies the existing resource group's location, defaults to the
location passed when calling `ResourceGroupPreparer`.
2. **`AZURE_CLI_TEST_DEV_STORAGE_ACCOUNT_NAME`** : Specifies an existing storage
account. Associated with the `StorageAccountPreparer`.
3. **`AZURE_CLI_TEST_DEV_KEY_VAULT_NAME`** : Specifies an existing key vault name.
Associated with the `KeyVaultPreparer`.
5. **`AZURE_CLI_TEST_DEV_SP_NAME` and `AZURE_CLI_TEST_DEV_SP_PASSWORD`** :
Specifies the name of an existing service principal and its password. Associated
with the `RoleBasedServicePrincipalPreparer`.
4. **`AZURE_CLI_TEST_DEV_APP_NAME`and `AZURE_CLI_TEST_DEV_APP_SECRET`** : Specifies
an existing managed app and its secret (secret defaults to `None`). Associated with
the `ManagedApplicationPreparer`.
Setting these variables can reduce the time taken to run live tests new resource as
the preparers don't have to create and delete new resources.
> To learn more about these environment variables, please see the preparers' source
code in [`/src/azure-cli-testsdk/azure/cli/testsdk/preparers.py`]
(https://github.com/Azure/azure-cli/blob/dev/src/azure-cli-
testsdk/azure/cli/testsdk/preparers.py).