Kakapo
🐤Dynamically Mock server behaviors and responses in Swift
Install / Use
/learn @devlucky/KakapoREADME
Kakapo 
Dynamically Mock server behaviors and responses.
Contents
Kakapo is a dynamic mocking library. It allows you to replicate your backend APIs and logic.
With Kakapo you can easily prototype your application based on your API specifications.
Why Kakapo?
A common approach when testing network requests is to stub them with fake network responses from local files or recorded requests. This has some disadvantages:
- All files need to be updated when the APIs are updated.
- Lots of files have to be generated and included in the project.
- Are just static responses that can only be used for unit tests since they don't reflect backend behaviors and state.
While still this approach may work good, Kakapo will be a game changer in your network tests: it will give you complete control when it comes to simulating backend behaviors. Moreover, is not just unit testing: you can even take a step further and prototype your application before having a real service behind!
With Kakapo you can just create Swift structs/classes/enums that are automatically serialized to JSON.
7 billion people on Earth
Less than 150 Kakapo
Time is critical donate to Kakapo recovery
Features
- Dynamic mocking
- Prototyping
- Swift 3.0 compatible (from version 2.0.0, master branch)
- Swift 2.2 and 2.3 compatible (from version 1.0.0, branch feature/legacy-swift)
- Compatible with
- Protocol oriented and pluggable
- Fully customizable by defining custom serialization and custom responses
- Out-of-the-box serialization
- JSONAPI support
Installation
Using CocoaPods:
use_frameworks!
pod 'Kakapo'
Using Carthage:
github "devlucky/Kakapo"
Usage
NOTE: The project also contains a README.playground. Check it out to see some examples of the key features of Kakapo.
Kakapo is made with an easy-to-use design in mind. To quickly get started, you can create a Router that intercepts network requests like this:
let router = Router.register("http://www.test.com")
router.get("/users") { request in
return ["id" : 2, "name": "Kakapo"]
}
You might be wondering where the dynamic part is; here is when the different modules of Kakapo take place:
let store = Store()
store.create(User.self, number: 20)
router.get("/users") { request in
return store.findAll(User.self)
}
Now, we've created 20 random User objects and mocked our request to return them.
Let's get a closer look to the different features:
Serializable protocol
Kakapo uses the Serializable protocol in order to serialize objects to JSON. Any type can be serialized as long as it conforms to this protocol:
struct User: Serializable {
let name: String
}
let user = User(name: "Alex")
let serializedUser = user.serialized()
// -> ["name": "Alex"]
Also, standard library types are supported: this means that Array, Dictionary or Optional can be serialized:
let serializedUserArray = [user].serialized()
// -> [["name": "Alex"]]
let serializedUserDictionary = ["test": user].serialized()
// -> ["test": ["name": "Alex"]]
Router - Register and Intercept
Kakapo uses Routers in order to keep track of the registered endpoints that have to be intercepted.
You can match any relative path from the registered base URL, as long as the components are matching the request's components. You can use wildcard components:
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") { ... }
The handler will have to return a Serializable object that will define the response once the URL of a request is matched.
When a Router intercepts a request, it automatically serializes the Serializable object returned by the handler and converts it to Data.
router.get("/users/:id") { request in
return ["id": request.components["id"]!, "name": "Joan"]
}
Now everything is ready to test your mocked API; you can perform your request as you usually would do:
let session = URLSession.shared
let url = URL(string: "http://www.test.com/users/1")!
session.dataTask(with: url) { (data, _, _) in
// handle response
}.resume()
Note: query parameters are not affecting the route match
http://www.test.com/users/1?foo=barwould also be matched
In the previous example the handler was returning a simple Dictionary; while this works because Dictionary is already Serializable, you can also create your own entities that conform to 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"]!)
}
When a request is matched, the RouteHandler receives a Request object that represents your request including components, query parameters, httpBody and httpHeaders. The Request object can be useful when building dynamic responses.
Third-Party Libraries
Third-Party libraries that use the Foundation networking APIs are also supported but you might need to set a proper URLSessionConfiguration.
For example, to setup Alamofire:
let configuration = URLSessionConfiguration.default
configuration.protocolClasses = [Server.self]
let sessionManager = SessionManager(configuration: configuration)
Leverage the Store - Dynamic mocking
Kakapo gets even more powerful when using your Routers together with the Store. You can create, insert, remove, update or find objects.
This lets you mock the APIs behaviors as if you were using a real backend. This is the dynamic side of Kakapo.
To create entities that can be used with the store, your types need to conform to the Storable protocol.
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!
}
}
An example usage could be to retrieve a specific 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)
}
Of course you can perform any logic which fits your needs:
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"]
}
CustomSerializable
In Serializable we described how your classes can be serialized. The serialization, by default, will Mirror (using Swift's reflection) an entity by recursively serializing its properties.
Whenever a different behavior is needed, you can instead conform to CustomSerializable to provide your custom serialization.
For instance, Array uses CustomSerializable to return an Array containing its serialized elements. Dictionary, similarly, is serialized by creating a Dictionary with the same keys and serialized values.
For other examples of CustomSerializable and how to use it to create more complex serializations, take a look at the JSONAPISerializer implementation.
JSONAPI
Since Kakapo was built with JSONAPI support in mind, JSONAPISerializer is able to serialize your e
