GithubHelp home page GithubHelp logo

kanecheshire / peasy Goto Github PK

View Code? Open in Web Editor NEW
38.0 3.0 10.0 669 KB

A pure Swift mock server for embedding and running directly within iOS/macOS UI tests. Easy peasy.

License: MIT License

Swift 98.25% Ruby 1.75%
ios macos xcui xcuitest xctest swift

peasy's Introduction

Peasy

Actions Status Version License Platform

Closed Issues Open Issues

A lightweight mock server written purely in Swift, that you can run directly in your UI tests, with no need for any external process to be spun up as part of the tests. 🎉

Quick start

Simply create and start a server in your tests, then tell it what to respond with for requests:

import XCTest
import Peasy

class MyUITest: XCTestCase {

  let server = Server()

  override func setUp() {
    server.start() // Default port is 8880
    let ok = Response(status: .ok)
    server.respond(with: ok, when: .path(matches: "/"))
  }

  func test_someStuff() {
    // Run your tests that cause the app to call http://localhost:8880/
  }

}

Starting and stopping the server

To start a Peasy server, just call start:

let server = Server()
server.start()

By default, Peasy starts the server on whatever port is available on the system, but you can override this and choose any specific port you want:

server.start(port: 8080)

NOTE: Starting two servers on the same port is not supported and will fail.

NOTE: iOS simulators share the same network as your Mac, so you can communicate directly with your Peasy server from Terminal, Safari or Postman on your Mac.

To stop a server (i.e. when tests finish), just call stop:

server.stop()

Configuring responses

By default Peasy doesn't know how to respond to any requests made to it.

You'll need to tell Peasy what to respond with, which you do by building up a set of rules that must be valid for an incoming request:

let response = Response(status: .ok)
server.respond(with: response, when: .path(matches: "/"))

You can provide multiple rules to filter responses further, for example, only matching paths "/" for GET requests:

server.respond(with: response, when: .path(matches: "/"), .method(matches: .get))

Now, whenever Peasy receives a GET request matching the root path of "/", it will respond with an empty response of 200 OK.

If none of Peasy's built in rules work for you, you can always provide a custom handler:

let customRule: Rule = .custom { request in
  return request.path.contains("/common/path")
}
server.respond(with: response, when: customRule)

Default and override responses

It's common to want to set a "default" response to a request, but sometimes override it.

For example, in a UI test, you might want to set the default response as happy path, but then test that if the request is made again what happens if you get a different response.

Peasy supports this by allowing you to indicate whether a set of rules are removed after they're matched with removeAfterResponding:

server.respond(with: happyPathResponse, when: .path(matches: "/api")) // removeAfterResponding defaults to false, so this will persist

server.respond(with: unhappyPathResponse, when: .path(matches: "/api"), removeAfterResponding: true) // This will match before the happy path response and will be removed after responding

In the case of multiple configurations matching a request (as above), Peasy will use the last set one (the unhappy path one above). Since we're also telling the unhappy path response to be removed after responding, Peasy will then carry on matching the first configuration (the happy path) until a new override response is set.

Delaying requests

You might want to slow responses down on your Peasy servers, for example to write a UI test to test what happens when a request times out.

Peasy makes this really easy, just provide a TimeInterval when configuring your responses:

server.respond(with: response, when: .path(matches: "/api"), delay: 60)

By default, Peasy will respond immediately to requests you've told it to respond to. You must provide an explicit delay if you want a delay.

Intercepting requests

There might be times when you want to know when Peasy has received a request so you you know how to respond or take some other action, like track certain requests (i.e. analytics).

Peasy supports this by allowing you to provide a handler to return a response where the request is provided to you as an argument:

var analytics: [Request] = []
server.respond(with { request in
  analytics.append(request)
  return response
}, when: .path(matches: "/analytics-event"))

Wildcards and variables in paths

It's common to use wildcards and variables in paths that may be dynamic. Peasy supports this by allowing you to indicate which parts of the path can be dynamic with the :variable syntax:

server.respond(with: response, when: .path(matches: "/constant/:variable"))

The name after : can be anything you like, but that path component must exist otherwise the rule will fail to match (i.e. "/constant/" is not valid, but "/constant/value" is).

If you want to get the value of a variable you can do so using a key-value subscript on the request:

server.respond(with { request in
  print("The value is", request["variable"])
  return response
}, when: .path(matches: "/constant/:variable"))

FAQs

Q: Does Peasy run on a real device?

A: Yes! Peasy uses low level Darwin APIs built into the open-source bits of iOS and macOS.


Q: Can this ever break?

A: Technically yeah, but these APIs have been around forever and haven't been deprecated.


Q: Can I use this in my app for the App Store?

A: Peasy is designed for UI tests but also works in regular apps. Peasy doesn't use any private APIs, so shouldn't be rejected, but it will also depend on what you're using Peasy for.

Credits

Peasy was hugely inspired by Envoy's Embassy server, but it does too much and is far too complicated for what I want to achieve. Peasy's interface is designed to be the simplest and most accessible it can be.

Without Envoy's hard work, Peasy would not exist.

peasy's People

Contributors

chaoscoder avatar corteggo-roche avatar kanecheshire avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

peasy's Issues

Stopping and restarting a server on the same address crashes

The way it's currently set up (with not great error handling) is that if you stop and immediately restart the server with the same port and address it crashes.

Although Peasy does close ports when they're deinitialized, there's some odd behaviour with the InputLoop which stops it from closing immediately.

Probably easily rectified by explicitly closing things rather than waiting for deinits.

Handle port in use errors better

If the port is in use it would be better to let people know rather than fatalErroring so they can change the port.

Generally errors need to be handled better throughout but this is the first step.

Load JSON body data from file

It would be nice if you could load JSON from a file to respond with, like so:

extension Response {
    public init(status: Status, headers: [Header] = [], bodyFromJSONFile: String) {
        // load body data as JSON from given file
    }
}

Use unused ports

As I don't control all the used ports on the system that runs the ui tests, it would be great to just use the next unused port and ask the server about the port it used. This would make parallel UI testing with one server per UI test more reliable.

E.g. usage like this:

let server = Server()
server.start(port: 0) // indicate we want to use a port the system chooses for us. Method name could be different.
let serverAddress = "http://localhost:\(server.port)" // Server needs a new property e.g. `var port: Int?` that gets set when start is called.

let app = XCUIApplication()
app.launchArguments.append(contentsOf: ["-SERVER_ADDRESS", serverAddress]) // use serverURL to pass to app as the server url

Is that possible?

Reverse order of matched configurations

As mentioned in #21, it's more beneficial if configs are matched in reverse order, so that you can set up a set of "defaults", and then add overrides that are removed after they're matched, so it falls back to the defaults after.

The reason I haven't approved that PR is because I want to spend some time and make sure I get the API right.

Improve Loop usage

Drawing it out, the loop is only needed in two places:

  • Server for incoming connection events
  • Transport for requests and responses

Currently, we create one Loop in Server, and then pass that into Connections which don't use it directly but pass it to its Transport.

Wondering if it's even needed and whether we could just use a regular DispatchQueue async call on a queue.

Split Peasy up into raw server and API

Currently everything is bundled up as one lib which includes the code to handle sockets, and the nice API on top of that to handle intercepting requests and responding etc.

It would be beneficial for the socket stuff to be broken out into a separate reusable lib so that any API can be built on top of it. Peasy's API is nice but may not suit everyone, and also restricts the way that new APIs must be written (for example I've had to really think about the way websockets work and fit nicely with Peasy's existing API).

The socket lib (name pending lol) will essentially be a wrapper around the core Darwin APIs to listen for incoming socket requests, keep them open and handle reading from and writing to a socket.

Requests will fail to parse if header end indicator is split over two events

Currently the parser expects the double line break that indicates the header section is complete is sent in one chunk of data, so if it happens to have been chunked so the line break is split over two chunks the parsing will fail.

It’s unlikely to happen but possible and would be one of those things that’s super hard to debug when tests randomly fail using Peasy.

Luckily it’s easy to add unit tests to cover this.

queryParameters fields are inaccessible due to 'internal' protection level

Not sure if an issue or if im just approaching this wrong. but i am unable to use the the queryParameters of the request due to its protection level is internal.

        let customRule: Server.Rule = .custom { request in
            print(request.queryParameters)
            let id = request.queryParameters.first(where: {$0.name == "id"})?.value
            return id == "1"
         }

'name' is inaccessible due to 'internal' protection level
'value' is inaccessible due to 'internal' protection level

DarwinError() - Operation not permitted - MacOS - UITests target

I got an issue "operation not permitted"

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.security.app-sandbox</key>
	<true/>
	<key>com.apple.security.network.client</key>
	<true/>
	<key>com.apple.security.network.server</key>
	<true/>
</dict>
</plist>

and if I run:

server = Server()
server.start(port: 8880)

from main target it works, but from uiTests target it always fails.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.