How to mock the service layer in a python (flask) webapp for unit testing?
You can use dependency injection or inversion of control to achieve a code much simpler to test.
replace this:
def addtrans(): ... # check that both users exist if not services.user_exists(to_id) or not services.user_exists(from_id): return "no such users" ...
with:
def addtrans(services=services): ... # check that both users exist if not services.user_exists(to_id) or not services.user_exists(from_id): return "no such users" ...
what's happening:
- you are aliasing a global as a local (that's not the important point)
- you are decoupling your code from
services
while expecting the same interface. - mocking the things you need is much easier
e.g.:
class MockServices: def user_exists(id): return True
Some resources:
You can patch out the entire services module at the class level of your tests. The mock will then be passed into every method for you to modify.
@patch('routes.services')class MyTestCase(unittest.TestCase): def test_my_code_when_services_returns_true(self, mock_services): mock_services.user_exists.return_value = True self.assertIn('success', routes.addtrans()) def test_my_code_when_services_returns_false(self, mock_services): mock_services.user_exists.return_value = False self.assertNotIn('success', routes.addtrans())
Any access of an attribute on a mock gives you a mock object. You can do things like assert that a function was called with the mock_services.return_value.some_method.return_value
. It can get kind of ugly so use with caution.
I would also raise a hand for using dependency injection for such needs. You can use Dependency Injector to describe structure of your application using inversion of control container(s) to make it look like this:
"""Example of dependency injection in Python."""import loggingimport sqlite3import boto3import example.mainimport example.servicesimport dependency_injector.containers as containersimport dependency_injector.providers as providersclass Core(containers.DeclarativeContainer): """IoC container of core component providers.""" config = providers.Configuration('config') logger = providers.Singleton(logging.Logger, name='example')class Gateways(containers.DeclarativeContainer): """IoC container of gateway (API clients to remote services) providers.""" database = providers.Singleton(sqlite3.connect, Core.config.database.dsn) s3 = providers.Singleton( boto3.client, 's3', aws_access_key_id=Core.config.aws.access_key_id, aws_secret_access_key=Core.config.aws.secret_access_key)class Services(containers.DeclarativeContainer): """IoC container of business service providers.""" users = providers.Factory(example.services.UsersService, db=Gateways.database, logger=Core.logger) auth = providers.Factory(example.services.AuthService, db=Gateways.database, logger=Core.logger, token_ttl=Core.config.auth.token_ttl) photos = providers.Factory(example.services.PhotosService, db=Gateways.database, s3=Gateways.s3, logger=Core.logger)class Application(containers.DeclarativeContainer): """IoC container of application component providers.""" main = providers.Callable(example.main.main, users_service=Services.users, auth_service=Services.auth, photos_service=Services.photos)
Having this will give your a chance to override particular implementations later:
Services.users.override(providers.Factory(example.services.UsersStub))
Hope it helps.