Dynamically Mock server behaviors and responses.
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.
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:
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
Using CocoaPods :
use_frameworks!
pod 'Kakapo'
NOTE: The project also contains aREADME.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 db = KakapoDB() db.create(User.self, number: 20) router.get("/users") { request in return db.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:
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.serialize() print(serializedUser["name"]) // Alex
Also, standard library types are supported: this means that Array
, Dictionary
or Optional
can be serialized:
let serializedUserArray = [user].serialize() // -> [["name": "Alex"]] let serializedUserDictionary = ["test": user].serialize() // -> ["test": ["name": "Alex"]]
Kakapo uses Router
s 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:
session.dataTaskWithURL(NSURL(string: "http://www.test.com/users/1")!) { (data, _, _) in // handle response }.resume()
Note: query parameters are not affecting the route match http://www.test.com/users/1?foo=bar
would 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: Storable, 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 parmaters, HTTPBody and HTTPHeaders. The Request
object can be useful when building dynamic repsonses.
Third-Party libraries that use the Foundation networking APIs are also supported but you might need to set a proper NSURLSessionConfiguration
. For example, to setup Alamofire
:
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration() configuration.protocolClasses = [KakapoServer.self] let manager = Manager(configuration: configuration)
Kakapo gets even more powerful when using your Routers together with the Database. 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 database, your types need to conform to the Storable
protocol.
struct Articles: Storable, Serializable { let id: String let text: String init(id: String, db: KakapoDB) { 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 db = KakapoDB() db.create(Article.self, number: 20) router.get("/articles/:id") { request in let articleId = request.components["id"] return db.find(Article.self, id: articleId) }
But, of course, you could perform any logic which fits your needs:
router.post("/article/:id") { request in return db.insert { (id) -> User in return Article(text: "text from body", id: id) } } router.del("/article/:id") { request in let articleId = request.components["id"] let article = db.find(Article.self, id: articleId) db.delete(article) return ["status": "success"] }
InSerializablewe 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.
Since Kakapo was built with JSONAPI support in mind, JSONAPISerializer
is able to serialize your entity into JSON conforming to jsonapi.org .
Your entities, in order to be serialized conforming to JSONAPI, need to conform to JSONAPIEntity
protocol.
Let's see an example:
struct Cat: JSONAPIEntity { let id: String let name: String } struct User: JSONAPIEntity { let id: String let name: String let cats: [Cat] }
Note that JSONAPIEntity
objects are already Serializable
and you could just use them together with your Routers. However, to completely follow the JSONAPI structure in your responses, you should wrap them into a JSONAPISerializer
struct:
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) }
When serializing to JSON, you may want to represent a property value as null
. For this, you can use the PropertyPolicy
enum. It is similar to Optional
, providing an additional .Null
case:
public enum PropertyPolicy<Wrapped>: CustomSerializable { case None case Null case Some(Wrapped) }
It's only purpose is to be serialized in 3 different ways, to cover all possible behaviors of an Optional property. PropertyPolicy
works exactly as Optional
properties:
.None
-> property not included in the serialization .Some(wrapped)
-> serialize wrapped
The additional case , .Null
, is serialized as null
when converted to json.
PropertyPolicy<Int>.None.serialize() // nil PropertyPolicy<Int>.Null.serialize() // NSNull PropertyPolicy<Int>.Some(1).serialize() // 1
The keys of the JSON generated by the serialization are directly reflecting the property names of your entities. However, you might need different behaviors. For instance, many APIs use snake_case
keys but almost everyone use camelCase
properties in Swift.
To transform the keys you can use SerializationTransformer
. Objects conforming to this protocol are able to transform the keys of a wrapped object at serialization time.
For a concrete implementation, check SnakecaseTransformer
: a struct that implements SerializationTransformer
to convert keys into snake case:
let user = User(userName: "Alex") let serialized = SnakecaseTransformer(user).serialize() print(serialized) // [ "user_name" : "Alex" ]
If your responses need to specify status code (which will be 200 by default) and/or header fields, you can take advantage of ResponseFieldsProvider
to customize your responses.
Kakapo provides a default ResponseFieldsProvider
implementation in the Response struct, which you can use to wrap your Serializable objects:
router.get("/users/:id"){ request in return Response(statusCode: 400, body: ["id" : 2], headerFields: ["access_token" : "094850348502"]) } session.dataTaskWithURL(NSURL(string: "http://www.test.com/users/2")!) { (data, response, _) in let allHeaders = response.allHeaderFields let statusCode = response.statusCode print(allHeaders["access_token"]) // 094850348502 print(statusCode) // 400 }.resume()
Otherwise your Serializable
object can directly implement the protocol: take a look at JSONAPIError
to see another example.
Even though Kakapo is ready to use, it is not meant to be shipped to the App Store although you can also do it! In fact, you might see it in action in some Apple stores since it was used to mock some features of Runtastic's demo app; however, it's at its early stage and we would love to hear your thoughts. We encourage you to open an issue if you have any questions, feedbacks or you just want to propose new features.
Make sure you check thedemo app we created using Kakapo: a prototyped newsfeed app which lets the user create new posts and like/unlike them.
@MP0w -@zzarcon -@joanromano