-
Notifications
You must be signed in to change notification settings - Fork 110
Feature/python hexagonal arch #107
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
base: main
Are you sure you want to change the base?
Changes from 4 commits
23bdc81
958ae97
ad46856
76ea9a7
e4ac402
8274013
7cafe63
b8f22e3
542cb75
b8b2bb5
7d052fd
375633e
c330939
4f993cf
15dadc4
6a9db61
e78fd23
9254862
8f20faf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,165 +0,0 @@ | ||
AWSTemplateFormatVersion: '2010-09-09' | ||
Transform: AWS::Serverless-2016-10-31 | ||
Description: > | ||
This template deploys a code sample for testing an asynchronous architecture using Python. | ||
|
||
Parameters: | ||
DeployTestResources: | ||
Description: The parameter instructs the template whether or not to deploy test resources to your environment. | ||
Default: "True" | ||
Type: String | ||
AllowedValues: | ||
- "True" | ||
- "False" | ||
ConstraintDescription: Allowed values are True and False | ||
|
||
Conditions: | ||
CreateTestResources: !Equals [!Ref DeployTestResources, "True"] | ||
|
||
Globals: # https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-specification-template-anatomy-globals.html | ||
Function: | ||
Timeout: 15 | ||
MemorySize: 256 | ||
Runtime: python3.9 | ||
Tracing: Active # https://docs.aws.amazon.com/lambda/latest/dg/lambda-x-ray.html | ||
# Embed Lambda Powertools as a shared Layer | ||
# See: https://awslabs.github.io/aws-lambda-powertools-python/latest/#lambda-layer | ||
Layers: # | ||
- !Sub arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPython:9 | ||
Environment: | ||
Variables: | ||
DESTINATION_BUCKET: | ||
!Sub "async-destination-${AWS::StackName}-${AWS::AccountId}" | ||
RESULTS_TABLE: | ||
!Sub "async-results-${AWS::StackName}-${AWS::AccountId}" | ||
# Powertools env vars: https://awslabs.github.io/aws-lambda-powertools-python/#environment-variables | ||
LOG_LEVEL: INFO | ||
POWERTOOLS_LOGGER_SAMPLE_RATE: 0.1 | ||
POWERTOOLS_LOGGER_LOG_EVENT: true | ||
POWERTOOLS_METRICS_NAMESPACE: MyServerlessApplication | ||
POWERTOOLS_SERVICE_NAME: async | ||
|
||
Resources: | ||
SourceBucket: | ||
Type: AWS::S3::Bucket | ||
UpdateReplacePolicy: Delete | ||
Properties: | ||
BucketName: | ||
!Sub "async-source-${AWS::StackName}-${AWS::AccountId}" | ||
BucketEncryption: | ||
ServerSideEncryptionConfiguration: | ||
- ServerSideEncryptionByDefault: | ||
SSEAlgorithm: AES256 | ||
PublicAccessBlockConfiguration: | ||
BlockPublicAcls: true | ||
BlockPublicPolicy: true | ||
IgnorePublicAcls: true | ||
RestrictPublicBuckets: true | ||
|
||
DestinationBucket: | ||
Type: AWS::S3::Bucket | ||
UpdateReplacePolicy: Delete | ||
Properties: | ||
BucketName: | ||
!Sub "async-destination-${AWS::StackName}-${AWS::AccountId}" | ||
BucketEncryption: | ||
ServerSideEncryptionConfiguration: | ||
- ServerSideEncryptionByDefault: | ||
SSEAlgorithm: AES256 | ||
PublicAccessBlockConfiguration: | ||
BlockPublicAcls: true | ||
BlockPublicPolicy: true | ||
IgnorePublicAcls: true | ||
RestrictPublicBuckets: true | ||
|
||
ToUppercaseTextTransformer: | ||
Type: AWS::Serverless::Function | ||
Properties: | ||
FunctionName: | ||
!Sub "ToUppercaseTextTransformer-${AWS::StackName}-${AWS::AccountId}" | ||
CodeUri: src/ | ||
Handler: app.handler | ||
Policies: | ||
- S3ReadPolicy: | ||
BucketName: | ||
!Sub "async-source-${AWS::StackName}-${AWS::AccountId}" | ||
- S3CrudPolicy: | ||
BucketName: | ||
!Sub "async-destination-${AWS::StackName}-${AWS::AccountId}" | ||
Events: | ||
FileUpload: | ||
Type: S3 | ||
Properties: | ||
Bucket: !Ref SourceBucket | ||
Events: s3:ObjectCreated:* | ||
Filter: | ||
S3Key: | ||
Rules: | ||
- Name: suffix | ||
Value: '.txt' | ||
|
||
AsyncTransformTestResultsTable: | ||
Type: AWS::DynamoDB::Table | ||
Condition: CreateTestResources | ||
Properties: | ||
TableName: | ||
!Sub "async-results-${AWS::StackName}-${AWS::AccountId}" | ||
AttributeDefinitions: | ||
- AttributeName: id | ||
AttributeType: S | ||
KeySchema: | ||
- AttributeName: id | ||
KeyType: HASH | ||
ProvisionedThroughput: | ||
ReadCapacityUnits: 2 | ||
WriteCapacityUnits: 2 | ||
|
||
DestinationBucketListener: | ||
Type: AWS::Serverless::Function | ||
Condition: CreateTestResources | ||
Properties: | ||
FunctionName: | ||
!Sub "DestinationBucketListener-${AWS::StackName}-${AWS::AccountId}" | ||
CodeUri: tests/integration | ||
Handler: event_listener_lambda.handler | ||
Policies: | ||
- DynamoDBWritePolicy: | ||
TableName: !Ref AsyncTransformTestResultsTable | ||
- S3ReadPolicy: | ||
BucketName: | ||
!Sub "async-destination-${AWS::StackName}-${AWS::AccountId}" | ||
Events: | ||
FileUpload: | ||
Type: S3 | ||
Properties: | ||
Bucket: !Ref DestinationBucket | ||
Events: s3:ObjectCreated:* | ||
Filter: | ||
S3Key: | ||
Rules: | ||
- Name: suffix | ||
Value: '.txt' | ||
|
||
|
||
Outputs: | ||
|
||
SourceBucketName: | ||
Description: "Source bucket for asynchronous testing sample" | ||
Value: !Ref SourceBucket | ||
|
||
DestinationBucketName: | ||
Description: "Destination bucket for asynchronous testing sample" | ||
Value: !Ref DestinationBucket | ||
|
||
DestinationBucketListenerName: | ||
Condition: CreateTestResources | ||
Description: "Lambda Function to listen for test results" | ||
Value: !Ref DestinationBucketListener | ||
|
||
AsyncTransformTestResultsTable: | ||
Condition: CreateTestResources | ||
Description: "DynamoDB table to persist test results" | ||
Value: !Ref AsyncTransformTestResultsTable | ||
|
||
|
||
|
||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
[](https://img.shields.io/badge/Python-3.9-green) | ||
[](https://img.shields.io/badge/AWS-DynamoDB-blueviolet) | ||
[](https://img.shields.io/badge/Test-Unit-blue) | ||
[](https://img.shields.io/badge/Test-Integration-yellow) | ||
|
||
# Python: Hexagonal Architecture Example | ||
|
||
## Introduction | ||
Hexagonal architecture is a pattern used for encapsulating domain logic and decoupling it from other implementation details, such as infrastructure or client requests. You can use these types of architectures to improve how to organize and test your Lambda functions. | ||
|
||
System Under Test (SUT) | ||
|
||
The SUT in this pattern is a Lambda function that is organized using a hexagonal architecture. You can read this blog post to learn more about these types of architectures. The example in this test pattern receives a request via API Gateway and makes calls out to other AWS cloud services like DynamoDB. | ||
|
||
The project uses the [AWS Serverless Application Model](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) (SAM) CLI for configuration, testing and deployment. | ||
|
||
--- | ||
|
||
## Contents | ||
- [Python: Hexagonal Architecture Example](#python-hexagonal-architecture-example) | ||
- [Introduction](#introduction) | ||
- [Contents](#contents) | ||
- [Key Files in the Project](#key-files-in-the-project) | ||
- [Sample project description](#sample-project-description) | ||
- [Testing Data Considerations](#testing-data-considerations) | ||
- [Run the Unit Test](#run-the-unit-test) | ||
- [Run the Integration Test](#run-the-integration-test) | ||
--- | ||
|
||
## Key Files in the Project | ||
- [app.py](src/app.py) - Lambda handler code to test | ||
- [template.yaml](template.yaml) - SAM script for deployment | ||
- [mock_test.py](tests/unit/mock_test.py) - Unit test using mocks | ||
- [test_api_gateway.py](tests/integration/test_api_gateway.py) - Integration tests on a live stack | ||
|
||
[Top](#contents) | ||
|
||
--- | ||
|
||
## Sample project description | ||
|
||
Hexagonal Architecture: | ||
Hexagonal architecture is also known as the ports and adapters architecture. It is an architectural pattern used for encapsulating domain logic and decoupling it from other implementation details, such as infrastructure or client requests. | ||
|
||
In Lambda functions, hexagonal architecture can help you implement new business requirements and improve the agility of a workload. This approach can help create separation of concerns and separate the domain logic from the infrastructure. For development teams, it can also simplify the implementation of new features and parallelize the work across different developers. | ||
|
||
Terms: | ||
|
||
1. Domain logic: Represents the task that the application should perform, abstracting any interaction with the external world. | ||
2. Ports: Provide a way for the primary actors (on the left) to interact with the application, via the domain logic. The domain logic also uses ports for interacting with secondary actors (on the right) when needed. | ||
rohanmeh marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
3. Adapters: A design pattern for transforming one interface into another interface. They wrap the logic for interacting with a primary or secondary actor. | ||
4. Primary actors: Users of the system such as a webhook, a UI request, or a test script. | ||
5. Secondary actors: used by the application, these services are either a Repository (for example, a database) or a Recipient (such as a message queue). | ||
|
||
Application Description | ||
|
||
The example application is a backend web service built using Amazon API Gateway, AWS Lambda, and Amazon DynamoDB. Business logic in the domain layer should be tested with unit tests. Responses from secondary actors via ports should be mocked during unit testing to speed up test execution. | ||
|
||
Adapter and port code can be tested in the cloud by deploying primary and secondary actors such as an API Gateway and a DynamoDB table. The test code will create an HTTP client that will send requests to the deployed API Gateway endpoint. The endpoint will invoke the primary actor, test resource configuration, IAM permissions, authorizers, internal business logic, and secondary actors of the SUT. | ||
|
||
This project consists of an [API Gateway](https://aws.amazon.com/api-gateway/), a single [AWS Lambda](https://aws.amazon.com/lambda) function, and 2 [Amazon DynamoDB](https://aws.amazon.com/dynamodb) tables. | ||
|
||
The two DynamoDB tables are meant to track Stock ID's and prices in EUR (Euros) and Euro Currency Conversion rates. | ||
|
||
[Top](#contents) | ||
|
||
--- | ||
|
||
## Testing Data Considerations | ||
|
||
Data persistence brings additional testing considerations. | ||
|
||
First, the data stores must be pre-populated with data to test certain functionality. In our example, we need a valid stock and valid currency conversion data to test our function. Therefore, we will add data to the data stores prior to running the tests. This data seeding operation is performed in the test setup. | ||
|
||
Second, the data store will be populated as a side-effect of our testing. In our example, stock and currency conversion data will be populated in our DynamoDB tables. To prevent unintended side-effects, we will clean-up data generated during the test execution. This data cleaning operation is performed in the test tear-down. | ||
|
||
[Top](#contents) | ||
|
||
--- | ||
|
||
## Run the Unit Test | ||
[mock_test.py](tests/unit/mock_test.py) | ||
|
||
In the [unit test](tests/unit/mock_test.py), all references and calls to the DynamoDB service [are mocked on line 18](tests/unit/mock_test.py#L20). | ||
|
||
The unit test establishes the STOCKS_DB_TABLE and CURRENCIES_DB_TABLE environment | ||
variables that the Lambda function uses to reference the DynamoDB tables. STOCKS_DB_TABLE and CURRENCIES_DB_TABLE are defined in the [setUp method of test class in mock_test.py](tests/unit/mock_test.py#L37-38). | ||
|
||
In a unit test, you must create a mocked version of the DynamoDB table. The example approach in the [setUp method of test class in mock_test.py](tests/unit/mock_test.py#L43-50) reads in the DynamoDB table schema directly the [SAM Template](template.yaml) so that the definition is maintained in one place. This simple technique works if there are no intrinsics (like !If or !Ref) in the resource properties for KeySchema, AttributeDefinitions, & BillingMode. Once the mocked table is created, test data is populated. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The unit test mocks all calls to DynamoDB. The example approach in the setUp method of test class in mock_test.py reads in the DynamoDB table schema directly from the SAM Template so that the definition is maintained in one place. |
||
|
||
With the mocked DynamoDB table created and the STOCKS_DB_TABLE and CURRENCIES_DB_TABLE set to the mocked table names, the Lambda function will use the mocked DynamoDB tables when executing. | ||
|
||
The [unit test tear-down](tests/unit/mock_test.py#L61-66) removes the mocked DynamoDB tables and clears the STOCKS_DB_TABLE and CURRENCIES_DB_TABLE environment variables. | ||
|
||
To run the unit test, execute the following | ||
```shell | ||
# Create and Activate a Python Virtual Environment | ||
# One-time setup | ||
hexagonal-architectures$ pip3 install virtualenv | ||
hexagonal-architectures$ python3 -m venv venv | ||
hexagonal-architectures$ source ./venv/bin/activate | ||
|
||
# install dependencies | ||
hexagonal-architectures$ pip3 install -r tests/requirements.txt | ||
rohanmeh marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
# run unit tests with mocks | ||
hexagonal-architectures$ python3 -m coverage run -m pytest | ||
``` | ||
|
||
[Top](#contents) | ||
|
||
--- | ||
|
||
## Run the Integration Test | ||
|
||
(Coming Soon) | ||
|
||
|
||
[test_api_gateway.py](tests/integration/test_api_gateway.py) | ||
|
||
For integration tests, the full stack is deployed before testing: | ||
```shell | ||
hexagonal-architectures$ sam build | ||
hexagonal-architectures$ sam deploy --guided | ||
``` | ||
|
||
The [integration test](tests/integration/test_api_gateway.py) setup determines both the [API endpoint](tests/integration/test_api_gateway.py#L50-53) and the name of the [DynamoDB table](tests/integration/test_api_gateway.py#L56-58) in the stack. | ||
|
||
The integration test then [populates data into the DynamoDB table](tests/integration/test_api_gateway.py#L66-70). | ||
|
||
The [integration test tear-down](tests/integration/test_api_gateway.py#L73-87) removes the seed data, as well as data generated during the test. | ||
|
||
To run the integration test, create the environment variable "AWS_SAM_STACK_NAME" with the name of the test stack, and execute the test. | ||
|
||
```shell | ||
# Set the environment variables AWS_SAM_STACK_NAME and (optionally)AWS_DEFAULT_REGION | ||
# to match the name of the stack and the region where you will test | ||
|
||
hexagonal-architectures$ AWS_SAM_STACK_NAME=<stack-name> AWS_DEFAULT_REGION=<region_name> python -m pytest -s tests/integration -v | ||
rohanmeh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
``` | ||
|
||
[Top](#contents) | ||
|
||
--- |
Uh oh!
There was an error while loading. Please reload this page.