Hi! I work with Eduard and I've been trying out da...
# apollo-kotlin
d
Hi! I work with Eduard and I've been trying out data builders for our integration tests. They are a huge improvement over our previous manual mock response construction so thanks! I'm now trying to figure out a robust way to share mock data between multiple requests. A lot of our requests query different things about the currently logged in user. So ideally we would be able to do something like this:
Copy code
val user = Builder(__CustomScalarAdapters).run {
    buildUser {
        contactMobile = "+2217730634835"
        // etc, lots of setup 
    }
}

addMockResponse(LoginMutation, LoginMutation.Data(fakeResolver) { user = user })
addMockResponse(PermissionsQuery, PermissionsQuery.Data(fakeResolver) { user = user })
addMockResponse(AnnouncementsQuery, AnnouncementsQuery.Data(fakeResolver) { user = user })
// ^ not actually this simple, but this gives the idea
This almost works but there are a few issues: • If we use any fake resolvers, they are run separately for each response. So different responses will actually return different data even though it's supposed to be the same user. • We use
__CustomScalarAdapters
which is presumably supposed to be private since it has the
__
user
is a weakly typed map object which isn't a blocker but means we aren't type safe if we want to get e.g. the user's id for use in another builder.
m
Can you ellaborate a bit more? Are you looking for a way to get a Json instead of an instance of
Data
?
Ah, thanks for the details 🙂
d
Sorry, I hit enter instead of shift-enter before finishing 🤦
m
Ahah no problem 🙂
I see... Well, that's not easy
One of the things that make it hard is aliases. If you have a (not very realistic but possible) query like this:
Copy code
{
  user: {
    id: firstName
  }
}
user.id
is not what you think it is
So this the primary reason why we can't be typesafe there.
Now for the
fakeResolver
returning different values, I can see the pain.
Is the use case that you don't want to specify all the fields manually?
d
Yeah, we have a lot of data in our queries so we only want to specify the relevant things in each test. That's one of the things which is so nice about databuilders.
m
First solution that comes to mind would be to register a "stable"
FakeResolver
, i.e. something that always returns the same value for the same object
This way you can run it multiple times and you're guaranteed the results
The hard part there is identifying "the same object", I don't think you can do this from
FakeResolverContext
just yet
d
Yeah that's what I was just trying to figure out, I can't see a way of doing that either right now.
Oh, maybe this: the fake resolver caches everything using the stringified path as the key (which we can get from the context), and we create a new fake resolver instances for each user.
m
The thing is two identical objects might be at different paths, that could be an issue
Copy code
{
  viewer {
    name
  }
  repository {
    owner {
      # Same user, different path
      name
    }
  }
}
d
Oh yeah, this seems hard...
m
I don't know, maybe it's ok for you. If that's the case, then you can "just" keep a huge
Map<String, Any>
for each path in your graph and reuse
But in the general case that doesn't work
d
No, I think we do have things like your example in our codebase
m
Then I'm thinking something like:
Copy code
val user = Builder(__CustomScalarAdapters).run {
    buildUser(seed = "42") {
        contactMobile = "+2217730634835"
        // etc, lots of setup 
    }
}
Each composite type would have an (optional)
seed
value that resets the fake resolver state
If no
seed
is specified, it uses the stringified path
d
Oh yeah, that might work 🤔 Another idea (if you were interested in a design very different from the current one): do the equivalent of the
fakeResolver
in the
buildUser
function after
builder.block
? That would mean that the auto-generated defaults are stored into the UserMap. I think that would be easier to understand as a user of this, but it would mean that you can't have e.g. the path (and maybe other things) in the fakeResolver equivalent.
m
Indeed, that's the other option, iterate all the fields and put a default value if there's none
Aliases make this awkward again because it means you need the FakeResolver in both places
Copy code
val user = Builder(__CustomScalarAdapters, fakeResolver).run {
    buildUser {
        contactMobile = "+2217730634835"
        // etc, lots of setup 
    }
}
Copy code
{
  viewer {
    personalPhone: phone(PERSONAL)
    professionalPhone: phone(PROFESSIONAL)
  }
}
You also need to fake for each query
d
I think fakeResolver's interface would need to change for that to work though, no? In particular we wouldn't have access to the path from inside the builder (just the field name).
m
fakeResolver's interface would need to change for that to work though, no?
Indeed, we'd need to remove the path, alias, arguments from
FakeResolverContext
mainly
You also need to fake for each query
That would take us back to step 1 anytime aliases are involved because there's no way "in advance" (without knowledge of the query) to compute those values
d
I think I still don't fully understand aliases (I hadn't heard of them before today!) but if we have e.g.
Copy code
{
    user {
        id: firstName
    }
}
wouldn't we set
id
in the query data to be equal to
firstName
from the builder-generated map?
m
That could be a way to do it but what if you want different arguments like
Copy code
{
  viewer {
    personalPhone: phone(PERSONAL)
    professionalPhone: phone(PROFESSIONAL)
  }
}
It's the same
phone
field name but with different arguments so most of the times you'll want different values
Back to the seed idea, I was looking at the faker JS lib, and they have a
.seed(seed)
function.
Do you mind opening a Github issue for this?
d
It's the same phone field name but with different arguments so most of the times you'll want different values
Ah yeah. For fields with arguments could we store a function in the map instead of a value? I guess that's quite a big change from the current setup where it's only primitives and maps of primitives...
Yeah sure 🙂
m
store a function in the map instead of a value
That's a neat idea. I'll play with this a bit.
d
I made a github issue, I hopefully captured everything! https://github.com/apollographql/apollo-kotlin/issues/4453
m
Thank you!
That's perfect 👌
@David Nikel-Shepherd sorry for the delay on this. https://github.com/apollographql/apollo-kotlin/pull/4468 should now be ready for review. Let me know what you think!
d
Sorry for my delay replying as well 😛, I had a crazy week. I like it! I left a minor comment on your PR. Looking forward to giving it a try when I have a chance.