├── sum.py ├── requirements.txt ├── package.json ├── tests ├── test_sum.py └── test_handler.py ├── .travis.yml ├── serverless.yml ├── LICENSE ├── handler.py ├── README.md └── .gitignore /sum.py: -------------------------------------------------------------------------------- 1 | def sum(x,y): 2 | return x + y -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytest>=4.0.2 2 | codecov>=2.0.15 3 | pytest-cov>=2.6.0 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tdd-sample-app", 3 | "version": "0.2.0", 4 | "description": "A sample app that follows CI/CD principles", 5 | "author": "Mustafa İlhan", 6 | "license": "MIT" 7 | } -------------------------------------------------------------------------------- /tests/test_sum.py: -------------------------------------------------------------------------------- 1 | import os,sys,inspect 2 | current_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) 3 | parent_dir = os.path.dirname(current_dir) 4 | sys.path.insert(0, parent_dir) 5 | import sum 6 | 7 | def test_sum(): 8 | assert sum.sum(3, 4) == 7 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "3.6" 5 | 6 | install: 7 | - pip install -r requirements.txt 8 | 9 | script: 10 | - pytest --cov=./ 11 | 12 | cache: 13 | directories: 14 | - node_modules 15 | - .serverless 16 | 17 | jobs: 18 | include: 19 | - if: branch = master 20 | env: DEPLOY_STAGE=production 21 | - if: branch =~ /^release\/v\d+\.\d+(\.\d+)?$/ 22 | env: DEPLOY_STAGE=beta 23 | - if: branch = dev 24 | env: DEPLOY_STAGE=dev 25 | 26 | after_success: 27 | - codecov 28 | - npm install -g serverless 29 | - serverless deploy --stage $DEPLOY_STAGE 30 | 31 | branches: 32 | only: 33 | - master 34 | - /^release\/v\d+\.\d+(\.\d+)?$/ 35 | - dev -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: tdd-sample-app 2 | 3 | provider: 4 | name: aws 5 | runtime: python3.6 6 | region: eu-central-1 7 | memorySize: 128 8 | timeout: 30 9 | 10 | functions: 11 | hello: 12 | handler: handler.hello_endpoint 13 | events: 14 | - http: 15 | path: hello 16 | method: get 17 | sum: 18 | handler: handler.sum_endpoint 19 | events: 20 | - http: 21 | path: sum 22 | method: post 23 | 24 | package: 25 | exclude: 26 | - .pytest_cache/** 27 | - __pycache__/** 28 | - tests/ 29 | - venv/** 30 | - node_modules/** 31 | - .covarage 32 | - .gitignore 33 | - .travis.yml 34 | - LICENSE 35 | - package.json 36 | - package-lock.json 37 | - README.me 38 | - requirements.txt 39 | - serverless.yml 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mustafa İlhan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /handler.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import re 4 | 5 | 6 | logger = logging.getLogger() 7 | logger.setLevel(logging.INFO) 8 | 9 | def hello_endpoint(event, context): 10 | logger.info(event) 11 | 12 | body = { 13 | "message": "Hello world!" 14 | } 15 | 16 | response = { 17 | "statusCode": 200, 18 | "body": json.dumps(body) 19 | } 20 | 21 | return response 22 | 23 | def sum_endpoint(event, context): 24 | logger.info(event) 25 | 26 | # Default response 27 | body = { 28 | "error": "Please provide at least one number e.g. numbers=1,2,3" 29 | } 30 | 31 | if 'body' in event and event['body'] is not None: 32 | # Regex that matches "numbers=1,2,33" 33 | r = re.compile("^numbers=([-+]?([1-9]\\d*|0),?)+$") 34 | # Get numbers from post data 35 | numbers = list(filter(r.match, event['body'].split('&'))) 36 | if len(numbers) > 0: 37 | numbers = numbers[0].replace('numbers=', '') 38 | numbers = numbers.split(',') 39 | body = { 40 | "sum": sum(map(int, numbers)) 41 | } 42 | 43 | response = { 44 | "statusCode": 200, 45 | "body": json.dumps(body) 46 | } 47 | 48 | return response -------------------------------------------------------------------------------- /tests/test_handler.py: -------------------------------------------------------------------------------- 1 | import os,sys,inspect 2 | current_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) 3 | parent_dir = os.path.dirname(current_dir) 4 | sys.path.insert(0, parent_dir) 5 | import handler 6 | 7 | def test_hello_endpoint(): 8 | expected_result = { 9 | "statusCode": 200, 10 | "body": "{\"message\": \"Hello world!\"}" 11 | } 12 | assert handler.hello_endpoint(None, None) == expected_result 13 | 14 | def test_sum_endpoint_numbers(): 15 | event = { 16 | "body": "numbers=1,2" 17 | } 18 | expected_result = { 19 | "statusCode": 200, 20 | "body": "{\"sum\": 3}" 21 | } 22 | assert handler.sum_endpoint(event, None) == expected_result 23 | 24 | def test_sum_endpoint_many_numbers(): 25 | event = { 26 | "body": "numbers=1,2,34" 27 | } 28 | expected_result = { 29 | "statusCode": 200, 30 | "body": "{\"sum\": 37}" 31 | } 32 | assert handler.sum_endpoint(event, None) == expected_result 33 | 34 | def test_sum_endpoint_nobody(): 35 | event = { 36 | } 37 | expected_result = { 38 | "statusCode": 200, 39 | "body": "{\"error\": \"Please provide at least one number e.g. numbers=1,2,3\"}" 40 | } 41 | assert handler.sum_endpoint(event, None) == expected_result 42 | 43 | def test_sum_endpoint_string(): 44 | event = { 45 | "body": "numbers=asdas,1,2" 46 | } 47 | expected_result = { 48 | "statusCode": 200, 49 | "body": "{\"error\": \"Please provide at least one number e.g. numbers=1,2,3\"}" 50 | } 51 | assert handler.sum_endpoint(event, None) == expected_result 52 | 53 | def test_sum_endpoint_nonumbers(): 54 | event = { 55 | "body": "nuasdambers=1,2" 56 | } 57 | expected_result = { 58 | "statusCode": 200, 59 | "body": "{\"error\": \"Please provide at least one number e.g. numbers=1,2,3\"}" 60 | } 61 | assert handler.sum_endpoint(event, None) == expected_result -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tdd-sample-app 2 | [![Build Status](https://travis-ci.com/ilhan-mstf/tdd-sample-app.svg?branch=master)](https://travis-ci.com/ilhan-mstf/tdd-sample-app) [![codecov](https://codecov.io/gh/ilhan-mstf/tdd-sample-app/branch/master/graph/badge.svg)](https://codecov.io/gh/ilhan-mstf/tdd-sample-app) 3 | 4 | > A sample app that implements Continous Integration and Continous Deployment flow. 5 | 6 | This repo provides a sample flow for integration test and automatic deployment of a project. The functionality of the project is to sum given numbers. It is deployed to the AWS Lamda. Lambda function is invoked by a HTTP call. There is two different HTTP call; first one is a `GET` request and just returns 'Hello World' json response and the other one is a `POST` request gets numbers from request body, validates numbers and returns summation of them as a json. 7 | 8 | In this project, GitFlow branching model is followed and automatic tests and deployment operations are run when there is a new commit to branches: dev, release/\*, and master. Each branch has a different deployment stage. For example, master branch is deployed to production, on the other hand, release/\* branch is deployed to beta, and lastly dev is deployed to dev. 9 | 10 | Project is coded in Python and for unit tests PyTest is used. To deploy AWS Lamba, Serverless Framework is used. For continuos integration and deployment Travis CI is used. Furthermore, Codecov is used to measure code covarage by unit tests. 11 | 12 | When a commit is pushed to one of the branches among master, dev, and release/\* a TravisCI job is triggered. First, it prepares the environment and then run tests. If the code passes tests, code covarage report is generated and deployed to the related staging environment. If it fails, you'll get a notification about failure, and also you can track the status from TravisCI dashboard. Furthermore, badges provides a shortcut to view the status of branch and they are generated by TravisCI and CodeCov. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # IPython 78 | profile_default/ 79 | ipython_config.py 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # SageMath parsed files 88 | *.sage.py 89 | 90 | # Environments 91 | .env 92 | .venv 93 | env/ 94 | venv/ 95 | ENV/ 96 | env.bak/ 97 | venv.bak/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | .dmypy.json 112 | dmypy.json 113 | 114 | # Pyre type checker 115 | .pyre/ 116 | 117 | # Serverless 118 | .serverless/ 119 | --------------------------------------------------------------------------------