Skip to content

Commit

Permalink
Added Redis mock. Closes #30
Browse files Browse the repository at this point in the history
  • Loading branch information
shanejansen committed Sep 30, 2021
1 parent b235130 commit 3ce951e
Show file tree
Hide file tree
Showing 15 changed files with 409 additions and 88 deletions.
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pyyaml = "~=5.4"
pymongo = "~=3.10"
pymysql = "~=0.9.3"
minio = "~=5.0.7"
redis = "~=3.5.3"
touchstone-testing = {editable = true,path = "."}

[requires]
Expand Down
233 changes: 146 additions & 87 deletions Pipfile.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ example, foo-app might make a call to `bar-app:8080/some-endpoint`.
* [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.
Expand Down
35 changes: 35 additions & 0 deletions docs/mocks/redis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
Redis
======
Used to mock a Redis data store dependency.

## Specs

* Name: redis
* Dev Port: 6379
* Password:

## Configuration

N/A

## Defaults Example

```yaml
---
objects:
foo: bar
baz: buzz
```
## Usage Example
```python
# Set a value
self.mocks.redis.setup().set('foo')

# Get a value
self.mocks.redis.verify().get('foo')

# Get a JSON value as dict
result: dict = self.mocks.redis.verify().get_json('foo')
```
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
'pyyaml~=5.4',
'pymongo~=3.10',
'pymysql~=0.9.3',
'minio~=5.0.7'
'minio~=5.0.7',
'redis~=3.5.3'
],
entry_points={
'console_scripts': [
Expand Down
4 changes: 4 additions & 0 deletions touchstone-tests/touchstone/defaults/redis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
objects:
foo: bar
baz: buzz
51 changes: 51 additions & 0 deletions touchstone-tests/touchstone/tests/test_redis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import json

from touchstone.lib.touchstone_test import TouchstoneTest


class Set(TouchstoneTest):
def given(self) -> object:
pass

def when(self, given) -> object:
self.mocks.redis.setup().set('foo', 'bar')
return None

def then(self, given, result) -> bool:
return self.mocks.redis.verify().value_matches('foo', 'bar')


class ValueExists(TouchstoneTest):
def given(self) -> object:
pass

def when(self, given) -> object:
self.mocks.redis.setup().set('foo', 'bar')
return None

def then(self, given, result) -> bool:
return self.mocks.redis.verify().value_exists('foo')


class ValueMatches(TouchstoneTest):
def given(self) -> object:
pass

def when(self, given) -> object:
self.mocks.redis.setup().set('foo', 'bar')
return None

def then(self, given, result) -> bool:
return self.mocks.redis.verify().value_matches('foo', 'bar')


class ValueMatchesJson(TouchstoneTest):
def given(self) -> object:
return {'foo': 'bar'}

def when(self, given) -> object:
self.mocks.redis.setup().set('foo', json.dumps(given))
return None

def then(self, given, result) -> bool:
return self.mocks.redis.verify().value_matches_json('foo', given)
1 change: 1 addition & 0 deletions touchstone-tests/touchstone/touchstone.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ mocks:
mysql:
s3:
filesystem:
redis:
35 changes: 35 additions & 0 deletions touchstone/lib/nodes/mocks/behaviors/i_redis_behavior.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import abc

from touchstone.lib.nodes.mocks.behaviors.i_behavior import IBehavior


class IRedisSetup(object):
@abc.abstractmethod
def set(self, key: str, value: str):
"""Sets a key-value pair."""
pass


class IRedisVerify(object):
@abc.abstractmethod
def value_exists(self, key: str) -> bool:
"""Returns True if the given key exists."""
pass

@abc.abstractmethod
def value_matches(self, key: str, value: str) -> bool:
"""Returns True if the given key matches the given value."""

@abc.abstractmethod
def value_matches_json(self, key: str, value: dict) -> bool:
"""Returns True if the given key matches the given JSON value."""


class IRedisBehavior(IBehavior):
@abc.abstractmethod
def setup(self) -> IRedisSetup:
pass

@abc.abstractmethod
def verify(self) -> IRedisVerify:
pass
Empty file.
75 changes: 75 additions & 0 deletions touchstone/lib/nodes/mocks/docker/redis/docker_redis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from redis import Redis

from touchstone.lib import exceptions
from touchstone.lib.configurers.i_configurable import IConfigurable
from touchstone.lib.managers.docker_manager import DockerManager
from touchstone.lib.networking.docker_network import DockerNetwork
from touchstone.lib.networking.i_network import INetwork
from touchstone.lib.nodes.mocks.behaviors.i_behavior import IBehavior
from touchstone.lib.nodes.mocks.behaviors.i_redis_behavior import IRedisBehavior, IRedisSetup, IRedisVerify
from touchstone.lib.nodes.mocks.docker.i_runnable_docker import IRunnableDocker
from touchstone.lib.nodes.mocks.docker.redis.docker_redis_setup import DockerRedisSetup
from touchstone.lib.nodes.mocks.docker.redis.docker_redis_verify import DockerRedisVerify


class DockerRedis(IRunnableDocker, IRedisBehavior):
def __init__(self, defaults_configurer: IConfigurable, is_dev_mode: bool, setup: DockerRedisSetup,
verify: DockerRedisVerify, docker_manager: DockerManager, docker_network: DockerNetwork):
self.__defaults_configurer = defaults_configurer
self.__is_dev_mode = is_dev_mode
self.__setup = setup
self.__verify = verify
self.__docker_manager = docker_manager
self.__docker_network = docker_network
self.__ui_container_id = None

def get_behavior(self) -> IBehavior:
return self

def get_network(self) -> INetwork:
return self.__docker_network

def initialize(self):
redis_client = Redis(self.__docker_network.external_host(), self.__docker_network.external_port())
self.__setup.set_redis_client(redis_client)
self.__verify.set_redis_client(redis_client)
self.__setup.init(self.__defaults_configurer.get_config())

def start(self):
run_result = self.__docker_manager.run_background_image('redis:6.2.5-alpine', port=6379)
self.__docker_network.set_container_id(run_result.container_id)
if self.__is_dev_mode:
ui_run_result = self.__docker_manager.run_background_image(
'rediscommander/redis-commander:redis-commander-210', ui_port=8081,
environment_vars=[('REDIS_HOST', self.__docker_network.internal_host())])
self.__ui_container_id = ui_run_result.container_id
self.__docker_network.set_ui_port(ui_run_result.ui_port)
self.__docker_network.set_internal_port(run_result.internal_port)
self.__docker_network.set_external_port(run_result.external_port)

def stop(self):
if self.__docker_network.container_id():
self.__docker_manager.stop_container(self.__docker_network.container_id())
if self.__ui_container_id:
self.__docker_manager.stop_container(self.__ui_container_id)

def reset(self):
self.__setup.init(self.__defaults_configurer.get_config())

def is_healthy(self) -> bool:
try:
client = Redis(self.__docker_network.external_host(), self.__docker_network.external_port())
client.info()
return True
except Exception:
return False

def setup(self) -> IRedisSetup:
if not self.__setup:
raise exceptions.MockException('Setup unavailable. Mock is still starting.')
return self.__setup

def verify(self) -> IRedisVerify:
if not self.__verify:
raise exceptions.MockException('Verify unavailable. Mock is still starting.')
return self.__verify
22 changes: 22 additions & 0 deletions touchstone/lib/nodes/mocks/docker/redis/docker_redis_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from redis import Redis

from touchstone.lib import exceptions
from touchstone.lib.nodes.mocks.behaviors.i_redis_behavior import IRedisSetup


class DockerRedisSetup(IRedisSetup):
def __init__(self):
self.__redis_client: Redis = None

def set_redis_client(self, redis_client: Redis):
self.__redis_client = redis_client

def init(self, defaults: dict):
self.__redis_client.flushall()
for key, value in defaults.get('objects', []).items():
self.__redis_client.set(key, value)

def set(self, key: str, value: str):
if not isinstance(value, str):
raise exceptions.MockException('Value must be a str.')
return self.__redis_client.set(key, value)
24 changes: 24 additions & 0 deletions touchstone/lib/nodes/mocks/docker/redis/docker_redis_verify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import json

from redis import Redis

from touchstone.helpers import validation
from touchstone.lib.nodes.mocks.behaviors.i_redis_behavior import IRedisVerify


class DockerRedisVerify(IRedisVerify):
def __init__(self):
self.__redis_client: Redis = None

def set_redis_client(self, redis_client: Redis):
self.__redis_client = redis_client

def value_exists(self, key: str) -> bool:
return self.__redis_client.get(key) is not None

def value_matches(self, key: str, value: str) -> bool:
return self.__redis_client.get(key).decode('utf-8') == value

def value_matches_json(self, key: str, value: dict) -> bool:
value_json = json.loads(self.__redis_client.get(key).decode('utf-8'))
return validation.matches(value, value_json)
10 changes: 10 additions & 0 deletions touchstone/lib/nodes/mocks/mock_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
from touchstone.lib.nodes.mocks.docker.rabbitmq.docker_rabbitmq_context import DockerRabbitmqContext
from touchstone.lib.nodes.mocks.docker.rabbitmq.docker_rabbitmq_setup import DockerRabbitmqSetup
from touchstone.lib.nodes.mocks.docker.rabbitmq.docker_rabbitmq_verify import DockerRabbitmqVerify
from touchstone.lib.nodes.mocks.docker.redis.docker_redis import DockerRedis
from touchstone.lib.nodes.mocks.docker.redis.docker_redis_setup import DockerRedisSetup
from touchstone.lib.nodes.mocks.docker.redis.docker_redis_verify import DockerRedisVerify
from touchstone.lib.nodes.mocks.docker.runnable_docker_mock import RunnableDockerMock
from touchstone.lib.nodes.mocks.docker.s3.docker_s3 import DockerS3
from touchstone.lib.nodes.mocks.docker.s3.docker_s3_setup import DockerS3Setup
Expand Down Expand Up @@ -99,4 +102,11 @@ def get_mock(self, mock_name: str) -> Optional[IMockable]:
verify = LocalFilesystemVerify(files_path)
runnable = LocalFilesystem(defaults_configurer, files_path, setup, verify)
mock = RunnableLocalMock('filesystem', 'Filesystem', runnable)
elif mock_name == 'redis':
defaults_configurer = FileConfigurer(mock_defaults_paths)
setup = DockerRedisSetup()
verify = DockerRedisVerify()
runnable = DockerRedis(defaults_configurer, self.__is_dev_mode, setup, verify, self.__docker_manager,
DockerNetwork())
mock = RunnableDockerMock('redis', 'Redis', runnable)
return mock
2 changes: 2 additions & 0 deletions touchstone/lib/nodes/mocks/mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from touchstone.lib.nodes.mocks.behaviors.i_mongodb_behavior import IMongodbBehavior
from touchstone.lib.nodes.mocks.behaviors.i_mysql_behabior import IMysqlBehavior
from touchstone.lib.nodes.mocks.behaviors.i_rabbitmq_behavior import IRabbitmqBehavior
from touchstone.lib.nodes.mocks.behaviors.i_redis_behavior import IRedisBehavior
from touchstone.lib.nodes.mocks.behaviors.i_s3_behavior import IS3Behavior
from touchstone.lib.nodes.mocks.docker.runnable_docker_mock import RunnableDockerMock
from touchstone.lib.nodes.mocks.i_mockable import IMockable
Expand All @@ -21,6 +22,7 @@ def __init__(self):
self.mysql: IMysqlBehavior = None
self.s3: IS3Behavior = None
self.filesystem: IFilesystemBehavior = None
self.redis: IRedisBehavior = None
self.__runnable_docker_mocks: List[RunnableDockerMock] = []
self.__runnable_local_mocks: List[RunnableLocalMock] = []
self.__mocks_running = False
Expand Down

0 comments on commit 3ce951e

Please sign in to comment.