Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Comparing two GraphQLResult #160

Open
zamderax opened this issue Dec 13, 2024 · 3 comments
Open

Comparing two GraphQLResult #160

zamderax opened this issue Dec 13, 2024 · 3 comments

Comments

@zamderax
Copy link

zamderax commented Dec 13, 2024

Problem

Currently can't compare two GraphQLResult even though the data is the same.
I'm currently using swift-testing with Swift 6.0.2

let queryOrganizationBySlug: String = """
  query FindSingleOrganization($slug: String!) {
      findSingleOrganization(slug: $slug) {
        slug
        displayName
        details
        domain
      }
    }
"""
let variables: [String: Map] = ["slug": "acme-corp"]
let graphQLRequest = GraphQLRequest(query: query, variables: variables)
let encoder = GraphQLJSONEncoder()
    let buffer = ByteBuffer(data: try encoder.encode(graphQLRequest))
let response: GraphQLResult = try await client.runGraphQLQuery(
  url: "/graphql",
  buffer: buffer
)
let expected: GraphQLResult = GraphQLResult(
  data: [
    "findSingleOrganization": [
      "slug": "acme-corp",
      "displayName": "Acme Corporation",
      "details": "A leading technology company",
      "domain": "acme.com"
    ]
  ]
)
#expect(response == expected)

However I'm getting an issue on comparison even though GraphQLResult is Equatable. The error is as follows:

 Expectation failed: (response  {"data":{"findSingleOrganization":{"displayName":"Acme Corporation","details":"A leading technology company","domain":"acme.com","slug":"acme-corp"}}}) == (expected  {"data":{"findSingleOrganization":{"slug":"acme-corp","displayName":"Acme Corporation","details":"A leading technology company","domain":"acme.com"}}})

Which we can see that the data is the same, the only thing is that the data is in differing orders:

[
  {
    "data": {
      "findSingleOrganization": {
        "details": "A leading technology company",
        "domain": "acme.com",
        "slug": "acme-corp",
        "displayName": "Acme Corporation"
      }
    }
  },
  {
    "data": {
      "findSingleOrganization": {
        "slug": "acme-corp",
        "displayName": "Acme Corporation",
        "details": "A leading technology company",
        "domain": "acme.com"
      }
    }
  }
]

What's the proper way to compare the results?

In addition, I used

let graphQLRequest = GraphQLRequest(query: query, variables: variables)
let encoder = GraphQLJSONEncoder()
@NeedleInAJayStack
Copy link
Member

NeedleInAJayStack commented Dec 20, 2024

It looks like you were doing the right thing using GraphQLJSONEncoder. Do you happen to know what Decoder client.runGraphQLQuery is using under the hood to convert to a GraphQLResult?

For background, JSON doesn't care about ordering, but GraphQL does, so we've had to do quite a bit of work to get GraphQL to play nice, and may have missed a spot on decoding.

As an alternative, you could check each field (this is psudocode - you might have to fix it up):

let expectedDictionary = expected.data?.dictionary?["findSingleOrganization"].dictionary!
for expectedField, expectedValue in expectedDictionary {
    #expect(expectedValue == response.data?.dictionary?["findSingleOrganization"].dictionary?[expectedField])
}

It's not pretty, but it should do the trick.

@zamderax
Copy link
Author

Apologies for the late reply, was traveling for the holidays.

The following is the swift extension that I wrote for my Integration testing framework.

import Foundation
import GraphQL
import HTTPTypes
import Hummingbird
import HummingbirdTesting

extension TestClientProtocol {
  /// Run a GraphQL query and return the result.
  ///
  /// - Parameters:
  ///   - query: The GraphQL query to run.
  ///   - variables: The variables to pass to the query.
  ///   - client: The client to use to run the query.
  ///   - testUserId: The user ID to use for authentication.
  ///   - file: The file to use for logging.
  ///   - line: The line to use for logging.
  ///
  /// - Returns: The result of the GraphQL query.
  func runGraphQLQuery(
    _ query: String,
    variables: [String: Map] = [:],
    testUserId: String,
    file: StaticString = #filePath,
    line: UInt = #line
  ) async throws -> GraphQLResult {
    var headers: HTTPFields = HTTPFields()
    headers[HTTPField.Name("Authorization")!] = "Bearer \(testUserId)"
    headers[HTTPField.Name("Content-Type")!] = "application/json; charset=utf-8"

    let graphQLRequest = GraphQLRequest(query: query, variables: variables)
    let encoder = GraphQLJSONEncoder()
    let buffer = ByteBuffer(data: try encoder.encode(graphQLRequest))
    let response = try await self.execute(uri: "/graphql", method: .post, headers: headers, body: buffer)
    let decoder = JSONDecoder()
    let graphQLResult: GraphQLResult = try decoder.decode(GraphQLResult.self, from: response.body)
    return graphQLResult
  }
}

@NeedleInAJayStack
Copy link
Member

NeedleInAJayStack commented Dec 28, 2024

Oh okay, that makes sense.

For background, GraphQLResult.data uses the Map structure, which itself uses an OrderedDictionary of fields. This is important in order to deliver the correct ordering of results in the JSON response, which is required by the graphQL spec. However, it means that Map equality requires consistent ordering of the fields.

The issue here is that JSONDecoder internally uses an ordinary Dictionary, which does not preserve ordering, so your decoded GraphQLResult has non-deterministic ordering of its fields, which may not match your declared expected GraphQLResult

I'd suggest that you instead model the expected response as a standard decodable type and don't use GraphQLResult which will avoid any ordering considerations. For example:

struct FindSingleOrganizationResponse: Codable {
    let data: DataResponse
    
    struct DataResponse: Codable {
        let findSingleOrganization: [Organization]
        
        struct Organization: Codable {
            let slug: String
            let displayName: String
            let details: String
            let domain: String
        }
    }
}

let expected = FindSingleOrganizationResponse(
  data: .init(
    findSingleOrganization: [.init(
      slug: "acme-corp",
      displayName: "Acme Corporation",
      details: "A leading technology company",
      domain: "acme.com"
    )]
  )
)

... // Get response

let actual = try JSONDecoder().decode(FindSingleOrganizationResponse.self, from: response.body)
#expect(actual == expected)

Just be aware: I didn't test the code above, so you might have to fiddle with it a bit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants