SwiftNIOMock

一个基于 SwiftNIO 的 Web 服务器,旨在用作 UI 自动化测试中的 Mock 服务器。

在针对真实服务器运行 UI 测试时,可能会出现一些问题:网络可能不稳定,远程服务器上的内容可能会更改,某些测试场景可能需要在某些外部系统中执行操作,并且通常进行网络调用会减慢所有测试的速度。

SwiftNIOMock 旨在通过提供一个在 localhost 上运行的 Mock Web 服务器实现来解决这些问题,应用程序在 UI 测试下运行时应访问该服务器而不是真实的服务器。与其他解决方案(例如使用 URLProtocol 注册自定义协议)不同,使用 Mock 服务器只需要将应用程序切换到 localhost,并为您提供更大的灵活性和控制权,因为服务器由测试控制,并且您可以从测试场景中更改其状态。

SwiftNIOMock 支持三种常见场景

用法

在测试中,创建服务器的实例并在 setUp 方法中启动它,并为其提供一个中间件来处理请求。 在 tearDown 方法中停止服务器。

override func setUp() {
    server = Server(port: 8080, handler: <#Middleware#>)
    try! server.start()
}

override func tearDown() {
    try! server.stop()
    server = nil
}

中间件是一个函数,它基于传入的请求可以修改响应。

typealias Middleware = (
    _ request: Server.HTTPHandler.Request,
    _ response: Server.HTTPHandler.Response,
    _ next: @escaping () -> Void
) -> Void

当中间件函数完成响应后,它应该调用传递给它的 next 闭包(可以异步调用)以将控制权返回给服务器。 您可以编写自己的中间件,也可以使用 SwiftNIOMock 提供的中间件,包括 redirectrouterdelay。 这是一个简单的中间件示例,它会回显任何传入的请求

func echo(
    request: Server.HTTPHandler.Request,
    response: Server.HTTPHandler.Response,
    next: @escaping () -> Void
) {
    response.statusCode = .ok
    response.body = request.body
    next()
}

redirect 中间件允许您重定向传入的请求并拦截响应。 它还接受 URLSession 的实例(默认情况下为 URLSession.shared 会话),它将使用该实例来执行请求,从而允许它记录和重放所有请求。 查看 SwiftNIOMockExampleUITests.swift 以查看如何在 Vinyl 支持的记录和重放模式下使用 SwiftNIOMock 的示例。

func redirect(
    session: URLSession = URLSession.shared,
    request override: @escaping (Server.HTTPHandler.Request) -> Server.HTTPHandler.Request,
    response intercept: @escaping (Server.HTTPHandler.Response) -> Void = { _ in }
) -> Middleware

router 中间件允许您根据请求(即其方法和路径)返回任意中间件。 如果无法处理请求,则将使用作为 notFound 参数传递的备用中间件。

func router(
    route: @escaping (Server.HTTPHandler.Request) -> Middleware?,
    notFound: @escaping Middleware
) -> Middleware

要查看在 UI 测试中使用 SwiftNIOMock 的示例,请查看 SwiftNIOMockExample。

路由

虽然您可以使用 router 函数为 Mock 服务器创建路由中间件,但在实践中,最好将其分解为更小的服务。为此,您可以使用 Service 类型

let fooService = Service {
    route({ $0.head.uri == "/foo" }) { (request, response, next) in
        response.sendString(.ok, value: "Foo")
        next()
    }
}
let barService = Service {
    route({ $0.head.uri == "/bar" }) { (request, response, next) in
        response.sendString(.ok, value: "Bar")
        next()
    }
}

let router = SwiftNIOMock.router(services: [
    fooService,
    barService
])

您也可以通过继承 Service 来实现您自己的服务

class HelloService: Service {
    override init() {
        super.init()
        routes {
            route({ $0.head.uri == "/helloworld" }) { (request, response, next) in
                response.sendString(.ok, value: "Hello World!")
                next()
            }
        }
    }
}
let helloService = HelloService()
let router = SwiftNIOMock.router(services: [helloService])

您可以像上述示例中那样使用闭包谓词注册路由,也可以使用 URLFormat

import URLFormat

routes {
    route(GET/.helloworld) { (request, response, next) in
        response.sendString(.ok, value: "Hello World!")
        next()
    }
}

您还可以使用简写语法将路由绑定到具有 Encodable 返回值类型的服务 KeyPath 或函数

class HelloService: Service {
    let users: [String]
    
    func user(byName name: String) -> String? {
        users.first { $0 == name }
    }
    
    init(users: [String]) {
        self.users = users
        super.init()
        
        routes {
            GET/.users == \HelloService.users
            GET/.users/.string == HelloService.user(byName:)
        }
    }
}

由于路由绑定到键路径或函数,您可以更改服务的状态,这些更改将反映在后面的响应中。

将路由绑定到函数时,其参数类型必须与 URLFormat 类型匹配,即 URLFormat<((String, String), Int)> 只能绑定到函数 (String, String, Int) -> T where T: Encodable。 此外,您可以添加一个 Server.HTTPHandler.Request 参数作为函数的最后一个参数,以访问请求正文数据。

安装

import PackageDescription

let package = Package(
    dependencies: [
        .package(url: "https://github.com/ilyapuchka/SwiftNIOMock.git", .branch("master")),
    ]
)