斯威夫特双打测试


单元测试的目的是检查一个组件的预期行为,这被称为系统测试(又名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检查它是否调用方法saveSaverProtocol仅一次,以面积为参数:

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两者都保留saveCallsCountsaveValue私人:

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

掌握测试双打是迈向完美单元测试的一个好步骤。每个测试都有其复杂性,你必须弄清楚哪一个双测试更适合你的需求。