From c13cab792be21fb00788100b3b83810620e9cfc7 Mon Sep 17 00:00:00 2001 From: Pepe Fagoaga Date: Mon, 21 Aug 2023 09:05:57 +0200 Subject: [PATCH] docs(testing): Mocking the service and the service client at the service client level (#2747) --- docs/developer-guide/unit-testing.md | 95 +++++++++++++++++++++------- 1 file changed, 71 insertions(+), 24 deletions(-) diff --git a/docs/developer-guide/unit-testing.md b/docs/developer-guide/unit-testing.md index 1d6560dd..6a38f26c 100644 --- a/docs/developer-guide/unit-testing.md +++ b/docs/developer-guide/unit-testing.md @@ -20,30 +20,6 @@ Here we left some good reads about unit testing and things we've learnt through - https://docs.python.org/3/library/sys.html#sys.settrace - https://github.com/kunalb/panopticon -**Patching vs. Importing** - -This is an important topic within the Prowler check's unit testing. Due to the dynamic nature of the check's load, the process of importing the service client from a check is the following: - -1. `.py`: -```python -from prowler.providers..services.._client import _client -``` -2. `_client.py`: -```python -from prowler.providers..lib.audit_info.audit_info import audit_info -from prowler.providers..services.._service import - -_client = (audit_info) -``` - -Due to the above import path it's not the same to patch the following objects because if you run a bunch of tests, either in parallel or not, some clients can be already instantiated by another check, hence your test exection will be using another test's service instance: - -- `_client` imported at `.py` -- `_client` initialised at `_client.py` -- `` imported at `_client.py` - -A useful read about this topic can be found in the following article: https://stackoverflow.com/questions/8658043/how-to-mock-an-import - ## General Recommendations When creating tests for some provider's checks we follow these guidelines trying to cover as much test scenarios as possible: @@ -370,6 +346,77 @@ with mock.patch( As you can see in the above code, it is required to mock the AWS audit info and both services used. + +#### Patching vs. Importing + +This is an important topic within the Prowler check's unit testing. Due to the dynamic nature of the check's load, the process of importing the service client from a check is the following: + +1. `.py`: +```python +from prowler.providers..services.._client import _client +``` +2. `_client.py`: +```python +from prowler.providers..lib.audit_info.audit_info import audit_info +from prowler.providers..services.._service import + +_client = (audit_info) +``` + +Due to the above import path it's not the same to patch the following objects because if you run a bunch of tests, either in parallel or not, some clients can be already instantiated by another check, hence your test exection will be using another test's service instance: + +- `_client` imported at `.py` +- `_client` initialised at `_client.py` +- `` imported at `_client.py` + +A useful read about this topic can be found in the following article: https://stackoverflow.com/questions/8658043/how-to-mock-an-import + + +#### Different ways to mock the service client + +##### Mocking the service client at the service client level + +Mocking a service client using the following code ... + +```python title="Mocking the service_client" +with mock.patch( + "prowler.providers..lib.audit_info.audit_info.audit_info", + new=audit_info, +), mock.patch( + "prowler.providers.aws.services...._client", + new=(audit_info), +): +``` +will cause that the service will be initialised twice: + +1. When the `(audit_info)` is mocked out using `mock.patch` to have the object ready for the patching. +2. At the `_client.py` when we are patching it since the `mock.patch` needs to go to that object an initialise it, hence the `(audit_info)` will be called again. + +Then, when we import the `_client.py` at `.py`, since we are mocking where the object is used, Python will use the mocked one. + +In the [next section](./unit-testing.md#mocking-the-service-and-the-service-client-at-the-service-client-level) you will see an improved version to mock objects. + + +##### Mocking the service and the service client at the service client level +Mocking a service client using the following code ... + +```python title="Mocking the service and the service_client" +with mock.patch( + "prowler.providers..lib.audit_info.audit_info.audit_info", + new=audit_info, +), mock.patch( + "prowler.providers.aws.services..", + return_value=(audit_info), +) as service_client, mock.patch( + "prowler.providers.aws.services.._client._client", + new=service_client, +): +``` +will cause that the service will be initialised once, just when the `(audit_info)` is mocked out using `mock.patch`. + +Then, at the check_level when Python tries to import the client with `from prowler.providers..services.._client`, since it is already mocked out, the execution will continue using the `service_client` without getting into the `_client.py`. + + ### Services For testing the AWS services we have to follow the same logic as with the AWS checks, we have to check if the AWS API calls made by the service are covered by Moto and we have to test the service `__init__` to verifiy that the information is being correctly retrieved.