3 minute read

Preview

Introduction

Automation testing can be a tricky thing, especially when you’re dealing with 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 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

    Sample app 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)
        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

    Sample app 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)")
  }
}

Updated: