Skip to content

class Athena::DependencyInjection::Spec::MockableServiceContainer
inherits Athena::DependencyInjection::ServiceContainer #

A mock implementation of ADI::ServiceContainer that be used within a testing context to allow for mocking out services without affecting the actual container outside of tests.

An example of this is when integration testing service based ART::Controllers. Service dependencies that interact with an external source, like a third party API or a database, should most likely be mocked out. However your other services should be left as is in order to get the most benefit from the test.

Mocking#

The ADI::ServiceContainer is nothing more than a normal Crystal class with some instance variables and methods. As such, mocking services is as easy as monkey patching self with the mocked versions, assuming of course they are of a compatible type.

Given Crystal's lack of a robust mocking shard, it isn't as straightforward as other languages. The best way at the moment is either using inheritance or interfaces (modules) to manually create a concrete test class/struct; with the latter option being preferred as it would work for both structs and classes.

For example, we can create a mock implementation of a type by extending it:

class MockMyService < MyService
  def get_value
    # Can now just return a static expected value.
    # Test properties/constructor(s) can also be added to make it a bit more generic.
    1234
  end
end

Because our mock extends MyService, it is a compatible type for anything typed as MyService.

Another way to handle mocking is via interfaces (modules).

module SomeInterface; end

struct MockMyService
  include SomeInterface
end

Because our mock implements SomeInterface, it is a compatible type for anything typed as SomeInterface.

Note

Service mocks do not need to registered as services themselves since they will need to be configured manually. NOTE: The type argument as part of the ADI::Register annotation can be used to set the type of a service within the container. See ADI::Register@customizing-services-type for more details.

Dynamic Mocks#

A dynamic mock consists of adding a setter to self that allows setting the mocked service dynamically at runtime, while keeping the original up until if/when it is replaced.

class ADI::Spec::MockableServiceContainer
  # The setter should be nilable as they're lazily initialized within the container.
  setter my_service : MyServiceInterface?
end

# ...

# Now the `my_service` service can be replaced at runtime.
mock_container.my_service = MockMyService.new

# ...

Global Mocks#

Global mocks totally replace the original service, i.e. always return the mocked service.

class ADI::Spec::MockableServiceContainer
  # Global mocks should use the block based `getter` macro.
  getter my_service : MyServiceInterface { MockMyService.new }
end

# `MockMyService` will now be injected across the board when using `self`.
# ...

Hybrid Mocks#

Dynamic and Global mocking can also be combined to allow having a default mock, but allow overriding if/when needed. This can be accomplished by adding both a getter and setter to self.

class ADI::Spec::MockableServiceContainer
  # Hybrid mocks should use the block based `property` macro.
  property my_service : MyServiceInterface { DefaultMockService.new }
end

# ...

# `DefaultMockService` will now be injected across the board by when using `self`.

# But can still be replaced at runtime.
mock_container.my_service = CustomMockService.new

# ...