XCUI Swift Mocking GraphQL RESTful Way

Tadas Stankevicius
The Startup
Published in
8 min readMay 31, 2020

Mocking REST requests is easy when creating automation scripts with XCUI Swift but when your app migrates to GraphQL then it becomes a little more difficult. With REST we are used to having each response coming from a different endpoint which made it simple to intercept and respond but with GraphQL we only have one that we can point to and that’s “/graphql”.

So how do we solve this problem?

Well there are several solutions. We can create our own Apollo Client Protocol class and set the cache to pre-load our expected data, we could try and mock the class dependencies unit style or we could create our own independent mock server which automatically handles the queries for us, and so on. However we want it to be black box yet easy enough to maintain. So instead we can go with the simplest option and use the request headers. Each GraphQL request must contain a parameter in the header which distinguishes each operation, this will allow us to capture the header value in our automation code.

For this example I have chosen to use Swifter as my REST mocking library. It’s easy to setup, works perfectly with REST requests and its available for both CocoaPods and Carthage. The principle I am showing here works with any mocking library as the only thing you are doing is intercepting each request and responding to the header value.

Lets begin

Before we proceed here is the sample project which has everything already setup.

Lets start by launching the app and using Charles Proxy to inspect what network requests are being made.

We can immediately see that there are 2 /graphql requests and 1 REST call. So now we can save these responses and add them to our test directory.

I Chose to create separate folders for both just to make it easier to maintain and also in case of a migration it will be easier to track. Its important to note that I have named the .json response files by their operation names, this is something we will need later on.

Setting up a mock server

First we create a BaseTest class which will do our setup and tear down for each test, you must also make sure to import “Swifter”.

let server = HttpServer()var requests = [String: [String]]()lazy var app: XCUIApplication = {
return XCUIApplication()
}()

We initialize our HTTP server which in this case is Swifter, then we create a dictionary to store our requests. You can of course handle that however you like but in this case I chose to do it this way. Finally initialize the XCUI app.

override func setUp() {
super.setUp()
configureMockServer()
startMockServer(port: 8080)
continueAfterFailure = false
app.launchArguments = ["UITEST"]
app.launch()
}
override func tearDown() {
super.tearDown()
app.terminate()
}

Now we need to override the setup and teardown methods. In the setup add 2 methods “configureMockServer()” and “startMockServer(port:8080)”, we will create those a bit later. Another important part is to add a launch argument, we will need this to make sure that the app is pointing to the localhost where we can control the responses. For teardown we just want to terminate the app after each test.

Mocking

Bellow the class create an extension called BaseTest.

extension BaseTest {}

First we need to be able to load our mocked json files, so create a method inside the extension and call it whatever you like, here I called it getParcedJsonFromFile.

func getParsedJsonFromFile(fileName: String) -> Any {
var json: Any?
let testBundle = Bundle(for: type(of: self))
let url = testBundle.path(forResource: fileName, ofType: "json")
do {
let jsonData = try Data(contentsOf:URL(fileURLWithPath:url!))
json = try JSONSerialization.jsonObject(with: jsonData)
} catch {
print(error)
}
return json!
}

When you do add new json files, just make sure that you add them to the correct test target. Here we simply look at resources for .json files and return it.

Now let’s create our missing configureMockServer function. Before we get into writing the logic, lets understand how its going to work. Every iOS Apollo request will have a header called “x-apollo-operation-name” and so we know that using the conventional way of reading the endpoint url is not going to work with graphql as all urls are the same, however we can use this header that each request contains to substitute it as the url path. So rather than allowing the mock library to compare the endpoints we will instead compare the headers.

We can see in Charles Proxy that the operation name for the first graphql query is “findAllUsers”, so in this case this means that your .json file must also be named “findAllUsers.json”.
So if you go to your test class or create one if you don’t have it yet, we can add this request to our setup.

class UITestSample: BaseTest {
let GETQuestionURL = "/questions?site=stackoverflow&order=desc&sort=votes&tagged=ios"
let GQLFindAllUsersRequest = "graphql_FindAllUsers"
let GQLGetEmailRequest = "graphql_GetEmail"
override func setUp() {
requests[GETQuestionURL] = ["RestQuestionResponse"]
requests[GQLGetEmailRequest] = ["GetEmail"]
requests[GQLFindAllUsersRequest] = ["findAllUsers"]
super.setUp()
}

In this example I have added all graphql requests and a REST request. Whats important here is the naming convention. When you create a new GraphQL request you must name it “graphql_OperationName” (Ill leave this to you to improve). Next in the setup we need to set the request response and as I mentioned earlier, the response file name must be the same as the operation header value, in this case “findAllUsers” & “GetEmail”. Now we can configure the mock server.

Go back to the BaseTest extension inside the configureMockServer function and create a new String array. I called it savedOperations. We will store all operation names that belong to the graphql path.

var savedOperations = [String]()

Next we need to run through the requests dictionary and grab path and response data.

requests.forEach { pairInfo in
let path = pairInfo.key
let response = pairInfo.value[0]
let json = getParsedJsonFromFile(fileName: response)

The path we get from the key is where you set your “requests[/graphql]” and the response is the value “= [findAllUsers]”. We also set the relevant json data which we know the location of because of the response name we pass.

Next we need to check if the path is a /graphql path

if path.contains("graphql") {
savedOperations.append(response)
server["graphql"] = { request_ in
let requestOperation = request_.headers["x-apollo-operation-name"]!
var gqlJson = self.getParsedJsonFromFile(fileName: response)

If the path is “graphql” then we need to add the response name to the savedOperations array which will also be the same as the operation name. After that we do the standard request with the endpoint set to “graphql” and inside we initialize the requestOperation which we get from the header “x-apollo-operation-name”.

savedOperations.forEach { operation in
if operation == requestOperation {
gqlJson = self.getParsedJsonFromFile(fileName: operation)
}
}
return HttpResponse.ok(.json(gqlJson))

Then all we need to do is run through all of the saved operations in the array and check if the operation value is the same as the current request header and if so then the json is now set to the matching operation. The last part is just sending a HTTP response with the json data.

else {
server[path] = { request_ in HttpResponse.ok(.json(json))} }

Otherwise if its a REST request then we just use the path variable and send matching response json. That is all for configuring the mock server logic.

This is how it looks complete

func configureMockServer() {
var savedOperations = [String]()

requests.forEach { pairInfo in
let path = pairInfo.key
let response = pairInfo.value[0]
let json = getParsedJsonFromFile(fileName: response)

if path.contains("graphql") {
savedOperations.append(response)
server["graphql"] = { request_ in
let requestOperation = request_.headers["x-apollo-operation-name"]!
var gqlJson = self.getParsedJsonFromFile(fileName: response)
savedOperations.forEach { operation in
if operation == requestOperation {
gqlJson = self.getParsedJsonFromFile(fileName: operation)
}
}
return HttpResponse.ok(.json(gqlJson))
}
} else {
server[path] = { request_ in HttpResponse.ok(.json(json))}
}
}
}

Starting the mock server

func startMockServer(port: UInt16) {
do {
try server.start(port, forceIPv4: true)
print("server started")
} catch {
print("error starting server")
}
}

Create a new function in the extension and add the code to start the mock server.

Everything is now complete on the test end, the last part is to make the GraphQL requests to go through the localhost. There are several ways to do this depending on how your app configuration is setup but for now we will use the simplest method. Find the class in the main project where the GraphQL url is set. In this sample project its in the “GraphQLNetwork.swift” class.

var url = URL(string: "https://parseapi.back4app.com/graphql")!if ProcessInfo.processInfo.arguments.contains("UITEST") {
url = URL(string: "http://localhost:8080/graphql")!
}

Since we set the launch argument “UITEST” in the beginning, we can use that to check if the app is running in test mode and if so then we simply set the url to point to localhost.

Results

Mocked REST Request
Mocked /graphql findAllUsers Operation
Mocked /graphql GetEmail Operation

As you can see the REST request was successful and responded with the correct data as well as the 2 GraphQL requests, each one containing relevant .json responses.

Conclusion

So in short, the idea is to simply use the headers for GraphQL requests and endpoint urls for REST. You should be able to implement the same logic with whatever mocking library you use for your XCUI tests as all this is really doing is just matching the json to the relevant header.

Thanks for reading.

Leave a comment if you have any queries.

--

--

Tadas Stankevicius
The Startup

Senior SDET/QA focused on automation and mobile game dev. Mobile, web, and API tech. Dedicated to delivering high-quality products. https://qainterviewbook.com