Skip to content

Commit

Permalink
Merge pull request #54 from shanejansen/v1.4.2
Browse files Browse the repository at this point in the history
V1.4.2
  • Loading branch information
shanejansen committed Dec 6, 2022
2 parents 5d955a7 + 3fd62bf commit d48c652
Show file tree
Hide file tree
Showing 102 changed files with 996 additions and 884 deletions.
21 changes: 0 additions & 21 deletions Pipfile

This file was deleted.

268 changes: 0 additions & 268 deletions Pipfile.lock

This file was deleted.

64 changes: 31 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Touchstone
======
![Unit Tests](https://github.com/shane-jansen/touchstone/workflows/Unit%20Tests/badge.svg?branch=develop)
![Touchstone Tests](https://github.com/shane-jansen/touchstone/workflows/Touchstone%20Tests/badge.svg?branch=develop)
![Unit Tests](https://github.com/shane-jansen/touchstone/workflows/Unit%20Tests/badge.svg?branch=master)
![Touchstone Tests](https://github.com/shane-jansen/touchstone/workflows/Touchstone%20Tests/badge.svg?branch=master)

Touchstone is a testing framework for your services that focuses on [component](https://martinfowler.com/articles/microservice-testing/#testing-component-out-of-process-diagram), [end-to-end](https://martinfowler.com/articles/microservice-testing/#testing-end-to-end-introduction), and [exploratory](https://martinfowler.com/bliki/ExploratoryTesting.html) testing.

Expand All @@ -10,7 +10,7 @@ Touchstone is a testing framework for your services that focuses on [component](
![Testing Pyramid](./docs/images/testing-pyramid.png)
[Image Credit](https://martinfowler.com/articles/microservice-testing/#conclusion-test-pyramid)

Touchstone aims to simplify the top three pieces of the testing pyramid by providing mock implementations of common service dependencies and exposing them via an easy to use testing framework. Whether your app is written in Java, Python, Go, C#, [Fortran](https://www.fortran.io/), or any other language, Touchstone handles its dependencies while you focus on writing tests. Not a single line of component or end-to-end testing code needs to change should you decide to refactor or rewrite your service.
Touchstone aims to simplify the top three pieces of the testing pyramid by providing real implementations of common service dependencies and exposing them via an easy to use testing framework. Whether your app is written in Java, Python, Go, C#, [Fortran](https://www.fortran.io/), or any other language, Touchstone handles its dependencies while you focus on writing tests. Not a single line of component or end-to-end testing code needs to change should you decide to refactor or rewrite your service.


## Use Case
Expand All @@ -21,7 +21,7 @@ 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 component and end-to-end tests for all of the above requirements independent of the language/framework used. For example, we can write a component 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.
With Touchstone, it is possible to write component and end-to-end tests for all of the above requirements independent of the language/framework used. For example, we can write a component 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 real 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 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/test_user.py) and [order messaging requirements](./examples/java-spring/touchstone/tests/test_order.py).

Expand All @@ -38,8 +38,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 would run your Touchstone 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 Touchstone tests or the services themselves. This will also provide a web interface to each mock dependency for additional debugging. Mocked dependencies can be altered or reset on the fly to make exploratory testing easier.
* `touchstone run` - Run all Touchstone tests and exit. This is typically how you would run your Touchstone 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 [dependency docs](#dependencies) 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 Touchstone tests or the services themselves. This will also provide a web interface to each dependency for additional debugging. Dependencies can be altered or reset on the fly to make exploratory testing easier.

Touchstone has the following options:
* `--log={LEVEL}` - Sets Touchstone's log level.
Expand All @@ -61,23 +61,23 @@ Your services and their monitored dependencies are defined here. Default values
* `availability_endpoint:` - Default: N/A. By default, Touchstone runs a Docker health check to determine the services' health. Supply this value to use URL based health checking. A HTTP status `2xx` must be returned from the endpoint to be considered healthy.
* `num_retries:` - Default: 20. The number of times Touchstone will try to successfully call the `availability_endpoint`.
* `seconds_between_retries:` - Default: 5. The number of seconds between each retry.
* `mocks:` - Each mock dependency your service(s) are being tested against.
* This is how Touchstone determines which mocked dependencies should be started.
* View each [mock's docs](#mocks) for values and additional configuration.
* `dependencies:` - Each dependency your service(s) are being tested against.
* This is how Touchstone determines which dependencies should be started.
* View each [dependency's docs](#dependencies) for values and additional configuration.

### `/defaults`
[Example](./examples/java-spring/touchstone/defaults/mysql.yml)
This directory contains YAML files where default values for mocked dependencies are defined. Defaults make it easy to test your service(s) locally by setting up your mock dependencies with sensible defaults. The name of each YAML file should match the name of a mock. For instance, with the MySQL mock, a `mysql.yml` file would contain default databases and tables to be created as well as statements to insert initial data. View each [mock's docs](#mocks) for allowable values.
This directory contains YAML files where default values for dependencies are defined. Defaults make it easy to test your service(s) locally by setting up your dependencies with sensible defaults. The name of each YAML file should match the name of a dependency. For instance, with the MySQL dependency, a `mysql.yml` file would contain default databases and tables to be created as well as statements to insert initial data. View each [dependency's docs](#dependencies) for allowable values.


### `/tests`
[Example](./examples/java-spring/touchstone/tests)
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 user document into a Mongo DB collection, send a "PUT" request to our service with an updated email address, and then verify the updated document exists:
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 dependencies to setup and then verify your requirements. For example, we can insert a user document into a Mongo DB collection, send a "PUT" request to our service with an updated email address, and then verify the updated document exists:
```python
class UpdateUser(TouchstoneTest):
def given(self) -> object:
self.mocks.mongodb.setup().insert_document('my_db', 'users', {'name': 'Foo', 'email': 'bar@example.com'})
self.deps.mongodb.setup().insert_document('my_db', 'users', {'name': 'Foo', 'email': 'bar@example.com'})
user_update = {'name': 'Foo', 'email': 'foo@example.com'}
return user_update # user_update is passed to "when" and "then" for reference

Expand All @@ -86,12 +86,12 @@ class UpdateUser(TouchstoneTest):
return result # The response from our service could be returned here for additional validation in "then"

def then(self, given, result) -> bool:
return self.mocks.mongodb.verify().document_exists('my_db', 'users', given)
return self.deps.mongodb.verify().document_exists('my_db', 'users', given)
```

Important APIs:

* `self.mocks` - Hook into Touchstone managed mock dependencies.
* `self.deps` - Hook into Touchstone managed dependencies.
* `self.service_url` - The service under test's URL. Useful for calling RESTful endpoints on the service under test.
* `touchstone.helpers.validation` - Contains methods for easily validating test results. `validation.ANY` can be used to
accept any value which is useful when the expected value is unknown. This only works when validating dicts or JSON.
Expand All @@ -104,25 +104,23 @@ When writing E2E tests, it is often needed for services to communicate with each
in `touchstone run` mode, use the name + port of the desired service specified in your `touchstone.yml` file. For
example, foo-app might make a call to `bar-app:8080/some-endpoint`.

## Mocks

* [HTTP](./docs/mocks/http.md)
* [Mongo DB](./docs/mocks/mongodb.md)
* [MySQL](./docs/mocks/mysql.md)
* [Rabbit MQ](./docs/mocks/rabbitmq.md)
* [S3](./docs/mocks/s3.md)
* [Filesystem](./docs/mocks/filesystem.md)
* [Redis](./docs/mocks/redis.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 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.
* `TS_{MOCK_NAME}_PORT` - Port where the mock is running.
* `TS_{MOCK_NAME}_URL` - Complete URL where the mock is running.
* `TS_{MOCK_NAME}_USERNAME` - Username for authenticating with the mock.
* `TS_{MOCK_NAME}_PASSWORD` - Password for authenticating with the mock.
## Dependencies

* [HTTP](./docs/dependencies/http.md)
* [Mongo DB](./docs/dependencies/mongodb.md)
* [MySQL](./docs/dependencies/mysql.md)
* [Rabbit MQ](./docs/dependencies/rabbitmq.md)
* [S3](./docs/dependencies/s3.md)
* [Filesystem](./docs/dependencies/filesystem.md)
* [Redis](./docs/dependencies/redis.md)
* [Add one!](./docs/add-dependency.md)

When running via `touchstone develop`, dev ports for each dependency are used. When running touchstone via `touchstone run`, ports are automatically discovered and available to your service containers via the following environment variables:
* `TS_{DEP_NAME}_HOST` - Host where the dependency is running.
* `TS_{DEP_NAME}_PORT` - Port where the dependency is running.
* `TS_{DEP_NAME}_URL` - Complete URL where the dependency is running.
* `TS_{DEP_NAME}_USERNAME` - Username for authenticating with the dependency.
* `TS_{DEP_NAME}_PASSWORD` - Password for authenticating with the dependency.

## Testing Executables
Touchstone can also be used to test non-service based applications. This includes applications that can be invoked via the command line, like Spark jobs, for example. A Python Spark job tested with Touchstone can be found [here](./examples/python-spark).
Expand Down
6 changes: 6 additions & 0 deletions docs/add-dependency.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Contribute a New Dependency
======
1. Create your dependency
1. Add a new property to the [Dependencies](../touchstone/lib/nodes/deps/deps.py) class, so your new dependency is accessible in user test cases
1. Build a concrete instance of your new dependency in the [Dependency Factory](../touchstone/lib/nodes/deps/dep_factory.py) with its required dependencies
1. Write [unit](../tests) and [Touchstone tests](../touchstone-tests) for your new dependency
6 changes: 0 additions & 6 deletions docs/add-mock.md

This file was deleted.

10 changes: 5 additions & 5 deletions docs/mocks/filesystem.md → docs/dependencies/filesystem.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Filesystem
======
Used to mock a filesystem for verifying file creation and contents.
Used to start a filesystem for verifying file creation and contents.

All filesystem I/O must be handled in the `touchstone/io` directory. This allows service containers to access the directory on your local machine using a bind mount. All files used in validation, whether created via a test, or the service itself, should be written to this directory.
Note: All paths used in the "setup" and "verify" APIs use the `touchstone/io` directory as a base path.
Expand All @@ -15,22 +15,22 @@ N/A


## Defaults Example
A YAML file is not supplied for the filesystem mock. A directory with name `filesystem` is used instead. All files in this directory are copied to `touchstone/io` before each test.
A YAML file is not supplied for the filesystem dependency. A directory with name `filesystem` is used instead. All files in this directory are copied to `touchstone/io` before each test.
[Example](../../examples/python-spark/touchstone/defaults/filesystem)


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

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

If you are performing filesystem operations in your test code, you must join with `get_io_path` when referring to file paths. This returns the path to the `touchstone/io` folder. For example:
```python
path = os.path.join(self.mocks.filesystem.get_io_path(), 'foo.csv')
path = os.path.join(self.deps.filesystem.get_io_path(), 'foo.csv')
with open(path, 'rb') as data:
return bytes(data.read())
```
6 changes: 3 additions & 3 deletions docs/mocks/http.md → docs/dependencies/http.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
HTTP
======
Used to mock a HTTP dependency.
Used to start a HTTP dependency.


## Specs
Expand Down 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.deps.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.deps.http.verify().get_called('/some-endpoint')
```
6 changes: 3 additions & 3 deletions docs/mocks/mongodb.md → docs/dependencies/mongodb.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Mongo DB
======
Used to mock a Mongo DB dependency.
Used to start a Mongo DB dependency.


## Specs
Expand Down 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.deps.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.deps.mongodb.verify().document_exists('my_db', 'my_collection', {'foo': 'bar'})
```
10 changes: 5 additions & 5 deletions docs/mocks/mysql.md → docs/dependencies/mysql.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MySQL
======
Used to mock a MySQL dependency.
Used to start a MySQL dependency.


## Specs
Expand All @@ -11,8 +11,8 @@ Used to mock a MySQL dependency.


## Configuration
* `camel_to_snake` - Default: True. When supplying a dict to this mock's API, this value determines if the dict keys should be converted to snake case to match MySQL's convention.
* `snapshot_databases` - Default: False. When set to True, a snapshot of the database(s) specified in the defaults file is taken when the services become healthy. This snapshot is then used during each reset the MySql mock. This can be useful if your app uses a database migration tool (e.g. Flyway) where some initial data is inserted by the apps themselves.
* `camel_to_snake` - Default: True. When supplying a dict to this dependency's API, this value determines if the dict keys should be converted to snake case to match MySQL's convention.
* `snapshot_databases` - Default: False. When set to True, a snapshot of the database(s) specified in the defaults file is taken when the services become healthy. This snapshot is then used during each reset of the dependency. This can be useful if your app uses a database migration tool (e.g. Flyway) where some initial data is inserted by the apps themselves.


## Defaults Example
Expand All @@ -30,8 +30,8 @@ databases:
## Usage Example
```python
# Insert a row into a table
self.mocks.mysql.setup().insert_row('my_db', 'my_table', {'foo': 'bar'})
self.deps.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.deps.mysql.verify().row_exists('my_db', 'my_table', {'foo': 'bar'})
```

0 comments on commit d48c652

Please sign in to comment.