Mock server inside native iOS tests

Alexey Alter-Pesotskiy
testableapple
Published in
4 min readFeb 18, 2022

This Note originally published on my Personal Blog here. Read original note so that you won’t miss any content.

Introduction

Automation testing can be a tricky thing, especially when you’re dealing with an unstable or constantly changing backend. What if we could get rid of the real backend and use a mock server in our tests instead? Perhaps that would solve a couple of our problems, eh?

Precondition

I’ll use Swifter just because I love it and it’s super easy to start hacking right away with it. But really there are a bunch of similar tools out there, so grab any other if Swifter does not suit you.

Create sample project

  1. Open Xcode
  2. Click on Create a new Xcode project
  3. Choose iOS App as a template
  4. Fill in the product name and click on the Next button
  5. Open the ContentView.swift file
  6. Replace an existed variable body with:
var body: some View {
Button("🚢") {
let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
guard let data = data else { return }
print(String(data: data, encoding: .utf8)!)
}
task.resume()
}.accessibility(identifier: "ferry")
}

7. Run an app on any iOS Simulator and tap on the ferry

You should get something like this

Install Swifter

  1. Open Xcode
  2. Go to File => Add Packages...
  3. Type https://github.com/httpswift/swifter.git in the search field
  4. Click on the Add Package button
  5. Open UITests target => Build Phases tab
  6. Click on the plus button under the Link Binary With Libraries section
  7. Find the Swifter from the list under the Swift Package section
  8. Click on the Add button
  9. Open a <package name>UITests file
  10. Import Swifter
import Swifter

11. Create this variable in the test class

private var server: HttpServer? = HttpServer()

12. Create this method in the test class

private func startServer() {
do {
try server?.start(8888, forceIPv4: true)
print("Server status: \(server.state)")
} catch {
print("Server start error: \(error)")
}
}

13. Run this test to make sure everything goes fine (hopefully, you’ll see a green light ✅)

func testExample() throws {
startServer()
let app = XCUIApplication()
app.launchArguments = ["-mockServer"]
app.launch()
app.buttons["ferry"].tap()
}

Mock production backend

  1. Open the ContentView.swift file
  2. Replace an existed variable url with:
var urlString = "https://jsonplaceholder.typicode.com/todos/1"
#if DEBUG
if ProcessInfo.processInfo.arguments.contains("-mockServer") {
urlString = "http://localhost:8888/todos/1"
}
#endif
let url = URL(string: urlString)!

3. Run an app on iOS Simulator and tap on the ferry

The response should remain unchanged

4. Open a <package name>UITests file

5. Create a new method to mock the required endpoint

func configureServer() {
server?["/todos/1"] = { _ in
.ok(.text("""
{ "hello": "world" }
""")
)
}
}

6. Run the test (you need a sniffer to see the magic, I’d recommend mitmproxy)

func testExample() throws {
configureServer()
startServer()
let app = XCUIApplication()
app.launchArguments = ["-mockServer"]
app.launch()
app.buttons["ferry"].tap()
}

Explore the mock server

Get request details

server?["/todos/1"] = { request in
let headers = request.headers
let queryParams = request.queryParams
let address = request.address
let body = request.body
let params = request.params
let path = request.path
let method = request.method
return HttpResponse.ok(.text("""
{ "hello": "world" }
""")
)
}

Mock dynamic endpoint

server?["/:route/:number"] = { request in
let route = request.params[":route"]
let number = request.params[":number"]
return HttpResponse.ok(.text("""
{ "hello": "world" }
""")
)
}

Return internal server error

server?["/:route/:number"] = { _ in
.internalServerError
}

Redirect

server?["/:route/:number"] = { _ in
.movedPermanently("http://www.google.com")
}

Customize response details

server?["/:route/:number"] = { _ in
let body = """
{ "hello": "world" }
""".data(using: .utf8)
let statusCode = 33
let message = "Test message"
let headers = ["User-Agent": "fake", "Test-Header": "test"]
return HttpResponse.raw(statusCode, message, headers, { (writer) in
try writer.write(body!)
})
}

WebSockets

server["/websocket-echo"] = websocket(text: { session, text in
session.writeText(text)
}, connected: { [weak self] _ in
print("WS connected")
}, disconnected: { [weak self] _ in
print("WS disconnected")
})

Set up a random port for parallel tests

private func startServer(
port: in_port_t = in_port_t.random(in: 8081..<10000),
maximumOfAttempts: Int = 10
) {
guard maximumOfAttempts > 0 else { return }
do {
try server?.start(port, forceIPv4: true)
} catch SocketError.bindFailed(let message) where message == "Address already in use" {
startServer(maximumOfAttempts: maximumOfAttempts - 1)
} catch {
print("Server start error: \(error)")
}
}

--

--