MockSwift
MockSwift allows you to write mocks and make better tests. Because MockSwift is an open source library 100% written in Swift, it is AVAILABLE ON ALL PLATFORMS.
Features
Actually MockSwift supports:
- Stub
- [x] Protocol methods
- [x] Protocol properties
- [x] Protocol subscripts
- [ ] Class
- [ ] Struct
- [ ] Enum
- [x] Default values for types
- Verify interactions
- [x] Protocol methods
- [x] Protocol properties
- [x] Protocol subscripts
- [ ] Class
- [ ] Struct
- [ ] Enum
_ Parameter matching - [x] Predicates
- [x] Generics
CHANGELOG
You can see all changes and new features here.
Installation
Swift Package Manager
MockSwift has been designed to work with Swift Package Manager.
// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "MyProject",
dependencies: [
.package(url: "https://github.com/leoture/MockSwift.git", from: "1.0.0")
],
targets: [
.testTarget(name: "MyProjectTests", dependencies: ["MockSwift"])
]
)
Usage
Quick Look
class AwesomeTests: XCTestCase {
private var printer: Printer!
@Mock private var userService: UserService
override func setUp() {
printer = Printer(userService)
}
func test_sayHello() {
// Given
given(userService).fetchUserName(of: "you").willReturn("my friend")
given(userService).isConnected.get.willReturn(true)
given(userService)[cache: .any()].set(.any()).willDoNothing()
// When
let message = printer.sayHello(to: "you", from: "me")
// Then
then(userService).fetchUserName(of: .any()).called()
then(userService).isConnected.get.called(times: 1)
then(userService)[cache: "you"].set("my friend").calledOnce()
XCTAssertEqual(message, "me: Hello my friend")
}
}
Details
Suppose that you have a UserService
protocol.
struct User: Equatable {
let identifier: String
let name: String
}
protocol UserService {
func fetch(identifier: String) -> User
}
And you want to test this UserCore
class.
class UserCore {
private let service: UserService
init(_ service: UserService) {
self.service = service
}
func fetchCurrentUser() -> User {
service.fetch(identifier: "current")
}
}
Make better tests
Now, with MockSwift, you can use a mocked UserService
in your tests with the @Mock
annotation.
@Mock private var service: UserService
// equivalent to
private var service: UserService = Mock()
And easly configure it to fully test UseCore
.
class UserCoreTests: XCTestCase {
private var core: UserCore!
@Mock private var service: UserService
override func setUp() {
core = UserCore(service)
}
func test_fetchCurrentUser() {
// Given
let expectedUser = User(identifier: "current", name: "John")
given(service).fetch(identifier: .any()).willReturn(expectedUser)
// When
let user = core.fetchCurrentUser()
// Then
then(service).fetch(identifier: .any()).called()
XCTAssertEqual(user, expectedUser)
}
}
Given
given()
enables you to define behaviours.
example:
given(service).fetch(identifier: .any()).willReturn(expectedUser)
// equivalent to
given(service) {
$0.fetch(identifier: .any()).willReturn(expectedUser)
}
given(service) {
$0.fetch(identifier: "current")
.willReturn(expectedUser, expectedUser1, expectedUser2)
$0.fetch(identifier: .match(when: \.isEmpty))
.will { (params) -> User in
// do something else
return expectedUser
}
}
you can also define behaviours when you instantiate the mock.
@Mock({
$0.fetch(identifier: .any()).willReturn(expectedUser)
})
private var service: UserService
Then
then()
enables you to verify calls.
example:
then(service).fetch(identifier: .any()).called()
// equivalent to
then(service) {
$0.fetch(identifier: .any()).called()
}
then(service) {
$0.fetch(identifier: "current").called(times: >=2)
$0.fetch(identifier: == "").called(times: 0)
}
You can go further and verify order of calls
let assertion = then(service).fetch(identifier: "current").called(times: >=2)
then(service).fetch(identifier: == "").called(times: 1, after: assertion)
Stubs
In MockSwift, stubs are default values that are returned when no behaviours has been found.
Global Stubs
You can define a global stub for any type.
It will concern all mocks you will use in every tests.
extension User: GlobalStub {
static func stub() -> User {
User(identifier: "id", name: "John")
}
}
Local Stubs
You can also define a stub localy for any type.
It will concern only the current mock.
@Mock(localStubs: [
User.self => User(identifier: "id", name: "John")
])
private var service: UserService
Strategy
The default strategy is to find behaviour defined with given()
. If no behaviour is found, it will return a local stub. If no local stub is found, it will return a global stub.
@Mock private var service: UserService
// equivalent to
@Mock(strategy: .default)
private var service: UserService
// equivalent to
@Mock(strategy: [.given, .localStubs, .globalStubs])
private var service: UserService
You can change the order of the strategy list or remove items as you want.
Write mocks
Automatically
MockSwift provides a stencil template for sourcery. You can use the AutoMockable
annotation to generate code.
// sourcery: AutoMockable
protocol UserService {
func fetch(identifier: String) -> User
}
To generate code at every build, you can add a build phase before Compile Sources
.
sourcery \
--sources MyLibrary \
--templates MyLibraryTests/path/to/MockSwift.stencil \
--output MyLibraryTests/path/to/GeneratedMocks.swift \
--args module=MyLibrary
Manually
To enable MockSwift for UserService type, you have to extend Mock.
extension Mock: UserService where WrappedType == UserService {
public func fetch(identifier: String) -> User {
mocked(identifier)
}
}
To allow behaviour definition through given()
method, you have to extend Given.
extension Given where WrappedType == UserService {
public func fetch(identifier: Predicate<String>) -> Mockable<User> {
mockable(identifier)
}
public func fetch(identifier: String) -> Mockable<User> {
mockable(identifier)
}
}
To allow call verification through then()
method, you have to extend Then.
extension Then where WrappedType == UserService {
public func fetch(identifier: Predicate<String>) -> Verifiable<User> {
verifiable(identifier)
}
public func fetch(identifier: String) -> Verifiable<User> {
verifiable(identifier)
}
}