Pylot

The AIO UI testing platform

Features

Mock Server

  • Loads a postman collection
     
  • Based on the "mounted" groups, returns first response matching "path"
let path = "/mock-server/api/path"
pylot.get(path)
// returns mock response

Mock API

  • Ability to control mock server's behavior
     
  • Allows for mounting and unmounting groups
let mount = "/mock-server-api/mount"
let query = "group=foo"
pylot.post("\(mount)?\(query)")
// group "foo" from postman is now mounted


let unmount = "/mock-server-api/unmount"
pylot.post("\(unmount)?\(query)")
// group "foo" from postman is now unmounted

pylot.post(unmount)
// all groups are now unmounted

Postman Dump

  • Give a JSON with a "request" and "response"
     
  • The server will format it as a postman collection
     
  • Then write it to disk under "captured"
let json = {
  'request': {
    'path': 'test-api/somePath',
    'method': 'GET',
    'headers': {},
    'body': '{"foo":"bar"}'
  },
  'response': {
    'headers': {
      'Content-Length': '13',
      'Content-Type': 'application/json'
    },
    'body': '{"foo":"bar"}',
    'code': 200
  }
}

pylot.post("/postman-dump", body: json)
# pylot will write it to disk:
# captured.postman_collection.json

Snap Audit

  • Our screenshots auditing webapp
    (Reactjs + Firebase)
     
  • Under rapid development
    • Screenshot diffs
    • Filtering
    • Logs
    • ...

 

Integration

Request Dumping

A logger that writes to the network

var postRequest = URLRequest(url: self.url)
postRequest.httpMethod = "POST"
postRequest.httpBody = data
postRequest.setValue("\(data.count)", forHTTPHeaderField: "Content-Length")
postRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")

URLSession.shared.dataTask(with: postRequest).resume()

URL Routing

Rewriting URLs in a URL Builder

let baseURL: BaseURL

switch Network.shared.buildConfig {
case var .mock(mockBase):
    mockBase.path.append(api.id)
    baseURL = mockBase
default:
    baseURL = api.baseUrls[baseUrlIndex]
}

One possibility for the future is using a proxy to capture/reroute requests

Test Results

UI Test side integration

final class Pylot {
        
    // MARK: methods
    
    func enforceServerAvailability() {
        // ...        
    }

    func uploadResult(_ result: ResultDTO, path: String) {
        // ...        
    }

    func uploadScreenshot(_ screenshot: Data, path: String, index: Int) {
        // ...        
    }
}

// this is critical ATM .. might make it flexible in the future
private var uploadPath: String {
    return "\(featureName)/\(variantName)/\(environmentName)"
}

Writing Tests

Capturing Requests

UI Test Code

// ui test launch sequence...
Pylot.shared.enforceServerAvailability()

// a sample test class code
func testExistingUserLogin() {
    // ...
    pylot.mount(group: "foo")
    // ...
    let tablesQuery = app.tables
    tablesQuery.buttons["Button - Select Country"].tap()
    addScreenshot(app)
    // ...
}

// tearDown
override func tearDown() {
    let testResult = ResultDTO(success: testRun!.failureCount == 0)
    pylot.uploadResult(testResult, path: uploadPath)
    pylot.unmount()
    
    super.tearDown()
}

Running Tests

Test Matrix

Pushing the automation limits

#!/bin/sh

function perform_tests () {

	xcodebuild \
		-workspace "$WORKSPACE" \
		-scheme "CareemUITests" \
		-configuration "Debug" \
		-destination "platform=iOS Simulator,name=$1" \
		TEST_LANG="$2" \
		SWIFT_OPTIMIZATION_LEVEL="-Owholemodule" \
		OTHER_SWIFT_FLAGS='$(inherited) -Onone' \
		test | xcpretty -c
}

perform_tests "iPhone SE" "en"
perform_tests "iPhone SE" "ar"

# perform_tests "iPhone X" "en"
# perform_tests "iPhone X" "ar"

Quick Demo

Made with Slides.com