Mocks
Mocks are entities used to verify expectations. As dummies, they don’t implement any functionality, just the bare minimum to capture its interaction with the subject under test.
Using the same protocol from the Dummy example, ShapeProtocol
,
protocol ShapeProtocol {
func draw(at point: CGPoint, radius: Float)
}
a basic example of a mock would be:
class ShapeMock: ShapeProtocol {
private(set) var didCallDraw = false
private(set) var lastPoint: CGPoint?
private(set) var lastRadius: Float?
func draw(at point: CGPoint, radius: Float) {
didCallDraw = true
lastPoint = point
lastRadius = radius
}
}
The method draw(at:radius:)
captures all the parameters passed to it, and sets a boolean indicating if the method was called or not.
Suppose we want to test a type, ShapeDrawer
, which has a property shape
:
struct ShapeDrawer {
let shape: ShapeProtocol
func draw() {
shape.draw(at: .zero, radius: 5.0)
}
}
In order to test it, instead of passing a real object , we pass an instance of ShapeMock
, a concrete implementation of ShapeProtocol
. This allows us to focus on the subject under test, ShapeDrawer
, while controlling the external entity.
func testDraw() {
// Given
let shapeMock = ShapeMock()
let drawer = ShapeDrawer(
shape: shapeMock
)
// When
drawer.draw()
// Then
XCTAssertTrue(
shapeMock.didCallDraw,
"It should call draw"
)
XCTAssertEqual(
shapeMock.lastPoint, .zero,
"It should pass the correct point"
)
XCTAssertEqual(
shapeMock.lastRadius, 5.0,
"It should pass the correct radius"
)
}
There are tools to automatically generate mocks from protocols, such as [Sourcery], but the idea is exactly the same: capturing interaction to verify expectations. Sourcery is my favorite solution to deal with mocks, it saves time with all the boilerplate of writing mocks, while creating a consistent interface for all the mocks.
Some engineers prefer to avoid protocols, working with simple types such as structs
and enums
. In this scenario, ShapeProtocol
would be replaced by a real type :
struct Shape {
var draw: (CGPoint, Float) -> Void
}
while ShapeDrawer
gets a real object injected:
struct ShapeDrawer {
let shape: Shape
func draw() {
shape.draw(.zero, 5.0)
}
}
The mock, in this scenario is a simple instance of Shape
, using its closure to capture parameters:
func testDraw() {
var passedPoint: CGPoint?
var passedRadius: Float?
// Given
let drawer = ShapeDrawer(
shape: .init(
draw: { point, radius in
passedPoint = point
passedRadius = radius
}
)
)
// When
drawer.draw()
// Then
XCTAssertEqual(
passedPoint, .zero,
"It should pass the correct point"
)
XCTAssertEqual(
passedRadius, 5.0,
"It should pas the correct radius"
)
}
Where to go from here: