Skip to content

Commit

Permalink
Improved mocks bootstrap
Browse files Browse the repository at this point in the history
  • Loading branch information
shanejansen committed Jan 30, 2020
1 parent e29c375 commit 583bec6
Show file tree
Hide file tree
Showing 7 changed files with 52 additions and 43 deletions.
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ Let's say we are building a microservice that is responsible for managing users.
* `DELETE /user/{id}` - A user is deleted from a relational database. A message is also published to a broker on the exchange: 'user.exchange' with a routing key of: 'user-deleted' and a payload containing the user's id.
* The service is also listening for messages published to the exchange: 'order-placed.exchange'. When a message is received, the order payload is saved to a NoSQL database.

With Touchstone, it is possible to write end-to-end tests for all of the above requirements independent of the language/framework used. For example, we can write an end-to-end test for the `DELETE /user/{id}` endpoint that will ensure the user record is removed from the database and a message is published to the correct exchange with the correct payload. When ran, Touchstone will monitor mock instances of the service's dependencies to ensure the requirements are met. Touchstone also makes it easy to perform exploratory testing locally during development by starting dependencies and populating them with data via a single command.
With Touchstone, it is possible to write end-to-end tests for all of the above requirements independent of the language/framework used. For example, we can write an end-to-end test for the `DELETE /user/{id}` endpoint that will ensure the user record is removed from the database and a message is published to the correct exchange with the correct payload. When ran, Touchstone will monitor mock instances of the service's dependencies to ensure the requirements are met. Touchstone also makes it easy to perform exploratory testing locally during development by starting dependencies and populating them with data in a single command.

An example of the above requirements are implemented in a Java/Spring service in this repo. Touchstone tests have been written to test the [user endpoint requirements](./examples/java-spring/touchstone/tests/user.py) and [order messaging requirements](./examples/java-spring/touchstone/tests/order.py).
An example of the above requirements is implemented in a Java/Spring service in this repo. Touchstone tests have been written to test the [user endpoint requirements](./examples/java-spring/touchstone/tests/user.py) and [order messaging requirements](./examples/java-spring/touchstone/tests/order.py).


## Installation
Expand All @@ -36,8 +36,8 @@ Requirements:
After installation, Touchstone will be available via `touchstone` in your terminal.
Touchstone has three basic commands:
* `touchstone init` - Initialize Touchstone in the current directory. Used for new projects.
* `touchstone run` - Run all Touchstone tests and exit. This is typically how you'd run your end-to-end tests on a build server. Ports will be auto-discovered in this mode to avoid collisions in case multiple runs occur on the same host. See [mocks docs](#mocks) for more information on how to hook into auto-discovered ports.
* `touchstone develop` - Start a development session of Touchstone. You'd typically use this to develop/debug a service locally. This will keep service dependencies running while you make changes to your end-to-end tests or the services themselves. This will also provide a web interface to each dependency for additional debugging.
* `touchstone run` - Run all Touchstone tests and exit. This is typically how you wouldd run your end-to-end tests on a build server. Ports will be auto-discovered in this mode to avoid collisions in case multiple runs occur on the same host. See [mocks docs](#mocks) for more information on how to hook into auto-discovered ports.
* `touchstone develop` - Start a development session of Touchstone. You would typically use this to develop/debug a service locally. This will keep service dependencies running while you make changes to your end-to-end tests or the services themselves. This will also provide a web interface to each dependency for additional debugging.

After running `touchstone init`, a new directory will be created with the following contents:

Expand All @@ -48,7 +48,7 @@ Your services and their monitored dependencies are defined here. Default values
* `services:` - Each service included in your end-to-end tests is defined here.
* `name:` - Default: unnamed-service. The name of the service.
* `tests:` - Default: ./tests. The path to Touchstone tests for this service.
* `host:` - Default: parent host. Fine grained host control per service.
* `host:` - Default: parent host. Fine-grained host control per service.
* `port:` - Default: 8080. The port used for this service.
* `dockerfile:` - Default: N/A. Used to containerize the service during `touchstone run`. If you are only running Touchstone locally, this can be omitted.
* `availability_endpoint:` - Default: N/A. Used to determine when the service is healthy so tests can be executed. A `200` must be returned from the endpoint to be considered healthy.
Expand All @@ -66,14 +66,14 @@ This directory contains YAML files where default values for mocked dependencies
### `tests`
[Example](./examples/java-spring/touchstone/tests)
This directory is the default location for your end-to-end tests. This can optionally be configured for each service in `touchstone.yml`.
Touchstone follows a _given_, _when_, _then_ testing pattern. Each test is declared in a Python file as a class that extends `TouchstoneTest`. By extending this class, you can access Touchstone mocked dependencies to setup and then verify your test requirements. For example, we can insert a document into a Mongo DB collection and then verify it exists using the following code:
Touchstone follows a _given_, _when_, _then_ testing pattern. Each test is declared in a Python file as a class that extends `TouchstoneTest`. By extending this class, you can access Touchstone mocked dependencies to setup and then verify your requirements. For example, we can insert a document into a Mongo DB collection and then verify it exists using the following code:
```python
self.mocks.mongodb.setup.insert_document('my_db', 'my_collection', {'foo': 'bar'})
result: bool = self.mocks.mongodb.verify.document_exists('my_db', 'my_collection', {'foo': 'bar'})
```
Important APIs:
* `self.mocks` - Hook into Touchstone managed mock dependencies.
* `self.service_url` - The service under test's URL. Useful for calling REST endpoints on the service under test.
* `self.service_url` - The service under test's URL. Useful for calling RESTful endpoints on the service under test.
* `touchstone.lib.mocks.validation` - Contains methods for validating test results. `validation.ANY` can be used to accept any value which is useful in some circumstances.

## Mocks
Expand Down
13 changes: 13 additions & 0 deletions docs/deploy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
```commandline
# Create dist
python setup.py sdist bdist_wheel
# Ensure Twine is installed
python -m pip install --upgrade twine
# Upload to test PyPi (remove --repository-url for prod PyPi)
python -m twine upload --repository-url https://test.pypi.org/legacy/ dist/*
# Install from test PyPi
pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple touchstone-testing==x.x.x
```
2 changes: 1 addition & 1 deletion docs/mocks/mongodb.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ databases:
# Insert a document into a collection
self.mocks.mongodb.setup.insert_document('my_db', 'my_collection', {'foo': 'bar'})

# Verify that a document was inserted into a collection
# Verify that a document exists in a collection
result: bool = self.mocks.mongodb.verify.document_exists('my_db', 'my_collection', {'foo': 'bar'})
```
5 changes: 4 additions & 1 deletion src/setup.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from setuptools import setup, find_packages

with open("../README.md", "r") as fh:
long_description = fh.read()

setup(
name='touchstone-testing',
version='0.1.0',
description='Touchstone is a testing framework for your services that focuses on end-to-end and exploratory testing.',
long_description='See "Homepage"',
long_description=long_description,
long_description_content_type="text/markdown",
url='https://github.com/shane-jansen/touchstone',
author='Shane Jansen',
Expand Down
40 changes: 17 additions & 23 deletions src/touchstone/bootstrap.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import os

import yaml

from touchstone import common
from touchstone.lib import exceptions
from touchstone.lib.configs.service_config import ServiceConfig
Expand Down Expand Up @@ -38,31 +37,26 @@ def __build_touchstone_config(self, root) -> TouchstoneConfig:

def __build_mocks(self, root, touchstone_config, host, docker_manager) -> Mocks:
mocks = Mocks(root)
mocks.http = Http(host, self.is_dev_mode, docker_manager)
mocks.rabbitmq = Rabbitmq(host, self.is_dev_mode, docker_manager)
mocks.mongodb = Mongodb(host, self.is_dev_mode, docker_manager)
mocks.mysql = Mysql(host, self.is_dev_mode, docker_manager)
potential_mocks = [mocks.http, mocks.rabbitmq, mocks.mongodb, mocks.mysql]

if not touchstone_config.config['mocks']:
return mocks

for mock in touchstone_config.config['mocks']:
user_config = touchstone_config.config['mocks'][mock]
if Http.name() == mock:
http = Http(host, self.is_dev_mode, docker_manager)
http.config = common.dict_merge(http.default_config(), user_config)
mocks.http = http
mocks.register_mock(http)
elif Rabbitmq.name() == mock:
rabbitmq = Rabbitmq(host, self.is_dev_mode, docker_manager)
rabbitmq.config = common.dict_merge(rabbitmq.default_config(), user_config)
mocks.rabbitmq = rabbitmq
mocks.register_mock(rabbitmq)
elif Mongodb.name() == mock:
mongodb = Mongodb(host, self.is_dev_mode, docker_manager)
mongodb.config = common.dict_merge(mongodb.default_config(), user_config)
mocks.mongodb = mongodb
mocks.register_mock(mongodb)
elif Mysql.name() == mock:
mysql = Mysql(host, self.is_dev_mode, docker_manager)
mysql.config = common.dict_merge(mysql.default_config(), user_config)
mocks.mysql = mysql
mocks.register_mock(mysql)
else:
found_mock = False
for potential_mock in potential_mocks:
if potential_mock.name() == mock:
found_mock = True
potential_mock.config = common.dict_merge(potential_mock.default_config(), user_config)
mocks.register_mock(potential_mock)
if not found_mock:
raise exceptions.MockNotSupportedException(
f'{mock} is not a supported mock. Please check your touchstone.yml file.')
f'"{mock}" is not a supported mock. Please check your touchstone.yml file.')
return mocks

def __build_services(self, touchstone_config, docker_manager, mocks) -> Services:
Expand Down
2 changes: 1 addition & 1 deletion src/touchstone/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ def execute():
"""
file.writelines(data)
open('touchstone/defaults/.gitkeep', 'a').close()
print('Touchstone has been initialized.')
print('Touchstone has been initialized in the current directory.')
19 changes: 9 additions & 10 deletions src/touchstone/lib/mocks/mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from typing import List

import yaml

from touchstone import common
from touchstone.lib import exceptions
from touchstone.lib.mocks.http.http import Http
Expand All @@ -21,38 +20,38 @@ def __init__(self, root: str):
self.mongodb: Mongodb = None
self.mysql: Mysql = None
self.__root = root
self.__mocks: List[Mock] = []
self.__registered_mocks: List[Mock] = []
self.__mocks_running = False

def register_mock(self, mock: Mock):
self.__mocks.append(mock)
self.__registered_mocks.append(mock)

def start(self) -> List[RunContext]:
if self.__mocks_running:
print('Mocks have already been started. They cannot be started again.')
else:
print(f'Starting mocks {[_.pretty_name() for _ in self.__mocks]}...')
print(f'Starting mocks {[_.pretty_name() for _ in self.__registered_mocks]}...')
run_contexts = []
for mock in self.__mocks:
for mock in self.__registered_mocks:
run_contexts.append(mock.start())
self.__wait_for_healthy_mocks()
for mock in self.__mocks:
for mock in self.__registered_mocks:
mock.initialize()
self.__mocks_running = True
print('Finished starting mocks.\n')
return run_contexts

def stop(self):
print('Stopping mocks...')
for mock in self.__mocks:
for mock in self.__registered_mocks:
mock.stop()
self.__mocks_running = False

def are_running(self):
return self.__mocks_running

def load_defaults(self):
for mock in self.__mocks:
for mock in self.__registered_mocks:
try:
with open(os.path.join(self.__root, f'defaults/{mock.name()}.yml'), 'r') as file:
defaults = yaml.safe_load(file)
Expand All @@ -61,11 +60,11 @@ def load_defaults(self):
pass

def print_available_mocks(self):
for mock in self.__mocks:
for mock in self.__registered_mocks:
print(f'Mock {mock.pretty_name()} UI running at: {mock.network.ui_url()}')

def __wait_for_healthy_mocks(self):
for mock in self.__mocks:
for mock in self.__registered_mocks:
attempt = 0
healthy = False
while not healthy and attempt is not 10:
Expand Down

0 comments on commit 583bec6

Please sign in to comment.