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

Support nested paths in GraphQlTester #457

Closed
koenpunt opened this issue Aug 2, 2022 · 10 comments
Closed

Support nested paths in GraphQlTester #457

koenpunt opened this issue Aug 2, 2022 · 10 comments
Labels
type: enhancement A general enhancement
Milestone

Comments

@koenpunt
Copy link
Contributor

koenpunt commented Aug 2, 2022

When testing the response of a nested query it would be practical to perform assertions on a subpath.

To give an example;

graphqlClient.document(query).execute()
  .path("addCartItemOptions.cart.id").isEqualTo(cartGlobalId)
  .path("addCartItemOptions.cartItem.id").isEqualTo(cartItemGlobalId)
  .path("addCartItemOptions.cartItem.options[0].productOption.name").isEqualTo(optionProduct1.name)
  .path("addCartItemOptions.cartItem.options[0].quantity").isEqualTo(1)
  .path("addCartItemOptions.cartItem.options[1].productOption.name").isEqualTo(optionProduct2.name)
  .path("addCartItemOptions.cartItem.options[1].quantity").isEqualTo(2)

This is obviously very verbose, and thus it would like to propose an api where you traverse paths and can perform assertions on a "subpath";

graphqlClient.document(query).execute()
  .path("addCartItemOptions.cart.id").isEqualTo(cartGlobalId)
  .path("addCartItemOptions.cartItem") { sub ->
    sub
      .path("id").isEqualTo(cartItemGlobalId)
      .path("options[0]") {
        it
          .path("productOption.name").isEqualTo(optionProduct1.name)
          .path("quantity").isEqualTo(1)
      }
      .path("options[1].productOption.name").isEqualTo(optionProduct2.name)
      .path("options[1].quantity").isEqualTo(2)
  }
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Aug 2, 2022
@rstoyanchev
Copy link
Contributor

Do you not have an entity object for the data, such as CartItem to decode to with .path("..").entity(..) and then use satisfies(cartItem -> ...) to assert the individual properties.

@rstoyanchev rstoyanchev added the status: waiting-for-feedback We need additional information before we can continue label Aug 3, 2022
@koenpunt
Copy link
Contributor Author

koenpunt commented Aug 3, 2022

Maybe sometimes, but otherwise we have to create entities just for matching a response, which is not something we want. The methods in the controller do data conversion quite often, so there isn't an entity that has that type defined, e.g. @SchemaMapping fun id(dto: OrderDto) = dto.id.encodeGlobalId<OrderType>(), where OrderDto.id is a UUID, but the encoded id is a string.

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Aug 3, 2022
@rstoyanchev
Copy link
Contributor

rstoyanchev commented Aug 3, 2022

Okay I see.

One other option comes to mind. The tester supports the full JSONPath syntax, so something like this should work:

graphqlClient.document(query).execute()
  .path("addCartItemOptions.cart.id").isEqualTo(cartGlobalId)
  .path("addCartItemOptions.cartItem.id").isEqualTo(cartItemGlobalId)
  .path("addCartItemOptions.cartItem.options[*].productOption.name").entityList(String.class).containsExactly(optionProduct1.name, optionProduct2.name)
  .path("addCartItemOptions.cartItem.options[*].quantity").entityList(Integer.class).isEqualTo(1, 2)

We could consider support for setting a path prefix that applies to subsequent paths. For example:

graphqlClient.document(query).execute()
  .pathPrefix("addCartItemOptions")
  .path("cart.id").isEqualTo(cartGlobalId)
  .path("cartItem.id").isEqualTo(cartItemGlobalId)
  .pathPrefix("addCartItemOptions.cartItem")
  .path("options[0].productOption.name").isEqualTo(optionProduct1.name)
  .path("options[0].quantity").isEqualTo(1)
  .path("options[1].productOption.name").isEqualTo(optionProduct2.name)
  .path("options[1].quantity").isEqualTo(2)

@koenpunt
Copy link
Contributor Author

koenpunt commented Aug 3, 2022

The tester supports the full JSONPath syntax

We know, and it does come in handy in a lot of cases.

We could consider support for setting a path prefix that applies to subsequent paths.

A pathPrefix could work, and should probably allow us to write an extension method that allows us to use the before mentioned API;

internal inline fun <reified T : Any> GraphQlTester.Path.prefix(
  path: String, 
  noinline consumer: (GraphQlTester.Path) -> GraphQlTester.Path
) = consumer(this.pathPrefix(path)).pathPrefix(null)

@koenpunt
Copy link
Contributor Author

koenpunt commented Oct 21, 2022

@rstoyanchev any more thoughts on this? Have been writing stuff like this again today:

.path("merchant.contentClassification.id").isEqualTo(
    contentClassification.id!!.encodeGlobalId<ContentClassification>()
)
.path("merchant.contentClassification.name").isEqualTo(
    contentClassification.name
)
.path("merchant.contentClassification.description").isEqualTo(
    contentClassification.description
)
.path("merchant.contentClassification.excludeFromSitemap").isEqualTo(
    contentClassification.excludeFromSitemap
)

And would still love to use it more like this:

.prefix("merchant.contentClassification") {
    it
      .path("id").isEqualTo(contentClassification.id!!.encodeGlobalId<ContentClassification>())
      .path("name").isEqualTo(contentClassification.name)
      .path("description").isEqualTo(contentClassification.description)
      .path("excludeFromSitemap").isEqualTo(contentClassification.excludeFromSitemap)
}

@koenpunt
Copy link
Contributor Author

koenpunt commented Oct 21, 2022

I do now realize that I could use something like this:

val mapTypeRef = object : ParameterizedTypeReference<Map<String, Any>>() {}

// ...

.path("merchant.contentClassification").entity(mapTypeRef).isEqualTo(
    mapOf(
        "id" to contentClassification.id!!.encodeGlobalId<ContentClassification>(),
        "name" to contentClassification.name,
        "description" to contentClassification.description,
        "excludeFromSitemap" to contentClassification.excludeFromSitemap,
        "metaTags" to listOf(
            mapOf("name" to "robots", "content" to "noindex")
        )
    )
)

Which I can make less verbose by adding an extension like this;

private val mapTypeRef = object : ParameterizedTypeReference<Map<String, Any>>() {}

internal fun GraphQlTester.Path.isEqualTo(value: Map<String, Any>) =
    this.also { entity(mapTypeRef).isEqualTo(value) }

And then use like;

.path("merchant.contentClassification").isEqualTo(mapOf(...))

But this makes me think that this maybe should be a builtin matcher?

Edit: this wouldn't be an exact replacement for the prefix functionality, because this requires all fields to be present in the map.

@rstoyanchev
Copy link
Contributor

We could add support for entityMap similar to entityList on GraphQlTester.Path and take some inspiration from AssertJ, e.g. with methods like containsExactly(Map<..>) vs containsAllEntriesOf(Map<..>) so you could check for a subset of entries. Maybe this is a good alternative to the prefix path idea above, for testing raw data when you don't have higher level Objects.

@koenpunt
Copy link
Contributor Author

with methods like containsExactly(Map<..>) vs containsAllEntriesOf(Map<..>)

I like this suggestion, because with the extension I proposed above it always has to be an exact match, which is not always desired/useful I realized.

@rstoyanchev rstoyanchev changed the title Testing: perform assertions on nested path Add Path#entityMap to GraphQlTester to assert data at given path as a Map Oct 31, 2022
@rstoyanchev rstoyanchev added type: enhancement A general enhancement and removed status: feedback-provided Feedback has been provided status: waiting-for-triage An issue we've not yet triaged labels Oct 31, 2022
@rstoyanchev rstoyanchev added this to the 1.1 Backlog milestone Oct 31, 2022
@rstoyanchev rstoyanchev modified the milestones: 1.2 Backlog, 1.2.0 Apr 27, 2023
@rstoyanchev rstoyanchev changed the title Add Path#entityMap to GraphQlTester to assert data at given path as a Map Support nested paths in GraphQlTester May 9, 2023
@rstoyanchev
Copy link
Contributor

After experimenting, I was able to add support for nested paths. It looks like this in the API, and you can see a test here. Please, give it a try and let me know if works for you.

@koenpunt
Copy link
Contributor Author

Haven't had the chance to try this, but it looks like it works as I proposed in the issue description above, so that's great!

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

No branches or pull requests

3 participants