单元测试的目的是检查一个组件的预期行为,这被称为系统测试(又名SUT)。当SUT有一些依赖时,测试可能会很痛苦,因为你必须以某种方式管理这些依赖。您不应该使用与生产中相同的方法,因为这会增加测试的复杂性。解决方案是使用定制的依赖项,这使得SUT易于测试。
这些“定制依赖”通常被称为“模拟”依赖。不幸的是,“模拟”并不是一个恰当的名称,因为一个定制的依赖项可以有不同的行为方式,这取决于您想要如何测试SUT。杰拉德·梅萨罗什在他的书《xUnit测试模式:重构测试代码》中创建了一个不同类型的“模拟”依赖项列表——测试加倍。
有不同类型的双重测试。以下列表的顺序从最简单到最复杂。出于这个原因,我建议你在阅读下一个测试之前,先好好理解每一个测试。
虚拟依赖是测试中不使用的对象。基本上,你只是用它来编译测试。
我们可以开始使用普通的Square
类,该类计算它的面积,并且由于协议的原因可以保存该面积的值SaverProtocol
:
protocol SaverProtocol {
func save(value: Float)
}
class Square {
private let saver: SaverProtocol
var side: Float = 0
var area: Float {
return pow(self.side, 2)
}
init(saver: SaverProtocol) {
self.saver = saver
}
func saveArea() {
saver.save(value: area)
}
}
然后,我们要测试Square
检查面积计算是否正确。对于这个测试,我们不关心方法saveArea
,因为我们关注的是面积计算,但是Square
需要一个SaverProtocol
即使我们不使用它。因此,我们可以创建一个虚拟类,它创建SaverProtocol
方法,唯一的目的是编译我们的测试:
func test_Area() {
let sut = Square(saver: DummySaver())
sut.side = 5
XCTAssertEqual(sut.area, 25)
}
class DummySaver: SaverProtocol {
func save(value: Float) { }
}
假依赖有一个实现,但它是提高测试速度和降低复杂性的捷径。
我们可以从一堂课开始UsersRepo
,它吸引了用户,这要归功于UsersServiceProtocol
,并具有返回带有实体计数的友好消息的方法。对于生产中使用的正常实现,我们从数据库中获取用户,如核心数据、领域等:
struct User {
let identifier: String
let username: String
}
protocol UsersServiceProtocol {
func fetchUsers() -> [User]
}
class UsersService: UsersServiceProtocol {
func fetchUsers() -> [User] {
let users = // execute a query in a Database
return users
}
}
class UsersRepo {
private let users: [User]
init(usersService: UsersServiceProtocol) {
self.users = usersService.fetchUsers()
}
func usersCountMessage() -> String {
return "Number of users in the system: \(users.count)"
}
}
然后,我们要测试usersCountMessage
检查它是否返回正确的消息。我们不能在测试中使用数据库,因为它会降低测试速度并增加测试的复杂性。为了避免数据库,我们可以创建一个FakeUsersService
返回硬编码用户:
func test_UsersCountMessage() {
let sut = UsersRepo(usersService: FakeUsersService())
XCTAssertEqual(sut.usersCountMessage(), "Number of users in the system: 2")
}
class FakeUsersService: UsersServiceProtocol {
func fetchUsers() -> [User] {
return [User(identifier: 1, username: "user01"), User(identifier: 2, username: "user02")]
}
}
不要害怕在测试中使用硬编码值。这不是生产代码,你必须找到一个好的平衡来进行快速和良好的测试。
存根依赖提供了关于SUT调用方法的固定答案——就像一个布尔标志,它允许您检查是否调用了一个内部方法。
我们可以重复使用“伪造”的例子:
class UsersRepo {
private let users: [User]
init(usersService: UsersServiceProtocol) {
self.users = usersService.fetchUsers()
}
func usersCountMessage() -> String {
return "Number of users in the system: \(users.count)"
}
}
现在,我们要测试构造函数是否加载了用户调用fetchUsers
。为了实现它,我们可以使用StubUsersService
它提供了一个布尔属性isFetchUsersCalled
检查是否fetchUsers
叫做:
func test_Init() {
let stubUsersService = StubUsersService()
_ = UsersRepo(usersService: stubUsersService)
XCTAssertTrue(stubUsersService.isFetchUsersCalled)
}
class StubUsersService: UsersServiceProtocol {
var isFetchUsersCalled = false
func fetchUsers() -> [User] {
isFetchUsersCalled = true
return []
}
}
间谍依赖是存根的一个更强大的版本。它提供基于如何调用其方法的信息,如参数值或方法被调用的次数。
我们可以重复使用假人的例子:
class Square {
private let saver: SaverProtocol
var side: Float = 0
var area: Float {
return pow(self.side, 2)
}
init(saver: SaverProtocol) {
self.saver = saver
}
func saveArea() {
saver.save(value: area)
}
}
现在,我们要测试这个方法saveArea
检查它是否调用方法save
的SaverProtocol
仅一次,以面积为参数:
func test_SaveArea() {
let saver = SpySaver()
let sut = Square(saver: saver)
sut.side = 5
sut.saveArea()
XCTAssertEqual(saver.saveCallsCount, 1)
XCTAssertEqual(saver.saveValue, 25)
}
class SpySaver: SaverProtocol {
var saveCallsCount = 0
var saveValue: Float?
func save(value: Float) {
saveCallsCount += 1
saveValue = value
}
}
看看用来解释Spy的测试,你会注意到,一旦你有很多参数要检查,你的测试就变得难以阅读,你必须暴露很多SpySaver
属性。
模拟依赖帮助您清理测试,因为您在内部检查值。
为了便于解释,我们可以重用用于Spy的测试:
func test_SaveArea() {
let saver = SpySaver()
let sut = Square(saver: saver)
sut.side = 5
sut.saveArea()
XCTAssertEqual(saver.saveCallsCount, 1)
XCTAssertEqual(saver.saveValue, 25)
}
class SpySaver: SaverProtocol {
var saveCallsCount = 0
var saveValue: Float?
func save(value: Float) {
saveCallsCount += 1
saveValue = value
}
}
我们用模拟依赖来重构它,而不是间谍依赖。我们在MockSaver
方法verify
两者都保留saveCallsCount
和saveValue
私人:
func test_SaveArea() {
let saver = MockSaver()
let sut = Square(saver: saver)
sut.side = 5
sut.saveArea()
saver.verify(saveCallsCount: 1, saveValue: 25)
}
class MockSaver: SaverProtocol {
private var saveCallsCount = 0
private var saveValue: Float?
func save(value: Float) {
saveCallsCount += 1
saveValue = value
}
func verify(saveCallsCount: Int, saveValue: Float) {
XCTAssertEqual(saveCallsCount, 1)
XCTAssertEqual(saveValue, 25)
}
}
你可以找到一个有趣的关于模拟对象的话题here。
掌握测试双打是迈向完美单元测试的一个好步骤。每个测试都有其复杂性,你必须弄清楚哪一个双测试更适合你的需求。