Swift Mocking is a collection of Swift macros used to generate mock dependencies.
Swift Mocking is Swift 6 compatible, fully concurrency-safe, and generates conditionally compiled mocks that can handle:
- Any access level
- Associated types, including primary associated types
- Actor conformance
- Generic
where
clauses - Initializers
- Static members
- Instance members
- Read-only properties, including those with getters marked with
async
,throws
,mutating
, etc. - Read-write properties
- Mutating methods
- Async methods
- Throwing methods
- Generic methods
- Method overloads
- Attributed types (
inout
,consuming
,sending
, etc.) - Variadic parameters
@Mocked(compilationCondition: .debug)
protocol WeatherService {
func currentTemperature(latitude Double, longitude: Double) async throws -> Double
}
struct WeatherViewModelTests {
@Test
func loadCurrentTemperature() async {
let weatherServiceMock = WeatherServiceMock()
let viewModel = WeatherViewModel(weatherService: weatherServiceMock)
// Set the dependency's implementation.
weatherServiceMock._currentTemperature.implementation = .returns(75)
// Invoke the method being tested.
await viewModel.loadCurrentTemperature(latitude: 37.3349, longitude: 122.0090)
// Validate the number of times the dependency was called.
#expect(weatherServiceMock._currentTemperature.callCount == 1)
// Validate the arguments passed to the dependency.
#expect(weatherServiceMock._currentTemperature.lastInvocation?.latitude == 37.3349)
#expect(weatherServiceMock._currentTemperature.lastInvocation?.longitude == 122.0090)
// Validate the view model's new state.
#expect(viewModel.state == .loaded(temperature: 75))
}
}
To add Swift Mocking to a Swift package manifest file:
- Add the
swift-mocking
package to your package'sdependencies
:.package( url: "https://github.com/fetch-rewards/swift-mocking.git", from: "<#latest swift-mocking tag#>" )
- Add the
Mocking
product to your target'sdependencies
:.product(name: "Mocking", package: "swift-mocking")
Import Mocking
:
import Mocking
Attach the @Mocked
macro to your protocol:
@Mocked(compilationCondition: .debug)
protocol Dependency {
var property: Int { get set }
func method(x: Int, y: Int) async throws -> Int
}
And that's it! You now have a sophisticated mock dependency that will be updated automatically any time you change your protocol.
Note
For mocking protocols that inherit from other protocols, see @MockedMembers
.
Important
Using @Mocked
without an explicit compilationCondition
argument will result in the generated mock being wrapped
in an #if
compiler directive with a SWIFT_MOCKING_ENABLED
condition (i.e. #if SWIFT_MOCKING_ENABLED
). To continue
using @Mocked
without any additional setup, use @Mocked(compilationCondition: .debug)
as shown in the examples above.
If you would like fine-tuned control over when generated mocks are compiled, see Compilation Condition.
Now let's take a look at the mock we've generated, stripping out some of the implementation details to highlight the mock's API:
#if DEBUG
final class DependencyMock: Dependency {
var property: Int
var _property: MockReadWriteProperty<Int>
func method(x: Int, y: Int) async throws -> Int
var _method: MockReturningParameterizedAsyncThrowingMethod<...>
}
#endif
Each member of the generated mock is backed by a single, underscored property. These backing properties contain the invocation records and implementation details for each member.
For example, the backing property for property
from the above mock would have the following structure and
implementation constructors:
// Invocation Records
mock._property.getter.callCount // Int
mock._property.getter.returnedValues // [Int]
mock._property.getter.lastReturnedValue // Int?
mock._property.setter.callCount // Int
mock._property.setter.invocations // [Int]
mock._property.setter.lastInvocation // Int?
// Implementation Constructors
mock._property.getter.implementation = .invokes { 5 }
mock._property.getter.implementation = .uncheckedInvokes { 5 }
mock._property.getter.implementation = .returns(5)
mock._property.getter.implementation = .uncheckedReturns(5)
mock._property.setter.implementation = .invokes { _ in }
mock._property.setter.implementation = .uncheckedInvokes { _ in }
And the backing property for method
from the above mock would have the following structure and implementation
constructors:
// Invocation Records
mock._method.callCount // Int
mock._method.invocations // [(x: Int, y: Int)]
mock._method.lastInvocation // (x: Int, y: Int)?
mock._method.returnedValues // [Result<Int, any Error>]
mock._method.lastReturnedValue // Result<Int, any Error>?
// Implementation Constructors
mock._method.implementation = .invokes { _, _ in 5 }
mock._method.implementation = .uncheckedInvokes { _, _ in 5 }
mock._method.implementation = .throws(URLError(.badServerResponse))
mock._method.implementation = .returns(5)
mock._method.implementation = .uncheckedReturns(5)
Note
Depending on the type of member being mocked, the backing property's structure and implementation constructors may differ slightly from the examples above.
Tip
Only use unchecked
implementation constructors when dealing with non-sendable types. For sendable types, use
the checked version of each implementation constructor (e.g. invokes
instead of uncheckedInvokes
and returns
instead of uncheckedReturns
). These checked constructors require the member's arguments and/or return value to
be sendable.
With Strict Concurrency Checking or Swift 6+ enabled, you will get concurrency warnings/errors if you try to use
these checked constructors with a non-sendable type, whether that non-sendable type is the member's argument or
return value or is a type captured by the closure passed to invokes
:
let nonSendableInstance = NonSendableType()
mock._methodReturningNonSendableType.implementation = .invokes { // Type 'NonSendableType' does not conform to the 'Sendable' protocol
nonSendableInstance // Capture of 'nonSendableInstance' with non-sendable type 'NonSendableType' in a `@Sendable` closure
}
Swift Mocking contains several Swift macros: @Mocked
, @MockedMembers
, @MockableProperty
, and @MockableMethod
.
It also contains two internal, underscored macros (@_MockedProperty
and @_MockedMethod
) which are not meant to be used directly.
@Mocked
is an attached peer macro that generates a mock class from a protocol declaration:
@Mocked(compilationCondition: .debug)
protocol Dependency {}
// Generates:
#if DEBUG
@MockedMembers
final class DependencyMock: Dependency {}
#endif
Using @Mocked
without an explicit compilationCondition
argument will result in the generated mock being wrapped
in an #if
compiler directive with a SWIFT_MOCKING_ENABLED
condition:
@Mocked
protocol Dependency {}
// Generates:
#if SWIFT_MOCKING_ENABLED
@MockedMembers
final class DependencyMock: Dependency {}
#endif
Because of the nature of Swift macros, @Mocked
only has access to the raw syntax of its arguments and the protocol
to which it's attached. This limitation precludes us from making compilationCondition
globally configurable. So when
deciding on an appropriate default value for compilationCondition
, we had two goals in mind:
- One-step install and one-line usage (excluding import statement) for users who want conditionally compiled, generated mocks without any additional setup
- The simplest setup possible for users with large codebases who want fine-tuned control over when generated mocks are compiled
As such, we decided to make the default compilation condition SWIFT_MOCKING_ENABLED
. This allows us to accomplish
both of these goals, albeit with the tiny caveat that one-step-install users need to explicitly specify the compilation
condition when using @Mocked
.
If you would like to make use of SWIFT_MOCKING_ENABLED
in an Xcode project, add SWIFT_MOCKING_ENABLED
as a compiler
flag to the build configurations for which you would like mocks to compile.
To make use of SWIFT_MOCKING_ENABLED
in a Swift package, add the following SwiftSetting
to your target's swiftSettings
array:
.define("SWIFT_MOCKING_ENABLED", .when(configuration: .debug))
Note
The .debug
build configuration in a Swift package applies to any Xcode project build configuration with a name
that begins with either "Debug" or "Development".
If you would like to specify a compilation condition other than SWIFT_MOCKING_ENABLED
, you can explicitly provide
one to the @Mocked
macro:
/// The mock will not be wrapped in an `#if` compiler directive.
@Mocked(compilationCondition: .none)
protocol NoneCompilationCondition {}
/// `#if DEBUG`
@Mocked(compilationCondition: .debug)
protocol DebugCompilationCondition {}
/// `#if !RELEASE`
@Mocked(compilationCondition: "!RELEASE")
protocol CustomCompilationCondition {}
The generated mock is marked with the access level required to conform to the protocol:
public
for public
, implicit internal
for both implicit and explicit internal
,
and fileprivate
for both fileprivate
and private
.
@Mocked
also supports protocols that conform to Actor
:
@Mocked(compilationCondition: .debug)
protocol Dependency: Actor {}
// Generates:
#if DEBUG
@MockedMembers
final actor DependencyMock: Dependency {}
#endif
When @Mocked
is applied to a protocol that defines associated types, the resulting mock uses
those associated types as its generic parameters in order to fulfill the protocol requirements:
@Mocked(compilationCondition: .debug)
protocol Dependency {
associatedtype Key: Hashable
associatedtype Value: Equatable
}
// Generates:
#if DEBUG
@MockedMembers
final class DependencyMock<Key: Hashable, Value: Equatable>: Dependency {}
#endif
In addition to the @MockedMembers
macro that gets applied to the mock declaration, @Mocked
also
utilizes the @MockableProperty
and @MockableMethod
macros when defining the mock's members:
@Mocked(compilationCondition: .debug)
protocol Dependency {
var readOnlyProperty: Int { get }
var readOnlyAsyncProperty: Int { get async }
var readOnlyThrowingProperty: Int { get throws }
var readOnlyAsyncThrowingProperty: Int { get async throws }
var readWriteProperty: Int { get set }
}
// Generates:
#if DEBUG
@MockedMembers
final class DependencyMock: Dependency {
@MockableProperty(.readOnly)
var readOnlyProperty: Int
@MockableProperty(.readOnly(.async))
var readOnlyAsyncProperty: Int
@MockableProperty(.readOnly(.throws))
var readOnlyThrowingProperty: Int
@MockableProperty(.readOnly(.async, .throws))
var readOnlyAsyncThrowingProperty: Int
@MockableProperty(.readWrite)
var readWriteProperty: Int
}
#endif
Because @MockedMembers
cannot look outward at the protocol declaration to determine whether, for example,
a property is read-only or read-write, @Mocked
uses @MockableProperty
and @MockableMethod
to provide
information about each member to @MockedMembers
. @MockedMembers
then applies the @_MockedProperty
and
@_MockedMethod
macros to those members, which then generate the mock's backing properties.
Note
See @Mockable
vs. @_Mocked
for more information.
Like @MockedMembers
, @Mocked
also cannot look outward. This presents a problem when the protocol you are trying
to mock inherits from another protocol. Because @Mocked
cannot see the other protocol's declaration, it is unable
to generate conformances to the requirements of that protocol. In this instance, you will need to write the mock
declaration yourself, along with the declarations for the properties and methods required by the protocols. Luckily,
using Xcode's Fix-It
feature to add protocol conformances and @MockedMembers
, @MockableProperty
, and @MockableMethod
to generate
backing properties, you can still easily create and maintain these mocks with minimal code:
protocol Dependency: SomeProtocol {
var propertyFromDependency: Int { get }
func methodFromDependency()
}
#if DEBUG
@MockedMembers
final class DependencyMock: Dependency {
@MockableProperty(.readOnly)
var propertyFromDependency: Int
@MockableProperty(.readWrite)
var propertyFromSomeProtocol: Int
func methodFromDependency()
func methodFromSomeProtocol()
}
#endif
When a mock contains static members, @MockedMembers
generates a static method named resetMockedStaticMembers
that can be used to reset the backing properties for those static members:
@MockedMembers
public final class DependencyMock: Dependency {
@MockableProperty(.readOnly)
public static var staticReadOnlyProperty: Int
@MockableProperty(.readWrite)
public static var staticReadWriteProperty: Int
public static func staticMethod()
// Generates:
/// Resets the implementations and invocation records of the mock's
/// static properties and methods.
public static func resetMockedStaticMembers() {
self.__staticReadOnlyProperty.reset()
self.__staticReadWriteProperty.reset()
self.__staticMethod.reset()
}
}
This method is useful for tearing down static state between test cases.
In instances where you are using @MockedMembers
directly instead of using @Mocked
, @MockableProperty
is required for @MockedMembers
to be able to generate backing properties for the property conformances
within your mock:
protocol Dependency {
var readOnlyProperty: Int { get }
var readOnlyAsyncProperty: Int { get }
var readOnlyThrowingProperty: Int { get }
var readOnlyAsyncThrowingProperty: Int { get }
var readWriteProperty: Int { get set }
}
#if DEBUG
@MockedMembers
final class DependencyMock: Dependency {
@MockableProperty(.readOnly)
var readOnlyProperty: Int { get }
@MockableProperty(.readOnly(.async))
var readOnlyAsyncProperty: Int { get }
@MockableProperty(.readOnly(.throws))
var readOnlyThrowingProperty: Int { get }
@MockableProperty(.readOnly(.async, .throws))
var readOnlyAsyncThrowingProperty: Int { get }
@MockableProperty(.readWrite)
var readWriteProperty: Int { get set }
}
#endif
Unlike @MockableProperty
, @MockableMethod
is not required when using @MockedMembers
directly.
@MockedMembers
can and will generate backing properties for method conformances within your mock
whether they are explicitly marked with @MockableMethod
or not.
Still, there may be cases where you want or need to use @MockableMethod
. While @Mocked
and @MockedMembers
do an excellent job of dealing with name conflicts caused by method overloads, there's always a possibility that
a name conflict may arise between two backing properties. In this case, you can provide an explicit name for the
method's backing property using @MockableMethod
:
@MockedMembers
final class DependencyMock: Dependency {
@MockableMethod(mockMethodName: "someUniqueName")
func methodWithNameConflict()
}
In other cases, you may simply dislike the name that @Mocked
or @MockedMembers
generates for a
method's backing property and wish to give the backing property a different name.
If you believe that @Mocked
or @MockedMembers
should have been able to resolve a name conflict,
or if you think the name conflict resolution logic can be improved in any way, please let us know by
opening an issue.
@MockableProperty
and @MockableMethod
do not produce expansions. They are simply markers that expose information
to @MockedMembers
. @MockableProperty
exposes propertyType
(.readOnly
, .readWrite
, etc.) and @MockableMethod
exposes mockMethodName
. @MockedMembers
then forwards this information to @_MockedProperty
and @_MockedMethod
along
with other parameters that @MockedMembers
provides for us. @_MockedProperty
and @_MockedMethod
then generate the mock's
backing properties. @MockableProperty
and @MockableMethod
exist so that the consumer has to provide as little information
as possible when manually applying @MockedMembers
. The usage of the prefix Mockable
is a deliberate choice to semantically
distinguish the macros that serve as markers from those that actually produce mocks.
The simplest way to contribute to this project is by opening an issue.
If you would like to contribute code to this project, please read our Contributing Guidelines.
By opening an issue or contributing code to this project, you agree to follow our Code of Conduct.
This library is released under the MIT license. See LICENSE for details.