Behavior vs State verification

We learned in the previous article Test doubles :
- What is a test double ?
- When is it used ?
- What are the different types of test double ?
Test doubles are ways of creating substitutes of an objects’ real implementation. Depending on which double we use in a test, there are 2 ways of verifying the expected result: behavior or state verification.
Behavior verification
Mock always uses behavior verification. It means that we verify that the order in which methods are invoked.
Here is an example from Mocks Aren’t Stubs from Martin Fowler:
We want to take an order object and fill it from a warehouse object. The order is very simple, with only one product and a quantity. The warehouse holds inventories of different products. When we ask an order to fill itself from a warehouse there are two possible responses. If there’s enough product in the warehouse to fill the order, the order becomes filled and the warehouse’s amount of the product is reduced by the appropriate amount. If there isn’t enough product in the warehouse then the order isn’t filled and nothing happens in the warehouse.
Let’s extend this example: we want to send an email message if we fail to fill an order. In this example, we order 51 bottles of Talisker, but there are only 50 bottles in stock. We will verify that filling a failing order only send one message. In this test, we use a mock for the collaborator since we want to verify that specific methods have been called with correct outputs.
class MailServiceMock: MailService {
var sendCallCount = 0 func send(_ message: String) {
sendCallCount += 1
}
}class WarehouseMock: Warehouse {
var hasInventoryMessage = [Bool]() override func hasInventory(_ product: String, _ quantity: Int) -> Bool {
let result = super.hasInventory(product, quantity)
hasInventoryMessage.append(result)
return result
}
}func testOrderSendsMailIfUnfilled() throws {
let order = Order(talisker, 51)
let mailer = MailServiceMock()
let warehouse = WarehouseMock()
order.setMailer(mailer) order.fill(warehouse) XCTAssertEqual(1, mailer.sendCallCount)
XCTAssertEqual(1, warehouse.hasInventoryMessage.count)
let hasInventory = try XCTUnwrap(warehouse.hasInventoryMessage.first)
XCTAssertFalse(hasInventory)
}
Since I do not use any external libraries for mocking, I made my own implementation in order to observe method invocations. In this example, WarehouseMock is a partial mocking since we only modify an object to partially behave differently for the test.
State verification
Stub and Spy use state verification. It means that after the method we want to test is invoked, we verify that the state of the SUT and/or its collaborators are the expected ones.
We will use the same example as above. In this test, we do not provide any predefined inputs to the SUT, but we want to verify how many messages were sent by the mailing service. It is the reason why we will use a spy.
class MailServiceSpy: MailService {
var messages = [String]() func send(_ message: String) {
messages.append(message)
}
}func testOrderSendsMailIfUnfilled() {
let order = Order(talisker, 51)
let mailer = MailServiceSpy()
order.setMailer(mailer) order.fill(warehouse) XCTAssertEqual(1, mailer.messages.count)
}
In this example, Order is the SUT and MailServiceSpy is the collaborator. The spy observes what happen to the send method when exercising the SUT.