Skip to content

Commit 47dfd59

Browse files
authored
[Test Proxy] Documentation updates and improvements (#27036)
1 parent defaa79 commit 47dfd59

File tree

2 files changed

+108
-39
lines changed

2 files changed

+108
-39
lines changed

doc/dev/test_proxy_migration_guide.md

+98-31
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ Resource preparers need a management client to function, so test classes that us
9595

9696
### Perform one-time setup
9797

98-
1. Docker is a requirement for using the test proxy. You can install Docker from [docs.docker.com][docker_install].
99-
2. After installing, make sure Docker is running and is using Linux containers before running tests.
98+
1. Docker (or Podman) is a requirement for using the test proxy. You can install Docker from [docs.docker.com][docker_install], or install Podman at [podman.io][podman]. To use Podman, set an alias for `podman` to replace the `docker` command.
99+
2. After installing, make sure Docker/Podman is running and is using Linux containers before running tests.
100100
3. Follow the instructions [here][proxy_cert_docs] to complete setup. You need to trust a certificate on your machine in
101101
order to communicate with the test proxy over a secure connection.
102102

@@ -110,6 +110,7 @@ In a `conftest.py` file for your package's tests, add a session-level fixture th
110110
`devtools_testutils.test_proxy` as a parameter (and has `autouse` set to `True`):
111111

112112
```python
113+
import pytest
113114
from devtools_testutils import test_proxy
114115

115116
# autouse=True will trigger this fixture on each pytest run, even if it's not explicitly used by a test method
@@ -119,9 +120,7 @@ def start_proxy(test_proxy):
119120
```
120121

121122
The `test_proxy` fixture will fetch the test proxy Docker image and create a new container called
122-
`ambitious_azsdk_test_proxy` if one doesn't exist already. If the container already exists, the fixture will start the
123-
container if it's currently stopped. The container will be stopped after tests finish running, but will stay running if
124-
test execution is interrupted.
123+
`ambitious_azsdk_test_proxy`, which will be deleted after test execution unless interrupted.
125124

126125
If your tests already use an `autouse`d, session-level fixture for tests, you can accept the `test_proxy` parameter in
127126
that existing fixture instead of adding a new one. For an example, see the [Register sanitizers](#register-sanitizers)
@@ -144,26 +143,22 @@ need old `.yml` recordings.
144143
> **Note:** support for configuring live or playback tests with a `testsettings_local.cfg` file has been
145144
> deprecated in favor of using just `AZURE_TEST_RUN_LIVE`.
146145
147-
> **Note:** the recording storage location is determined when the proxy Docker container is created. If there are
148-
> multiple local copies of the `azure-sdk-for-python` repo on your machine, you will need to delete any existing
149-
> `ambitious_azsdk_test_proxy` container before recordings can be stored in a different repo copy.
150-
151146
### Register sanitizers
152147

153148
Since the test proxy doesn't use [`vcrpy`][vcrpy], tests don't use a scrubber to sanitize values in recordings.
154149
Instead, sanitizers (as well as matchers and transforms) can be registered on the proxy as detailed in
155150
[this][sanitizers] section of the proxy documentation. Sanitizers can be registered via `add_*_sanitizer` methods in
156151
`devtools_testutils`. For example, the general-use method for sanitizing recording bodies, headers, and URIs is
157-
`add_general_regex_sanitizer`. Other sanitizers are available for more specific scenarios and can be found at
152+
`add_general_string_sanitizer`. Other sanitizers are available for more specific scenarios and can be found at
158153
[devtools_testutils/sanitizers.py][py_sanitizers].
159154

160155
Sanitizers, matchers, and transforms remain registered until the proxy container is stopped, so for any sanitizers that
161156
are shared by different tests, using a session fixture declared in a `conftest.py` file is recommended. Please refer to
162157
[pytest's scoped fixture documentation][pytest_fixtures] for more details.
163158

164159
As a simple example, to emulate the effect registering a name pair with a `vcrpy` scrubber, you can provide the exact
165-
value you want to sanitize from recordings as the `regex` in the general regex sanitizer. With `vcrpy`, you would likely
166-
do something like the following:
160+
value you want to sanitize from recordings as the `target` in the general string sanitizer. With `vcrpy`, you would
161+
likely do something like the following:
167162

168163
```python
169164
import os
@@ -180,19 +175,21 @@ To do the same sanitization with the test proxy, you could add something like th
180175

181176
```python
182177
import os
183-
from devtools_testutils import add_general_regex_sanitizer, test_proxy
178+
from devtools_testutils import add_general_string_sanitizer, test_proxy
184179

185180
# autouse=True will trigger this fixture on each pytest run, even if it's not explicitly used by a test method
186181
@pytest.fixture(scope="session", autouse=True)
187182
def add_sanitizers(test_proxy):
188-
add_general_regex_sanitizer(regex=os.getenv("AZURE_KEYVAULT_NAME"), value="fake-vault")
183+
# The default value for the environment variable should be the value you use in playback
184+
vault_name = os.getenv("AZURE_KEYVAULT_NAME", "fake-vault")
185+
add_general_string_sanitizer(target=vault_name, value="fake-vault")
189186
```
190187

191188
Note that the sanitizer fixture accepts the `test_proxy` fixture as a parameter to ensure the proxy is started
192189
beforehand.
193190

194191
For a more advanced scenario, where we want to sanitize the account names of all Tables endpoints in recordings, we
195-
could instead call
192+
could instead use the `add_general_regex_sanitizer` method:
196193

197194
```python
198195
add_general_regex_sanitizer(
@@ -267,32 +264,40 @@ This loader is meant to be paired with the PowerShell test resource management c
267264
[/eng/common/TestResources][test_resources]. It's recommended that all test suites use these scripts for live test
268265
resource management.
269266

270-
For an example of using the EnvironmentVariableLoader with the test proxy, you can refer to the Tables SDK. The
271-
CosmosPreparer and TablesPreparer defined in this [preparers.py][tables_preparers] file each define an instance of the
272-
EnvironmentVariableLoader, which are used to fetch environment variables for Cosmos and Tables, respectively. These
273-
preparers can be used to decorate test methods directly; for example:
267+
The EnvironmentVariableLoader accepts a positional `directory` argument and arbitrary keyword-only arguments:
268+
- `directory` is the name of your package's service as it appears in the Python repository; i.e. `service` in `azure-sdk-for-python/sdk/service/azure-service-package`.
269+
- For example, for `azure-keyvault-keys`, the value of `directory` is `keyvault`.
270+
- For each environment variable you want to provide to tests, pass in a keyword argument with the pattern `environment_variable_name="sanitized-value"`.
271+
- For example, to fetch the value of `STORAGE_ENDPOINT` and sanitize this value in recordings as `fake-endpoint`, provide `storage_endpoint="fake-endpoint"` to the EnvironmentVariableLoader constructor.
272+
273+
Decorated test methods will have the values of environment variables passed to them as keyword arguments, and these
274+
values will automatically have sanitizers registered with the test proxy. More specifically, the true values of
275+
requested variables will be provided to tests in live mode, and the sanitized values of these variables will be provided
276+
in playback mode.
277+
278+
The most common way to use the EnvironmentVariableLoader is to declare a callable specifying arguments by using
279+
`functools.partial` and then decorate test methods with that callable. For example:
274280

275281
```python
276-
from devtools_testutils import AzureRecordedTestCase, recorded_by_proxy
277-
from .preparers import TablesPreparer
282+
import functools
283+
from devtools_testutils import AzureRecordedTestCase, EnvironmentVariableLoader, recorded_by_proxy
284+
285+
ServicePreparer = functools.partial(
286+
EnvironmentVariableLoader,
287+
"service",
288+
service_endpoint="fake-endpoint",
289+
service_account_name="fake-account-name",
290+
)
278291

279292
class TestExample(AzureRecordedTestCase):
280293

281-
@TablesPreparer()
294+
@ServicePreparer()
282295
@recorded_by_proxy
283296
def test_example_with_preparer(self, **kwargs):
284-
tables_storage_account_name = kwargs.pop("tables_storage_account_name")
285-
tables_primary_storage_account_key = kwargs.pop("tables_primary_storage_account_key")
297+
service_endpoint = kwargs.pop("service_endpoint")
286298
...
287299
```
288300

289-
Or, they can be used in a custom decorator, as they are in the `cosmos_decorator` and `tables_decorator` defined in
290-
[preparers.py][tables_preparers]. `@tables_decorator`, for instance, is then used in place of `@TablesPreparer()` for
291-
the example above (note that the method-style `tables_decorator` is used without parentheses).
292-
293-
Decorated test methods will have the values of environment variables passed to them as keyword arguments, and these
294-
values will automatically have sanitizers registered with the test proxy.
295-
296301
### Record test variables
297302

298303
To run recorded tests successfully when there's an element of non-secret randomness to them, the test proxy provides a
@@ -420,6 +425,64 @@ container if it's not already running.
420425

421426
For more details on proxy startup, please refer to the [proxy documentation][detailed_docs].
422427

428+
### Use `pytest.mark.parametrize` with migrated tests
429+
430+
Migrating tests to use basic `pytest` tools allows us to take advantage of helpful features such as
431+
[parametrization][parametrize]. Parametrization allows you to share test code by re-running the same test with varying
432+
inputs. For example, [`azure-keyvault-keys` tests][parametrize_example] are parametrized to run with multiple API
433+
versions and multiple Key Vault configurations.
434+
435+
Because of how the `pytest.mark.parametrize` mechanism works, the `recorded_by_proxy(_async)` decorators aren't
436+
compatible without an additional decorator that handles the arguments we want to parametrize. The callable that
437+
`pytest.mark.parametrize` decorates needs to have positional parameters that match the arguments we're parametrizing;
438+
for example:
439+
440+
```python
441+
import pytest
442+
from devtools_testutils import recorded_by_proxy
443+
444+
test_values = [
445+
("first_value_a", "first_value_b"),
446+
("second_value_a", "second_value_b"),
447+
]
448+
449+
# Works because `parametrize` decorates a method with positional `a` and `b` parameters
450+
@pytest.mark.parameterize("a, b", test_values)
451+
def test_function(a, b, **kwargs):
452+
...
453+
454+
# Doesn't work; raises collection error
455+
# `recorded_by_proxy`'s wrapping function doesn't accept positional `a` and `b` parameters
456+
@pytest.mark.parameterize("a, b", test_values)
457+
@recorded_by_proxy
458+
def test_recorded_function(a, b, **kwargs):
459+
...
460+
```
461+
462+
To parametrize recorded tests, we need a decorator between `pytest.mark.parametrize` and `recorded_by_proxy` that
463+
accepts the expected arguments. We can do this by declaring a class with a custom `__call__` method:
464+
465+
```python
466+
class ArgumentPasser:
467+
def __call__(self, fn):
468+
# _wrapper accepts the `a` and `b` arguments we want to parametrize with
469+
def _wrapper(test_class, a, b, **kwargs):
470+
fn(test_class, a, b, **kwargs)
471+
return _wrapper
472+
473+
# Works because `ArgumentPasser.__call__`'s return value has the expected parameters
474+
@pytest.mark.parameterize("a, b", test_values)
475+
@ArgumentPasser()
476+
@recorded_by_proxy
477+
def test_recorded_function(a, b, **kwargs):
478+
...
479+
```
480+
481+
You can also introduce additional logic into the `__call__` method of your intermediate decorator. In the aforementioned
482+
[`azure-keyvault-keys` test example][parametrize_example], the decorator between `parametrize` and `recorded_by_proxy`
483+
is actually a [client preparer][parametrize_class] that creates a client based on the parametrized input and passes this
484+
client to the test.
485+
423486

424487
[detailed_docs]: https://github.com/Azure/azure-sdk-tools/tree/main/tools/test-proxy/Azure.Sdk.Tools.TestProxy/README.md
425488
[docker_install]: https://docs.docker.com/get-docker/
@@ -431,8 +494,12 @@ For more details on proxy startup, please refer to the [proxy documentation][det
431494

432495
[mgmt_recorded_test_case]: https://github.com/Azure/azure-sdk-for-python/blob/main/tools/azure-sdk-tools/devtools_testutils/mgmt_recorded_testcase.py
433496

497+
[parametrize]: https://docs.pytest.org/latest/example/parametrize.html
498+
[parametrize_example]: https://github.com/Azure/azure-sdk-for-python/blob/d92b63b9976b0025b274016c49a250fb7c4d7333/sdk/keyvault/azure-keyvault-keys/tests/test_key_client.py#L182
499+
[parametrize_class]: https://github.com/Azure/azure-sdk-for-python/blob/d92b63b9976b0025b274016c49a250fb7c4d7333/sdk/keyvault/azure-keyvault-keys/tests/_test_case.py#L59
434500
[pipelines_ci]: https://github.com/Azure/azure-sdk-for-python/blob/5ba894966ed6b0e1ee8d854871f8c2da36a73d79/sdk/eventgrid/ci.yml#L30
435501
[pipelines_live]: https://github.com/Azure/azure-sdk-for-python/blob/e2b5852deaef04752c1323d2ab0958f83b98858f/sdk/textanalytics/tests.yml#L26-L27
502+
[podman]: https://podman.io/
436503
[proxy_cert_docs]: https://github.com/Azure/azure-sdk-tools/blob/main/tools/test-proxy/documentation/test-proxy/trusting-cert-per-language.md
437504
[py_sanitizers]: https://github.com/Azure/azure-sdk-for-python/blob/main/tools/azure-sdk-tools/devtools_testutils/sanitizers.py
438505
[pytest_collection]: https://docs.pytest.org/latest/goodpractices.html#test-discovery

doc/dev/tests.md

+10-8
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,8 @@ To migrate an existing test suite to use the test proxy, or to learn more about
151151

152152
### Perform one-time test proxy setup
153153

154-
1. Docker is a requirement for using the test proxy. You can install Docker from [docs.docker.com][docker_install].
155-
2. After installing, make sure Docker is running and is using Linux containers before running tests.
154+
1. Docker (or Podman) is a requirement for using the test proxy. You can install Docker from [docs.docker.com][docker_install], or install Podman at [podman.io][podman]. To use Podman, set an alias for `podman` to replace the `docker` command.
155+
2. After installing, make sure Docker/Podman is running and is using Linux containers before running tests.
156156
3. Follow the instructions [here][proxy_cert_docs] to complete setup. You need to trust a certificate on your machine in
157157
order to communicate with the test proxy over a secure connection.
158158

@@ -213,6 +213,7 @@ Create a `conftest.py` file within your package's test directory (`sdk/{service}
213213
session-level fixture that accepts `devtools_testutils.test_proxy` as a parameter (and has `autouse` set to `True`):
214214

215215
```python
216+
import pytest
216217
from devtools_testutils import test_proxy
217218

218219
# autouse=True will trigger this fixture on each pytest run, even if it's not explicitly used by a test method
@@ -358,27 +359,27 @@ There are two primary ways to keep secrets from being written into recordings:
358359
1. The `EnvironmentVariableLoader` will automatically sanitize the values of captured environment variables with the
359360
provided fake values.
360361
2. Sanitizers can be registered via `add_*_sanitizer` methods in `devtools_testutils`. For example, the general-use
361-
method for sanitizing recording bodies, headers, and URIs is `add_general_regex_sanitizer`. Other sanitizers are
362+
method for sanitizing recording bodies, headers, and URIs is `add_general_string_sanitizer`. Other sanitizers are
362363
available for more specific scenarios and can be found at [devtools_testutils/sanitizers.py][py_sanitizers].
363364

364365
As a simple example of registering a sanitizer, you can provide the exact value you want to sanitize from recordings as
365-
the `regex` in the general regex sanitizer. To replace all instances of the string "my-key-vault" with "fake-vault" in
366+
the `target` in the general string sanitizer. To replace all instances of the string "my-key-vault" with "fake-vault" in
366367
recordings, you could add something like the following in the package's `conftest.py` file:
367368

368369
```python
369-
from devtools_testutils import add_general_regex_sanitizer, test_proxy
370+
from devtools_testutils import add_general_string_sanitizer, test_proxy
370371

371372
# autouse=True will trigger this fixture on each pytest run, even if it's not explicitly used by a test method
372373
@pytest.fixture(scope="session", autouse=True)
373374
def add_sanitizers(test_proxy):
374-
add_general_regex_sanitizer(regex="my-key-vault", value="fake-vault")
375+
add_general_string_sanitizer(target="my-key-vault", value="fake-vault")
375376
```
376377

377378
Note that the sanitizer fixture accepts the `test_proxy` fixture as a parameter to ensure the proxy is started
378379
beforehand (see [Start the test proxy server](#start-the-test-proxy-server)).
379380

380381
For a more advanced scenario, where we want to sanitize the account names of all storage endpoints in recordings, we
381-
could instead call
382+
could instead use `add_general_regex_sanitizer`:
382383

383384
```python
384385
add_general_regex_sanitizer(
@@ -640,7 +641,7 @@ Tests that use the Shared Access Signature (SAS) to authenticate a client should
640641
[engsys_wiki]: https://dev.azure.com/azure-sdk/internal/_wiki/wikis/internal.wiki/48/Create-a-new-Live-Test-pipeline?anchor=test-resources.json
641642
[env_var_loader]: https://github.com/Azure/azure-sdk-for-python/blob/main/tools/azure-sdk-tools/devtools_testutils/envvariable_loader.py
642643

643-
[generate_sas]: https://github.com/Azure/azure-sdk-for-python/blob/6e1f7c02af0c28d5725a532ebe4fc7125256858c/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py#L200
644+
[generate_sas]: https://github.com/Azure/azure-sdk-for-python/blob/bf4749babb363e2dc972775f4408036e31f361b4/tools/azure-sdk-tools/devtools_testutils/azure_recorded_testcase.py#L196
644645
[generate_sas_example]: https://github.com/Azure/azure-sdk-for-python/blob/3e3fbe818eb3c80ffdf6f9f1a86affd7e879b6ce/sdk/tables/azure-data-tables/tests/test_table_entity.py#L1691
645646

646647
[kv_test_resources]: https://github.com/Azure/azure-sdk-for-python/blob/main/sdk/keyvault/test-resources.json
@@ -650,6 +651,7 @@ Tests that use the Shared Access Signature (SAS) to authenticate a client should
650651
[mgmt_settings_fake]: https://github.com/Azure/azure-sdk-for-python/blob/main/tools/azure-sdk-tools/devtools_testutils/mgmt_settings_fake.py
651652

652653
[packaging]: https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/packaging.md
654+
[podman]: https://podman.io/
653655
[proxy_cert_docs]: https://github.com/Azure/azure-sdk-tools/blob/main/tools/test-proxy/documentation/trusting-cert-per-language.md
654656
[proxy_general_docs]: https://github.com/Azure/azure-sdk-tools/blob/main/tools/test-proxy/README.md
655657
[proxy_migration_guide]: https://github.com/Azure/azure-sdk-for-python/blob/main/doc/dev/test_proxy_migration_guide.md

0 commit comments

Comments
 (0)