动态模拟服务器行为和响应。
Kakapo 是一个动态模拟库。它允许您复制您的后端 API 和逻辑。
使用 Kakapo,您可以轻松基于您的 API 规范原型化您的应用程序。
在测试网络请求时的常见方法是使用来自本地文件或记录请求的假网络响应进行存根。这有一些缺点
尽管这种方法可能仍然有效,但 Kakapo 将是您网络测试中的游戏改变者:它将使您在模拟后端行为时拥有完全的控制权。此外,这不仅是单元测试:您甚至可以在拥有真实服务之前先原型化您的应用程序!
使用 Kakapo,您可以仅创建 Swift 结构体/类/枚举,它们将自动序列化为 JSON。
地球上 70 亿人口
少于 150 只 Kakapo
时间是关键 向Kakapo恢复捐赠
使用 CocoaPods
use_frameworks!
pod 'Kakapo'
注意:该项目还包含一个 README.playground。查看它以了解Kakapo的一些关键功能示例。
Kakapo采用了便于使用的设计理念。为了快速开始,您可以创建一个像这样的Router
来拦截网络请求:
let router = Router.register("http://www.test.com")
router.get("/users") { request in
return ["id" : 2, "name": "Kakapo"]
}
您可能会想知道动态部分在哪里;这里就是Kakapo的不同模块发挥作用的时候了
let store = Store()
store.create(User.self, number: 20)
router.get("/users") { request in
return store.findAll(User.self)
}
现在,我们已经创建了20个随机的User
对象,并模拟了我们的请求以返回它们。
让我们更详细地看看不同的功能
Kakapo使用Serializable
协议将对象序列化为JSON。只要类型符合此协议,任何类型都可以序列化。
struct User: Serializable {
let name: String
}
let user = User(name: "Alex")
let serializedUser = user.serialized()
// -> ["name": "Alex"]
还支持标准库类型:这意味着Array
、Dictionary
或Optional
可以序列化。
let serializedUserArray = [user].serialized()
// -> [["name": "Alex"]]
let serializedUserDictionary = ["test": user].serialized()
// -> ["test": ["name": "Alex"]]
Kakapo使用Router
来跟踪要拦截的已注册端点。
您可以使用匹配已注册基本URL的任何相对路径,只要组件与请求的组件匹配。您可以使用通配符组件。
let router = Router.register("http://www.test.com")
// Will match http://www.test.com/users/28
router.get("/users/:id") { ... }
// Will match http://www.test.com/users/28/comments/123
router.get("/users/:id/comments/:comment_id") { ... }
处理程序必须返回一个定义为响应的Serializable
对象,一旦匹配请求的URL,当Router
拦截请求时,它会自动将处理程序返回的Serializable
对象序列化并转换为Data
。
router.get("/users/:id") { request in
return ["id": request.components["id"]!, "name": "Joan"]
}
现在,您已经准备好测试模拟的API;您可以像平时一样执行请求
let session = URLSession.shared
let url = URL(string: "http://www.test.com/users/1")!
session.dataTask(with: url) { (data, _, _) in
// handle response
}.resume()
注意:查询参数不会影响路由匹配
http://www.test.com/users/1?foo=bar
也会被匹配
在前面的例子中,处理程序返回了一个简单的Dictionary
;虽然这是有效的,因为Dictionary
已经是Serializable
的,您还可以创建自己的符合Serializable
的自定义实体。
struct User: Serializable {
let firstName: String
let lastName: String
let id: String
}
router.get("/users/:id") { request in
return User(firstName: "Joan", lastName: "Romano", id: request.components["id"]!)
}
当请求匹配时,RouteHandler会接收到一个Request
对象,它代表您的请求,包括组件、查询参数、httpBody和httpHeaders。Request对象在构建动态响应时可能非常有用。
支持使用Foundation网络API的第三方库,但您可能需要设置适当的URLSessionConfiguration
。
例如,要设置 Alamofire
:
let configuration = URLSessionConfiguration.default
configuration.protocolClasses = [Server.self]
let sessionManager = SessionManager(configuration: configuration)
当使用存档与人共同rPid>来使用您的Router时,Kakapo将变得更加强大。您可以创建、插入、删除、更新或查找对象。
这使得您可以模拟API的行为,就像使用真实后端一样。这是Kakapo的 动态 部分。
要创建可以用于存档的实体,您的类型需要符合Storable
协议。
struct Article: Storable, Serializable {
let id: String
let text: String
init(id: String, store: Store) {
self.id = id
self.text = randomString() // you might use some faker library like Fakery!
}
}
一个例子用法是获取特定的Article
:
let store = Store()
store.create(Article.self, number: 20)
router.get("/articles/:id") { request in
let articleId = request.components["id"]!
return store.find(Article.self, id: articleId)
}
当然,您可以执行符合您需求的任何逻辑.
router.post("/article/:id") { request in
return store.insert { (id) -> Article in
return Article(id: id, text: "text from the body")
}
}
router.del("/article/:id") { request in
let articleId = request.components["id"]!
let article = store.find(Article.self, id: articleId)!
try! store.delete(article)
return ["status": "success"]
}
在可序列化中,我们描述了如何序列化您的类。默认情况下,序列化将通过Mirror
(使用Swift的反射)递归地序列化其实例的属性。
当需要不同的行为时,您可以改为实行CustomSerializable
以提供自定义的序列化。
例如,Array
使用CustomSerializable
返回一个包含其序列化元素的Array
。类似地,Dictionary
通过创建一个具有相同键和序列化值的Dictionary
进行序列化。
有关CustomSerializable
的其他示例以及如何使用它创建更复杂的序列化,请查看JSONAPISerializer
实现。
由于Kakapo在设计时考虑了JSONAPI支持,JSONAPISerializer
能够将您的实体序列化为符合jsonapi.org的JSON。
为了符合JSONAPI的序列化,您的实体需要符合JSONAPIEntity
协议。
让我们看看一个例子
struct Cat: JSONAPIEntity {
let id: String
let name: String
}
struct User: JSONAPIEntity {
let id: String
let name: String
let cats: [Cat]
}
请注意,JSONAPIEntity
对象已经是Serializable
的,并且您可以与您的Routers一起使用它们。然而,为了完全遵循JSONAPI结构在您的响应中,您应该将它们包装到JSONAPISerializer
结构中
router.get("/users/:id") { request in
let cats = [Cat(id: "33", name: "Joan"), Cat(id: "44", name: "Hez")]
let user = User(id: "11", name: "Alex", cats: cats)
return JSONAPISerializer(user)
}
在将数据序列化为JSON时,您可能希望将属性值表示为null
。为此,您可以使用PropertyPolicy
枚举。它类似于Optional
,提供了一个额外的.null
情况
public enum PropertyPolicy<Wrapped>: CustomSerializable {
case None
case Null
case Some(Wrapped)
}
它的唯一目的是以3种不同的方式序列化,以涵盖Optional
属性的所有可能行为。PropertyPolicy
的工作方式与Optional
属性完全相同
.none
-> 属性不包含在序列化中.some(wrapped)
-> 序列化wrapped
附加情况.null
,在转换为json时序列化为null
。
PropertyPolicy<Int>.none.serialized() // nil
PropertyPolicy<Int>.null.serialized() // NSNull
PropertyPolicy<Int>.some(1).serialized() // 1
序列化生成的JSON键直接反映了您实体的属性名称。然而,您可能需要不同的行为。例如,许多API使用snake_case
键,但几乎所有人都使用Swift中的camelCase
属性。
要转换键,可以使用SerializationTransformer
。遵守此协议的对象能够在序列化时间转换包装对象的键。
对于具体的实现,请查看SnakecaseTransformer
:一个实现SerializationTransformer
的结构体,用于将键转换为蛇形命名
let user = User(userName: "Alex")
let serialized = SnakecaseTransformer(user).serialized()
print(serialized) // [ "user_name" : "Alex" ]
如果您的响应需要指定状态码(默认将为200)和/或头部字段,则可以利用ResponseFieldsProvider
来自定义您的响应。
Kakapo在Response
结构体中提供了默认的ResponseFieldsProvider
实现,您可以使用它来包裹您的Serializable
对象
router.get("/users/:id"){ request in
return Response(statusCode: 400, body: user, headerFields: ["access_token" : "094850348502"])
}
let url = URL(string: "http://www.test.com/users/2")!
session.dataTaskWithURL() { (data, response, _) in
let allHeaders = response.allHeaderFields
let statusCode = response.statusCode
print(allHeaders["access_token"]) // 094850348502
print(statusCode) // 400
}.resume()
否则,您的Serializable
对象可以直接实行协议:查看JSONAPIError
以获取另一个示例。
尽管Kakapo已经准备好使用,但它并不打算发送到App Store,尽管你也可以这样做!实际上,你可能会在一些苹果店内看到它的实际应用,因为曾用它来模拟Runtastic演示应用的某些功能;然而,它还处于早期阶段,我们很乐意听取您的意见。如果您有任何问题、反馈或想提出新功能,我们鼓励您打开一个issue。
请确保检查我们用Kakapo创建的演示应用:一个原型新闻源应用,允许用户创建新帖子并点赞/取消点赞。
要快速尝试,请使用:pod try Kakapo
@MP0w - @zzarcon - @joanromano