shanejansen / touchstone

Touchstone is a testing framework for your services that focuses on component, end-to-end, and exploratory testing.
16 stars 2 forks source link
component-testing end-to-end-testing exploratory-testing mock mocking test-automation testing-pyramid

Touchstone

Unit Tests Touchstone Tests

Touchstone is a testing framework for your services that focuses on component, end-to-end, and exploratory testing.

Introduction

Testing Pyramid
Image Credit

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, 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

Let's say we are building a microservice that is responsible for managing users. The service exposes a REST API and the requirements for each endpoint are as follows:

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 and order messaging requirements.

Installation

pip install touchstone-testing

Requirements:

Usage

After installation, Touchstone will be available via touchstone in your terminal.
Touchstone has three basic commands:

Touchstone has the following options:

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

/touchstone.yml

Example
Your services and their monitored dependencies are defined here. Default values should be enough in most cases.

/defaults

Example
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 for allowable values.

/tests

Example
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 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:

class UpdateUser(TouchstoneTest):
    def given(self) -> object:
        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

    def when(self, given) -> object:
        result = http.put_json(f'{self.service_url}/user', given)
        return result # The response from our service could be returned here for additional validation in "then"

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

Important APIs:

Referencing Services

When writing E2E tests, it is often needed for services to communicate with each other via HTTP. When running 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.

Dependencies

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:

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.

To define a service as executable, set its type to executable in your touchstone.yml. The supplied Dockerfile or Docker image will be ran as the executable service. You can also supply a develop_command which will be used when running Touchstone in develop mode. Example

In your Touchstone tests, the executable service can be triggered using the following API:

def when(self, given) -> object:
    self.service_executor.execute(SERVICE_NAME)
    return None

Example

Docker-In-Docker

Some CI pipelines utilize Docker-in-Docker (DinD) to run tests. If Touchstone is running using DinD, the network must point to the host:

docker run --network="host" -v /var/run/docker.sock:/var/run/docker.sock your-touchstone-image