├── mountebank ├── __init__.py └── mountebank.py ├── setup.cfg ├── MANIFEST ├── setup.py ├── README.md ├── LICENSE.txt └── tests └── test.py /mountebank/__init__.py: -------------------------------------------------------------------------------- 1 | from mountebank import * -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | setup.cfg 3 | setup.py 4 | mountebank-python/__init__.py 5 | mountebank-python/mountebank.py 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | setup( 3 | name='mountebank-python', 4 | packages=['mountebank'], 5 | version='0.1.4', 6 | description='A thin convenience wrapper for interacting with Mountebank from Python', 7 | author='Alex Holyoke', 8 | author_email='aholyoke@uwaterloo.ca', 9 | license="MIT", 10 | url='https://github.com/aholyoke/mountebank-python', 11 | download_url='https://github.com/aholyoke/mountebank-python/tarball/0.1', 12 | keywords=['testing', 'mountebank'], 13 | classifiers=[], 14 | install_requires=['requests>=2.5.3'], 15 | ) 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mountebank-Python 2 | Simple bindings to make [Mountebank](http://www.mbtest.org) easier to use from Python 3 | 4 | Mountebank is a tool which makes it easier to write tests for [Microservice](http://martinfowler.com/articles/microservices.html) architectures by spawning processes which imitate servers (ie. listening to ports locally and responding to HTTP requests). 5 | 6 | ## Installation 7 | `npm install -g mountebank --production` 8 | 9 | `pip install mountebank-python` 10 | 11 | ## Usage 12 | 13 | An "imposter" is a process which listens on a port (pretending to be a server) 14 | 15 | An imposter has multiple "stubs" 16 | 17 | A stub has a list of "predicates" and "responses" 18 | 19 | Predicates define if a stub matches and incoming HTTP request 20 | 21 | When a stub matches it responds with the next response in it's responses list 22 | 23 | 24 | Run `mb` to start mountebank 25 | 26 | In python: 27 | 1. Define your imposters (example given in mountebank.py) 28 | 2. Initialize a microservice object 29 | 3. Make requests to it 30 | 31 | Example usage in mountebank.py 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Alex Holyoke 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | from mountebank import Microservice, MicroserviceArchitecture, delete_all_imposters 2 | import requests 3 | import unittest 4 | 5 | 6 | class TestMicroservice(unittest.TestCase): 7 | def setUp(self): 8 | example_imposter = { 9 | "protocol": "http", 10 | "stubs": [{ 11 | "responses": [ 12 | {"is": {"statusCode": 400}} 13 | ], 14 | "predicates": [{ 15 | "and": [ 16 | { 17 | "equals": { 18 | "path": "/overview", 19 | "method": "POST" 20 | } 21 | }, 22 | { 23 | "not": { 24 | "exists": { 25 | "query": { 26 | "param1": True, 27 | "param2": True, 28 | "param3": True 29 | } 30 | }, 31 | "caseSensitive": True 32 | } 33 | } 34 | ] 35 | }] 36 | }] 37 | } 38 | self.ms = Microservice(example_imposter) 39 | 40 | def test_required_params(self): 41 | r1 = requests.post( 42 | self.ms.get_url('overview'), 43 | params={'param1': 'a', 'param2': 'b', 'param3': 'c'}) 44 | r2 = requests.post( 45 | self.ms.get_url('overview'), 46 | params={'param1': 'a', 'param2': 'b'}) 47 | self.assertEqual(r1.status_code, 200) 48 | self.assertEqual(r2.status_code, 400) 49 | 50 | def tearDown(self): 51 | self.ms.destroy() 52 | delete_all_imposters() 53 | 54 | 55 | class TestMicroserviceArchitecture(unittest.TestCase): 56 | def setUp(self): 57 | example_architecture = { 58 | "imposters": [ 59 | { 60 | "protocol": "http", 61 | "port": 4546, 62 | "stubs": [{ 63 | "responses": [{"is": {"statusCode": 400}}], 64 | "predicates": [{ 65 | "equals": { 66 | "path": "/a/b", 67 | "method": "POST" 68 | } 69 | }] 70 | }], 71 | }, 72 | { 73 | "protocol": "http", 74 | "port": 4547, 75 | }, 76 | { 77 | "protocol": "smtp", 78 | "port": 4548 79 | } 80 | ] 81 | } 82 | # are we webscale yet? 83 | self.msa = MicroserviceArchitecture(example_architecture) 84 | 85 | def test_required_params(self): 86 | r1 = requests.post(self.msa.get_url(4546, 'a', 'b')) 87 | r2 = requests.put(self.msa.get_url(4546, 'a', 'b')) 88 | r3 = requests.post(self.msa.get_url(4546, 'a')) 89 | r4 = requests.post(self.msa.get_url(4547, 'a')) 90 | self.msa.destroy(4546) 91 | 92 | self.assertEqual(r1.status_code, 400) 93 | self.assertEqual(r2.status_code, 200) 94 | self.assertEqual(r3.status_code, 200) 95 | self.assertEqual(r4.status_code, 200) 96 | 97 | def tearDown(self): 98 | self.msa.destroy_all() 99 | 100 | 101 | if __name__ == '__main__': 102 | unittest.main() 103 | -------------------------------------------------------------------------------- /mountebank/mountebank.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | ''' 4 | http://www.mbtest.org/ 5 | imposter has multiple stubs 6 | stub has multiple predicates and responses 7 | predicates define which stub matches 8 | when a stub matches it uses its next response 9 | ''' 10 | 11 | MOUNTEBANK_HOST = 'http://localhost' 12 | MOUNTEBANK_URL = MOUNTEBANK_HOST + ':2525' 13 | IMPOSTERS_URL = MOUNTEBANK_URL + '/imposters' 14 | 15 | 16 | def create_imposter(definition, method='POST'): 17 | if isinstance(definition, dict): 18 | return requests.request(method, IMPOSTERS_URL, json=definition) 19 | else: 20 | return requests.request(method, IMPOSTERS_URL, data=definition) 21 | 22 | 23 | def create_all_imposters(definitions): 24 | """ PUTting a list of definitions to the imposter endpoint creates imposters in bulk""" 25 | return create_imposter(definitions, 'PUT') 26 | 27 | 28 | def delete_all_imposters(): 29 | return requests.delete(IMPOSTERS_URL) 30 | 31 | 32 | def delete_imposter(port): 33 | return requests.delete("{}/imposters/{}".format(MOUNTEBANK_URL, port)) 34 | 35 | 36 | def get_all_imposters(): 37 | return requests.get(IMPOSTERS_URL) 38 | 39 | 40 | def get_imposter(port): 41 | return requests.get("{}/imposters/{}".format(MOUNTEBANK_URL, port)) 42 | 43 | 44 | class MountebankException(Exception): 45 | pass 46 | 47 | 48 | class Microservice(object): 49 | def __init__(self, definition): 50 | self.definition = definition 51 | 52 | def __enter__(self): 53 | self.start() 54 | 55 | def __exit__(self, exc_type, exc_val, exc_tb): 56 | self.stop() 57 | 58 | def __repr__(self): 59 | return "Microservice(port={})".format(self.port) 60 | 61 | def start(self): 62 | resp = create_imposter(self.definition) 63 | if resp.status_code != 201: 64 | raise MountebankException("{}: {}".format(resp.status_code, resp.text)) 65 | self.port = resp.json()['port'] 66 | return self 67 | 68 | def stop(self): 69 | return self.destroy() 70 | 71 | def get_url(self, *endpoint): 72 | return "{}:{}/{}".format(MOUNTEBANK_HOST, self.port, "/".join(name for name in endpoint)) 73 | 74 | def get_self(self): 75 | return get_imposter(self.port) 76 | 77 | def destroy(self): 78 | return delete_imposter(self.port) 79 | 80 | 81 | class MicroserviceArchitecture(object): 82 | def __init__(self, definitions): 83 | self.definitions = definitions 84 | 85 | def __enter__(self): 86 | self.start() 87 | return self 88 | 89 | def __exit__(self, exc_type, exc_val, exc_tb): 90 | self.stop() 91 | 92 | def __repr__(self): 93 | return "MicroserviceArchitecture(ports={})".format(self.ports) 94 | 95 | def start(self): 96 | resp = create_all_imposters(self.definitions) 97 | if resp.status_code != 200: 98 | raise MountebankException("{}: {}".format(resp.status_code, resp.text)) 99 | self.ports = [imp['port'] for imp in resp.json()['imposters']] 100 | 101 | def stop(self): 102 | return [self.destroy(port) for port in self.ports] 103 | 104 | def get_url(self, port, *endpoint): 105 | return "{}:{}/{}".format(MOUNTEBANK_HOST, port, "/".join(name for name in endpoint)) 106 | 107 | def get_urls(self): 108 | return [self.get_url(port) for port in self.ports] 109 | 110 | def get_self(self, port): 111 | return get_imposter(port) 112 | 113 | def get_selves(self): 114 | return [self.get_self(port) for port in self.ports] 115 | 116 | def destroy(self, port): 117 | return delete_imposter(port) 118 | 119 | def destroy_all(self): 120 | delete_all_imposters() 121 | --------------------------------------------------------------------------------