Skip to content

Commit

Permalink
Merge pull request #11 from shanejansen/release/v1.1.0
Browse files Browse the repository at this point in the history
Release/v1.1.0
  • Loading branch information
shanejansen committed May 29, 2020
2 parents 18fc719 + 7747c3f commit 581f392
Show file tree
Hide file tree
Showing 111 changed files with 1,813 additions and 1,129 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ This directory contains YAML files where default values for mocked dependencies
This directory is the default location for your Touchstone 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 prefixed with `test_` containing classes that extend `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 APIs:
```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'})
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.
Expand All @@ -84,9 +84,10 @@ Important APIs:
* [MySQL](./docs/mocks/mysql.md)
* [Rabbit MQ](./docs/mocks/rabbitmq.md)
* [S3](./docs/mocks/s3.md)
* [Filesystem](./docs/mocks/filesystem.md)
* [Add one!](./docs/add-mock.md)

If a specific mock is not supported, consider building your service independent of the implementation layer. For example, if have a dependency on PostgreSQL, use the MySQL mock as your database implementation during testing.
If a specific mock is not supported, consider building your service independent of the implementation layer. For example, if you have a dependency on PostgreSQL, use the MySQL mock as your database implementation during testing.

When running via `touchstone develop`, dev ports for each mock are used. When running touchstone via `touchstone run`, ports are automatically discovered and available to your service containers via the following environment variables:
* `TS_{MOCK_NAME}_HOST` - Host where the mock is running.
Expand Down
5 changes: 2 additions & 3 deletions docs/add-mock.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
Contribute a New Mock
======
1. Create a new directory in [touchstone/lib/mocks](../touchstone/lib/mocks)
1. Create a new class extending [Mock](../touchstone/lib/mocks/mock.py)
1. Create a new [Runnable](../touchstone/lib/mocks/runnables) or [Networked Runnable](../touchstone/lib/mocks/networked_runnables)
1. Add a new property to the [Mocks](../touchstone/lib/mocks/mocks.py) class, so your new mock is accessible in user test cases
1. Build a concrete instance of your new mock in the [Bootstrap](../touchstone/bootstrap.py) `__build_mocks` method with its required dependencies
1. Build a concrete instance of your new mock in the [Mock Factory](../touchstone/lib/mocks/mock_factory.py) with its required dependencies
1. Write [unit](../tests) and [Touchstone tests](../touchstone-tests) for your new mock
44 changes: 44 additions & 0 deletions docs/mocks/filesystem.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
Filesystem
======
Used to mock a filesystem for verifying file creation and contents.

Note: All paths used in the "setup" and "verify" APIs use the "defaults" directory as a base path.


## Specs
* Name: filesystem


## Configuration
N/A


## Defaults Example
```yaml
---
directories:
- path: ./filesystem/some-dir
files:
- ./filesystem/foo.csv
- ./filesystem/bar.png
- path: ./filesystem/some-dir/sub-dir
files:
- ./filesystem/foo.csv
```


## Usage Example
```python
# Verify a file exists in a directory
result: bool = self.mocks.filesystem.verify().file_exists('./filesystem/some-dir/foo.csv')

# Verify a file's content matches as expected
result: bool = self.mocks.filesystem.verify().file_matches('./filesystem/some-dir/foo.csv', given)
```

If you are performing filesystem operations in your test code, you must join with `get_base_path` when referring to file paths. This returns the path to the "defaults" folder. For example:
```python
path = os.path.join(self.mocks.filesystem.get_base_path(), './filesystem/foo.csv')
with open(path, 'rb') as data:
return bytes(data.read())
```
4 changes: 2 additions & 2 deletions docs/mocks/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ requests:
## Usage Example
```python
# Return JSON when a GET request is made to an endpoint
self.mocks.http.setup.get('/some-endpoint', {'foo': 'bar'})
self.mocks.http.setup().get('/some-endpoint', {'foo': 'bar'})

# Verify that an endpoint was called
result: bool = self.mocks.http.verify.get_called('/some-endpoint')
result: bool = self.mocks.http.verify().get_called('/some-endpoint')
```
4 changes: 2 additions & 2 deletions docs/mocks/mongodb.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ databases:
## Usage Example
```python
# Insert a document into a collection
self.mocks.mongodb.setup.insert_document('my_db', 'my_collection', {'foo': 'bar'})
self.mocks.mongodb.setup().insert_document('my_db', 'my_collection', {'foo': 'bar'})

# Verify that a document exists in a collection
result: bool = self.mocks.mongodb.verify.document_exists('my_db', 'my_collection', {'foo': 'bar'})
result: bool = self.mocks.mongodb.verify().document_exists('my_db', 'my_collection', {'foo': 'bar'})
```
4 changes: 2 additions & 2 deletions docs/mocks/mysql.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ databases:
## Usage Example
```python
# Insert a row into a table
self.mocks.mysql.setup.insert_row('my_db', 'my_table', {'foo': 'bar'})
self.mocks.mysql.setup().insert_row('my_db', 'my_table', {'foo': 'bar'})

# Verify that a row exists in a table
result: bool = self.mocks.mysql.verify.row_exists('my_db', 'my_table', {'foo': 'bar'})
result: bool = self.mocks.mysql.verify().row_exists('my_db', 'my_table', {'foo': 'bar'})
```
6 changes: 3 additions & 3 deletions docs/mocks/rabbitmq.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ exchanges:
## Usage Example
```python
# Publish a message to an exchange
self.mocks.rabbitmq.setup.publish('default-direct.exchange', 'some payload', routing_key='foo')
self.mocks.rabbitmq.setup().publish('default-direct.exchange', 'some payload', routing_key='foo')

# Verify that a certain number of messages were published to an exchange and routing key
result: bool = self.mocks.rabbitmq.verify.messages_published('default-direct.exchange', num_expected=3, routing_key='foo')
result: bool = self.mocks.rabbitmq.verify().messages_published('default-direct.exchange', num_expected=3, routing_key='foo')

# Verify that a payload was published to an exchange and routing key
result: bool = self.mocks.rabbitmq.verify.payload_published('default-topic.exchange', 'some payload', routing_key='foo')
result: bool = self.mocks.rabbitmq.verify().payload_published('default-topic.exchange', 'some payload', routing_key='foo')
```
17 changes: 12 additions & 5 deletions docs/mocks/s3.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,28 @@ buckets:
objects:
- name: foo.csv
content-type: text/csv
path: ./s3-objects/foo.csv
path: ./s3/foo.csv
- name: test/bar.png
content-type: image/png
path: ./s3-objects/bar.png
path: ./s3/bar.png
```


## Usage Example
```python
# Create a bucket
self.mocks.s3.setup.create_bucket('bucket_name')
self.mocks.s3.setup().create_bucket('bucket_name')

# Put an object in a bucket
self.mocks.s3.setup.put_object('bucket_name', 'object_name', data)
self.mocks.s3.setup().put_object('bucket_name', 'object_name', data)

# Verify an object exists in a bucket
result: bool = self.mocks.s3.verify.object_exists('bucket_name', 'object_name')
result: bool = self.mocks.s3.verify().object_exists('bucket_name', 'object_name')
```

If you are performing filesystem operations in your test code, you must join with `get_base_path` when referring to file paths. This returns the path to the "defaults" folder. For example:
```python
path = os.path.join(self.mocks.s3.get_base_path(), './s3/foo.csv')
with open(path, 'rb') as data:
return bytes(data.read())
```
4 changes: 2 additions & 2 deletions examples/java-spring/touchstone/tests/test_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ def given(self) -> object:
}

def when(self, given) -> object:
self.mocks.rabbitmq.setup.publish('order-placed.exchange', json.dumps(given))
self.mocks.rabbitmq.setup().publish('order-placed.exchange', json.dumps(given))
return None

def then(self, given, result) -> bool:
return self.mocks.mongodb.verify.document_exists(creds.MONGO_DATABASE, creds.MONGO_COLLECTION, given)
return self.mocks.mongodb.verify().document_exists(creds.MONGO_DATABASE, creds.MONGO_COLLECTION, given)
18 changes: 9 additions & 9 deletions examples/java-spring/touchstone/tests/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def given(self) -> object:
'lastName': 'Brown',
'email': 'jane789@example.com'
}
self.mocks.mysql.setup.insert_row(creds.MYSQL_DATABASE, creds.MYSQL_TABLE, given)
self.mocks.mysql.setup().insert_row(creds.MYSQL_DATABASE, creds.MYSQL_TABLE, given)
return given

def when(self, given) -> object:
Expand All @@ -48,7 +48,7 @@ def given(self) -> object:
'lastName': 'Brown',
'email': 'jane789@example.com'
}
self.mocks.http.setup.get('/jane-brown/email', given['email'])
self.mocks.http.setup().get('/jane-brown/email', given['email'])
return given

def when(self, given) -> object:
Expand All @@ -66,7 +66,7 @@ def then(self, given, result) -> bool:
expected_response = given.copy()
expected_response['id'] = validation.ANY
expected_row = given.copy()
return validation.matches(expected_response, result) and self.mocks.mysql.verify.row_exists(
return validation.matches(expected_response, result) and self.mocks.mysql.verify().row_exists(
creds.MYSQL_DATABASE,
creds.MYSQL_TABLE,
expected_row)
Expand All @@ -93,7 +93,7 @@ def given(self) -> object:
'lastName': 'Brown',
'email': 'jane789@example.com'
}
self.mocks.mysql.setup.insert_row(creds.MYSQL_DATABASE, creds.MYSQL_TABLE, existing_user)
self.mocks.mysql.setup().insert_row(creds.MYSQL_DATABASE, creds.MYSQL_TABLE, existing_user)
return new_info

def when(self, given) -> object:
Expand All @@ -104,7 +104,7 @@ def when(self, given) -> object:
return None

def then(self, given, result) -> bool:
return self.mocks.mysql.verify.row_exists(creds.MYSQL_DATABASE, creds.MYSQL_TABLE, given)
return self.mocks.mysql.verify().row_exists(creds.MYSQL_DATABASE, creds.MYSQL_TABLE, given)


class DeleteUser(TouchstoneTest):
Expand All @@ -128,7 +128,7 @@ def given(self) -> object:
'lastName': 'Brown',
'email': 'jane789@example.com'
}
self.mocks.mysql.setup.insert_row(creds.MYSQL_DATABASE, creds.MYSQL_TABLE, user)
self.mocks.mysql.setup().insert_row(creds.MYSQL_DATABASE, creds.MYSQL_TABLE, user)
return user_id

def when(self, given) -> object:
Expand All @@ -140,6 +140,6 @@ def then(self, given, result) -> bool:
where = {
'id': given
}
return self.mocks.rabbitmq.verify.payload_published('user.exchange', str(given),
routing_key='user-deleted') and \
self.mocks.mysql.verify.row_does_not_exist(creds.MYSQL_DATABASE, creds.MYSQL_TABLE, where)
return self.mocks.rabbitmq.verify().payload_published('user.exchange', str(given),
routing_key='user-deleted') and \
self.mocks.mysql.verify().row_does_not_exist(creds.MYSQL_DATABASE, creds.MYSQL_TABLE, where)
1 change: 1 addition & 0 deletions examples/java-spring/touchstone/touchstone.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---
touchstone-version: 1.1.0
services:
- name: my-app
availability_endpoint: "/actuator/health"
Expand Down
6 changes: 0 additions & 6 deletions tests/lib/mocks/test_mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,6 @@ def setUp(self) -> None:
self.mock_mongodb = Mock()

self.mocks = Mocks()
self.mocks.http = self.mock_http
self.mocks.register_mock(self.mock_http)
self.mocks.rabbitmq = self.mock_rabbitmq
self.mocks.register_mock(self.mock_rabbitmq)
self.mocks.mongodb = self.mock_mongodb
self.mocks.register_mock(self.mock_mongodb)

def test_start_mocksNotRunning_mocksAreRunning(self):
# When
Expand Down
6 changes: 3 additions & 3 deletions tests/lib/test_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from unittest import TestCase
from unittest.mock import Mock

from touchstone.lib.service import Service
from touchstone.lib.services.networked_service import NetworkedService


class TestService(TestCase):
Expand All @@ -12,7 +12,7 @@ def setUp(self) -> None:

def test_start_dockerfileNotSet_serviceNotStarted(self):
# Given
service = Service('', '', self.mock_tests, None, '', 0, '', 0, 0, self.mock_docker_manager)
service = NetworkedService('', self.mock_tests, None, self.mock_docker_manager, '', '', '', 0, 0)

# When
service.start([])
Expand All @@ -21,7 +21,7 @@ def test_start_dockerfileNotSet_serviceNotStarted(self):
self.mock_docker_manager.run_image.assert_not_called()

def test_stop_serviceIsRunning_serviceStops(self):
service = Service('', '', self.mock_tests, '', '', 0, '', 0, 0, self.mock_docker_manager)
service = NetworkedService('', self.mock_tests, '', self.mock_docker_manager, '', '', '', 0, 0)

# When
service.start([])
Expand Down
8 changes: 5 additions & 3 deletions tests/lib/test_services.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import unittest
from unittest import TestCase
from unittest.mock import Mock
from unittest.mock import Mock, MagicMock

from touchstone.lib.services import Services
from touchstone.lib.services.i_testable import ITestable
from touchstone.lib.services.services import Services


class TestServices(TestCase):
Expand Down Expand Up @@ -41,7 +42,8 @@ def test_runTests_allTestsPass_ReturnsTrue(self):

def test_runTests_testFails_ReturnsFalse(self):
# Given
mock_service = Mock()
MagicMock()
mock_service = Mock(spec=ITestable)
mock_service.run_all_tests.return_value = False
services = Services([mock_service])

Expand Down
26 changes: 1 addition & 25 deletions tests/test_common.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,10 @@
import unittest
from unittest import TestCase, mock
from unittest import TestCase

from touchstone import common


class TestCommon(TestCase):
@mock.patch('touchstone.common.os')
def test_sanityCheckPasses_requirementsMet_ReturnsTrue(self, mock_os):
# Given
mock_os.getcwd.return_value = 'temp'
mock_os.path.exists.return_value = True

# When
result = common.sanity_check_passes()

# Then
self.assertTrue(result)

@mock.patch('touchstone.common.os')
def test_sanityCheckPasses_requirementsNotMet_ReturnsFalse(self, mock_os):
# Given
mock_os.getcwd.return_value = 'temp'
mock_os.path.exists.return_value = False

# When
result = common.sanity_check_passes()

# Then
self.assertFalse(result)

def test_dictMerge_emptyOverride_ReturnsBase(self):
# Given
base = {'foo': 'bar'}
Expand Down
9 changes: 9 additions & 0 deletions touchstone-tests/touchstone/defaults/filesystem.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
directories:
- path: ./filesystem/some-dir
files:
- ./filesystem/foo.csv
- ./filesystem/bar.png
- path: ./filesystem/some-dir/sub-dir
files:
- ./filesystem/foo.csv
4 changes: 2 additions & 2 deletions touchstone-tests/touchstone/defaults/s3.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ buckets:
objects:
- name: foo.csv
content-type: text/csv
path: ./s3-objects/foo.csv
path: ./s3/foo.csv
- name: test/bar.png
content-type: image/png
path: ./s3-objects/bar.png
path: ./s3/bar.png
Binary file added touchstone-tests/touchstone/defaults/s3/bar.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions touchstone-tests/touchstone/defaults/s3/foo.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
first name,last name
John ,Smith
Jane ,Brown
27 changes: 27 additions & 0 deletions touchstone-tests/touchstone/tests/test_filesystem.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import os

from touchstone.lib.touchstone_test import TouchstoneTest


class FileExists(TouchstoneTest):
def given(self) -> object:
return './filesystem/some-dir/foo.csv'

def when(self, given) -> object:
return None

def then(self, given, result) -> bool:
return self.mocks.filesystem.verify().file_exists(given)


class FileMatches(TouchstoneTest):
def given(self) -> object:
path = os.path.join(self.mocks.filesystem.get_base_path(), './filesystem/some-dir/sub-dir/foo.csv')
with open(path, 'rb') as data:
return bytes(data.read())

def when(self, given) -> object:
return None

def then(self, given, result) -> bool:
return self.mocks.filesystem.verify().file_matches('./filesystem/some-dir/sub-dir/foo.csv', given)

0 comments on commit 581f392

Please sign in to comment.