Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enterprise Unit Testing Sample #310

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from

Conversation

priyaananthasankar
Copy link
Collaborator

Subscription Manager is a canonical enterprise sample delivered to an enterprise customer. It consists of a durable orchestration workflow to create azure subscriptions and manage their levels and mainly demonstrates how unit tests are written in the current version of durable python.

@priyaananthasankar
Copy link
Collaborator Author

cc @scgbear , @davidmrdavid for review and merge.

@davidmrdavid
Copy link
Collaborator

Adding Connor as a review since this also exemplifies how to use EasyAuth w/ Durable

@priyaananthasankar
Copy link
Collaborator Author

Reason why the CI is failing:

I fixed the tests folder in samples, but it looks like pytest is trying to run tests, and it cannot find IsolatedAsyncioTestCase, looks like there is some pytest/unittest compatibility - IsolatedAsyncioTestCase is required to test the async methods in durable, else a co-routine object will be returned that is not worth inspecting.

I guess the best thing to do for now is to escape samples folder for CI/CD - or maybe samples can have their own test runner.

ImportError while importing test module '/home/vsts/work/1/s/samples/unit_testing/subscription-manager/unit_tests/test_createenvhttpstart_invalidauth.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
samples/unit_testing/subscription-manager/unit_tests/test_createenvhttpstart_invalidauth.py:4: in
from unittest import IsolatedAsyncioTestCase,mock
E ImportError: cannot import name 'IsolatedAsyncioTestCase'

Copy link
Collaborator

@davidmrdavid davidmrdavid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I finished a first pass over this PR. Overall, it looks good and well-structured, but I find it to be a bit hard to follow as there's a lot going on.

To be fully transparent, I think we'll probably have to iterate through this a few times until
(1) I get a full understanding of the scenario
(2) We simplify things a bit

Thanks for your patience with us, and please see my comments below. Thank you!!!


This also demonstrates usage of:

- EasyAuth using decoraters
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tiny nit

Suggested change
- EasyAuth using decoraters
- EasyAuth using decorators

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. This is a canonical customer user case so adapting it into a simpler form and keeping the balance between canonical use case vs simplicity is key. Will iterate till it reaches a shape we can agree on

Comment on lines +3 to +6
This project demonstrates a durable workflow that manages a subscription creation long running lifecyle and is adapted from a canonical
real world example.
The durable orchestration, will create a subscription, wait for the subscription to be created (through the durable timer)
and update status of subscription creation in an in-memory status object.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: fixing spacing and tiny typo

Suggested change
This project demonstrates a durable workflow that manages a subscription creation long running lifecyle and is adapted from a canonical
real world example.
The durable orchestration, will create a subscription, wait for the subscription to be created (through the durable timer)
and update status of subscription creation in an in-memory status object.
This project demonstrates a durable workflow that manages a subscription creation long running lifecycle and is adapted from a canonical real world example.
The durable orchestration, will create a subscription, wait for the subscription to be created (through the durable timer) and update status of subscription creation in an in-memory status object.

mock.call_sub_orchestrator.side_effect = sub_orc_mock
mock.task_all.side_effect = task_all_mock

# To get generator results do a next. If orchestrator response is a list, then wrap the function call around a list
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand the comment about list-wrapping in here. Why is that necessary?

Copy link
Collaborator Author

@priyaananthasankar priyaananthasankar Aug 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use next(orchestrator_fn(mock)) as well here, is that more intuitive? using list just makes sure the entire computed result is back for inspection - no big value with list. Let me know if converting it into a next call is more explanatory.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I think I understand this now. This is similar to how map() returns a lazily-evaluated list, so it requires a call to list() in order to obtain the materialized result, isn't it?

In this case, I think it would be simpler if we continued using next(), just for consistency :)

```python
def task_all_mock(tasks:list):
assert len(tasks) == 2
return list
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we returning just list here? Did you mean to return list() or []?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes changed it to [] (this is a mistake due to code churns - sorry!) The task_all never checked back the return type so it probably just returned the list Class and passed.

Comment on lines +139 to +140
The subscription manager uses a custom callback that gets called from another method invoked
inside of an activity function. The following code demonstrates how to patch these callbacks:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first sentence here is a little complex. Any chance we could simplify it? I also don't think the concept of a subscription manager has been introduced until now, so it would be great to have at least one sentence explaining what that is, or linking to the code :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do. This was very specific to the customer use case in fact. Actually would want to discuss in general how to do a lambda based call back.

Comment on lines +39 to +43
a_mock.start_new = AsyncMock()
a_mock().start_new.return_value = instance_id
a_mock().create_check_status_response.return_value = mock_response
response = await main(req=mock_request, starter=starter)
self.assertEqual(403,response.status_code)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to make sure I understand, this isn't testing the HTTPTrigger itself, this is actually just trying to test the decorator, right? I'm just confused because it seems to be like the return value for create_check_status_response is already being provided

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we are only testing the decorator here and making sure it authorizes and calls the function. So yes, for valid auth we should be removing the canned response. Will discuss further. For the invalid auth, the decorator itself would return a 403 - so we don't really care about the create_check_status_response. but I see why it can be confusing. Will remove this canned response

"""
Test class for CreateEnvironmentHTTPStart Durable HTTP Starter Function that kicks off orchestrations
Mocks the HTTP Request and expected HTTP Response from the Durable HTTP start method and tests
- invalid route params
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is the invalid route param being provided?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line 33: clientName is None

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. That wasn't entirely clear to me at first :) . I think that abstracting the boilerplate setup away, as suggested in another comment, will help make this clearer

Comment on lines +19 to +33
function_name = 'DurableFunctionsOrchestrator'
instance_id = 'f86a9f49-ae1c-4c66-a60e-991c4c764fe5'
productName = 'test_product'
clientName = 'microsoft'
environmentName = 'production'
starter = MagicMock()

mock_request = func.HttpRequest(
method='POST',
body=b'{"sub_product":"test"}',
url=f'http://localhost:7071/api/orchestrators/product/{productName}/clients/{clientName}/environments/{environmentName}/',
route_params={
'clientName' : clientName
},
headers={"X-MS-CLIENT-PRINCIPAL": "ICAgICAgICB7CiAgICAgICAgICAgICJhdXRoX3R5cCI6ICJhYWQiLAogICAgICAgICAgICAiY2xhaW1zIjogW3sKICAgICAgICAgICAgICAgICJ0eXAiOiAiaHR0cDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MvMjAwNS8wNS9pZGVudGl0eS9jbGFpbXMvc3VybmFtZSIsCiAgICAgICAgICAgICAgICAidmFsIjogIlVzZXIiCiAgICAgICAgICAgIH0sIHsKICAgICAgICAgICAgICAgICJ0eXAiOiAiZ3JvdXBzIiwKICAgICAgICAgICAgICAgICJ2YWwiOiAiZWY2ZDJkMWEtNzhlYi00YmIxLTk3YzctYmI4YThlNTA5ZTljIgogICAgICAgICAgICB9LCB7CiAgICAgICAgICAgICAgICAidHlwIjogImdyb3VwcyIsCiAgICAgICAgICAgICAgICAidmFsIjogIjNiMjMxY2UxLTI5YzEtNDQxZS1iZGRiLTAzM2Y5NjQwMTg4OCIKICAgICAgICAgICAgfSwgewogICAgICAgICAgICAgICAgInR5cCI6ICJuYW1lIiwKICAgICAgICAgICAgICAgICJ2YWwiOiAiVGVzdCBVc2VyIgogICAgICAgICAgICB9XSwKICAgICAgICAgICAgIm5hbWVfdHlwIjogImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiLAogICAgICAgICAgICAicm9sZV90eXAiOiAiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIgogICAgICAgIH0="}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of this setup here seems, at glance, to be repeated across many tests. Is it possible to abstract them away in a common utility? It would help with readability, and to identify what specific change in this setup leads allows the target scenario to be tested.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes agree.

Comment on lines +14 to +29
def call_activity_create_subscription(activityName: str, payload):
assert activityName == "CreateSubscription"
mock.call_activity.side_effect = call_activity_mock_status_check_succeeded

def call_activity_mock_status_check_accepted(activityName: str, payload):
assert activityName == "StatusCheck"

def call_activity_mock_status_check_succeeded(activityName: str, payload):
assert activityName == "StatusCheck"

def call_activity_mock_status_check_notfound(activityName:str, payload):
mock.call_activity.side_effect = call_activity_create_subscription
assert activityName == "StatusCheck"

def call_activity_mock_status_check_error(activityName:str, payload):
assert activityName == "StatusCheck"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if most of these are do the same, I think it would help simplify things if we just had one method for them all :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - I just thought if anyone wants to customize a very specific activity, they could and it helps makes it readable

Comment on lines +39 to +40
# change the call_activity side effect to succeeded call
mock.call_activity.side_effect = call_activity_mock_status_check_succeeded
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this necessary?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the confusion is because all the call_activity mocks are the same. While simulating a scenario where sub creation is already underway, where the subscription creation API's would respond with an "Accepted" state - the durable orchestrator will start a timer until this state changes from Accepted -> Succeeded. So I am mocking accordingly. What I can do to make this more meaningful is to return the canned response from each call activity to make them unique.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, let's discuss this one during our meeting :)

@RobertPaulson90
Copy link

Any particular reason not to use pytest? We are discussing in our team whether or not to roll with pure unittest or add in pytest. Would be interesting to hear your opinion!

@davidmrdavid
Copy link
Collaborator

Hi @binarypunch: we have no particular justification for using one unit testing framework from the other. I think both are similarly popular in the community :)

Test class for MgmtGroupSubOrchestrator Durable orchestrator that kicks off an activity function
Mocks the DurableOrchestrationContext and checks the sequence of sub-orchestration calls
"""
class TestMgmtGroupSubOrchestrator(TestCase):
Copy link

@mycaule mycaule May 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, have you thought of leveraging what is already in

I also think these classes and methods should be in the main package, and users should be able to import them just with from azure.durable_functions.testing import .... Refactoring what @scgbear and @davidmrdavid did in test_sequential_orchestrator.py for example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants