午夜测试 (Midnight Test)

午夜测试是一个帮助创建 Kitura 驱动的网站和服务的测试库。 午夜测试通过使 Kitura 项目的测试编写变得简单、快速和有趣,从而促进良好的测试驱动开发实践。

使用午夜测试

使用午夜测试很可能会稍微改变您构建 Kitura 站点的方式。 一般的想法是,您向项目中添加一个新目标,其中包含您站点的所有逻辑,包括一个返回已构建的 Router 对象的功能。 然后,您将“main”目标更改为仅使用该 Router 对象启动 Kitura。 接着,添加一个新的测试目标,其中将午夜测试作为依赖项,它也将使用该 Router 对象在运行测试时启动 Kitura。

例如,让我们使用 Kitura Until Dawn 第一章中的“Hello, World!”示例,它的 Package.swift 文件如下所示:

// swift-tools-version:4.0
​
import PackageDescription
​
let package = Package(
    name: "HelloWorld",
    dependencies: [
        .package(url: "https://github.com/IBM-Swift/Kitura.git", from: "2.0.0")
    ],
    targets: [
        .target(
            name: "HelloWorld",
            dependencies: ["Kitura"]),
    ]
)

其 Sources/main.swift 文件如下所示:

import Kitura
​
let router = Router()
​
router.get("/") { request, response, next in
    response.send("Hello world!\n")
    next()
}Kitura.addHTTPServer(onPort: 8080, with: router)
Kitura.run()

为了使其可测试,首先我们在 Package.swift 中创建另一个名为 HelloWorldApp 的目标,并将我们的主 HelloWorld 目标拥有的依赖项移动到 HelloWorldApp。 然后我们使 HelloWorld 依赖于 HelloWorldApp。 我们的 Package.swift 中的 targets 参数现在如下所示:

    targets: [
        .target(
            name: "HelloWorld",
            dependencies: ["HelloWorldApp"]),
        .target(
            name: "HelloWorldApp",
            dependencies: ["Kitura"]),
    ]

现在,我们在 Sources 目录中创建一个以我们新目标命名的目录,并在该目录中创建一个同名的 Swift 文件,因此是 Sources/HelloWorldApp/HelloWorldApp.swift。(如果您使用的是 Xcode,现在应该可以运行 swift package generate-xcodeproj 以在文件侧边栏中看到此文件。)在其中,我们将创建一个新类,其中包含一个返回我们的路由器逻辑的方法。

import Kitura

public class HelloWorldApp {

    public init() {
        // Nothing to do at initialization in this test site, but here you could
        // do things like reading configuration files, connecting to databses,
        // etc.
    }

    public func generateRouter() -> Router {
        let router = Router()

        router.get("/") { request, response, next in
            response.send("Hello world!\n")
            next()
        }

        return router
    }
}

现在我们回到 Sources/HelloWorld/main.swift 并将其更改为如下所示:

import Kitura
import HelloWorldApp

let hello = HelloWorldApp()
let router = hello.generateRouter()

Kitura.addHTTPServer(onPort: 8080, with: router)
Kitura.run()

如果我们现在构建并运行我们的站点,它应该像以前一样运行。

现在我们添加测试目标。 再次打开 Package.swift 并添加对午夜测试的依赖。 在 targets 部分中,添加一个依赖于 MidnightTest 以及 HelloWorldApp 的 testTarget。 我们的 Package.swift 现在如下所示:

// swift-tools-version:4.0

import PackageDescription

let package = Package(
    name: "HelloWorld",
    dependencies: [
        .package(url: "https://github.com/IBM-Swift/Kitura.git", from: "2.0.0"),
        .package(url: "https://github.com/NocturnalSolutions/MidnightTest.git", from: "0.0.1"),
    ],
    targets: [
        .target(
            name: "HelloWorld",
            dependencies: ["HelloWorldApp"]),
        .target(
            name: "HelloWorldApp",
            dependencies: ["Kitura"]),
        .testTarget(
            name: "HelloWorldTests",
            dependencies: ["HelloWorldApp", "MidnightTest"]),
        ]
)

为了确保测试在 Linux 上工作,我们在空的 Tests 目录中创建一个名为 LinuxMain.swift 的文件,并为其提供以下代码:

import XCTest
@testable import HelloWorldTests

XCTMain([
    testCase(HelloWorldTests.allTests)
])

最后,在 Tests 目录中,创建一个名为 HelloWorldTests 的目录,并在其中创建 HelloWorldTests.swift。 现在所有的准备工作都完成了,精彩的部分来了……

编写测试

MidnightTestCase 类是 XCTestCase 的子类,可用于以非常相似的方式定义您的测试。 您需要重写 setUp() 方法,并使用应用程序的 Router 对象设置 router 属性,并为 requestOptions 提供一个 ClientRequest.Options 数组,以构建对测试站点的查询。 一个简单的方法是使用 ClientRequest.parse() 方法从 URL 构建这样的数组,但我建议您查看 ClientRequest.Options 的代码以查看您还有哪些其他选项。

import HelloWorldApp
import MidnightTest
import KituraNet

class HelloWorldTests: MidnightTestCase {

    public override func setUp() {
        router = HelloWorldApp().generateRouter()
        requestOptions = ClientRequest.parse("https://:8080/")
        super.setUp()
    }
}

现在在您的测试方法中,您基本上想要调用 testResponse 方法或其众多重载之一,至少包含两个参数:一个路径,以及一个或多个 ResponseCheckerResponseChecker 是一个闭包,它接受两个参数:一个包含从服务器获取的响应正文的 Data 和从中提取数据的原始 ClientResponse 对象。 因此,让我们检查一下我们站点的“/”路径是否给了我们 200 OK 的 HTTP 状态码,并且其正文文本包含“Hello world!”消息。

    public func testFrontPage() {
        testResponse("/") { body, response in
            XCTAssertEqual(response.statusCode, HTTPStatusCode.OK, "Unexpected status code found in response.")
            guard let bodyString = String(data: body, encoding: .utf8) else {
                XCTFail("Could not convert response data to String.")
                return
            }
            XCTAssert(bodyString.contains("Hello world!"), "Response body did not contain \"Hello world!\"")
        }
    }

在 HelloWorldTests 类中添加一个 allTests 静态变量,其中包含所有测试方法的列表。(我们现在只有一个,但其他的应该具有相同的格式。)

    public static var allTests = [
        ("testFrontPage", testFrontPage),
    ]

现在尝试通过在 CLI 中运行 swift test 或单击 Xcode 中的“测试”按钮(如果 Xcode 给您一个烦人的错误消息,请参见下文)在 CLI 中运行您的项目测试。 您的测试应该运行并且(希望)通过!

但是等等! 我们可以让我们的测试代码更短,因为 Midnight Test 包含两个用于生成回调以检查 HTTP 状态代码和正文文本的方法,分别名为 checkStatus()checkString()。 所以上面的可以变成

    public func testFrontPage() {
        testResponse("/", checker: checkStatus(.OK), checkString("Hello world!"))
    }
}

(将来,我希望 Midnight Test 会有更多用于其他常见任务的“生成器”,例如检查响应标头、检查 JSON 或 XML 响应中的值等等。)

此示例仅发出一个简单的 GET 请求,因为这就是我们的 Hello World 站点所支持的全部。 但当然,Midnight Test 支持其他类型的请求,并允许您指定自定义请求标头和正文数据。 请参阅 testResponse 重载以了解更多信息。 您还可以查看 testPostResponse 函数,该函数接受一个 [String: [String]?] 数据以执行 POST 请求,并且可以自动将其格式化为多部分和 URL 编码。

玩得开心!

在 Xcode 中运行测试

如果您正在使用 Xcode 并尝试此时通过 Xcode 运行测试,您将收到此令人讨厌的消息。

Xcode sheet: "Configure 'HelloWorld' for testing."

要解决此问题,请单击“Edit Scheme…”按钮。 在下一个工作表中,左侧列表中选择了“Test”方案,并且打开了“Info”选项卡,单击左下角的小加号图标,如下图所示。

Xcode sheet showing info of the "Test" scheme

在下一个工作表中,只需选择“HelloWorldTests”目标(它应该是列表中唯一的那个),然后单击“添加”按钮。

Target selection sheet

然后单击“测试”,您的测试应该开始运行。

待办事项