Data builders
Apollo Kotlin generates models for your operations and parsers that create instances of these models from your network responses. Sometimes though, during tests or in other occasions, it is useful to instantiate models manually with known values.
Doing so is not as straightforward as it may seem, especially when fragments are used. Creating operationBased
models requires instantiating every fragment as well as choosing an appropriate __typename
for each composite type.
Data builders make this easier by providing builders that match the structure of the Json document.
Enabling data builders
To enable data builders, set the generateDataBuilders
option to true
:
1apollo {
2 service("service") {
3 // ...
4
5 // Enable data builder generation
6 generateDataBuilders.set(true)
7 }
8}
This generates a builder for each composite type in your schema as well as a helper Data {}
function for each of your operations.
Example usage
Let's say we're building a test that uses a mocked result of the following query:
1query HeroForEpisode($ep: Episode!) {
2 hero(episode: $ep) {
3 firstName
4 lastName
5 age
6
7 ship {
8 model
9 speed
10 }
11
12 friends {
13 firstName
14 lastName
15 }
16
17 ... on Droid {
18 primaryFunction
19 }
20
21 ... on Human {
22 height
23 }
24 }
25}
Here's how we can use the corresponding data builder for that mocked result:
1@Test
2fun test() {
3 val data = HeroForEpisodeQuery.Data {
4 // Set values for particular fields of the query
5 hero = buildHuman {
6 firstName = "John"
7 age = 42
8 friends = listOf(
9 buildHuman {
10 firstName = "Jane"
11 },
12 buildHuman {
13 lastName = "Doe"
14 }
15 )
16 ship = buildStarship {
17 model = "X-Wing"
18 }
19 }
20 }
21
22 assertEquals("John", data.hero.firstName)
23 assertEquals(42, data.hero.age)
24}
In this example, the hero
field is a Human
object with specified values for firstName
and age
. The values for lastName
and height
are automatically populated with mock values.
Similarly, values for the ship's speed, the 1st friend's last name and 2nd friend's first name are automatically populated.
You can replace
buildHuman
above withbuildDroid
to create aDroid
object instead.
Aliases
Because data builders are schema-based and aliases are defined in your queries, there is no way for the codegen to generate builder fields for them. Instead, you'll need to specify them explicitly.
Given a query like this:
1query GetHeroes {
2 luke: hero(id: "1002") {
3 name
4 }
5 leia: hero(id: "1003") {
6 name
7 }
8}
You can generate a fake data model like so:
1val data = GetHeroes.Data {
2 this["luke"] = buildHumanHero {
3 name = "Luke"
4 }
5 this["leia"] = buildHumanHero {
6 name = "Leia"
7 }
8}
@skip
and @include
directives
By default, the data builders match the types in your schema. If a field is non-null you will either have to provide a value or let the default resolver provide one. This is an issue for @skip
and @include
directives where a field might be absent even if it is non-nullable. To account for this case, use the same syntax as for aliases and set the value to Optional.Absent
.
1query Skip($skip: Boolean!) {
2 nonNullableInt @skip(if: $skip)
3}
You can generate a fake data model like so:
1val data = SkipQuery.Data {
2 this["nonNullableInt"] = Optional.Absent
3}
4
5assertNull(data.nonNullableInt)
Configuring default field values
To assign default values to fields, data builders use an implementation of the FakeResolver
interface. By default, they use an instance of DefaultFakeResolver
.
The DefaultFakeResolver
gives each String
field the field's name as its default value, and it increments a counter as it assigns default values for Int
fields. It defines similar default behavior for other types.
You can create your own FakeResolver
implementation (optionally delegating to DefaultFakeResolver
for a head start). You then pass this implementation as a parameter to the Data
function, like so:
1// A FakeResolver implementation that assigns -1 to all Int fields
2class MyFakeResolver : FakeResolver {
3 private val delegate = DefaultFakeResolver(__Schema.all)
4
5 override fun resolveLeaf(context: FakeResolverContext): Any {
6 return when (context.mergedField.type.leafType().name) {
7 "Int" -> -1 // Always use -1 for Int
8 else -> delegate.resolveLeaf(context)
9 }
10 }
11
12 override fun resolveListSize(context: FakeResolverContext): Int {
13 // Delegate to the default behaviour
14 return delegate.resolveListSize(context)
15 }
16
17 override fun resolveMaybeNull(context: FakeResolverContext): Boolean {
18 // Never
19 return false
20 }
21
22 override fun resolveTypename(context: FakeResolverContext): String {
23 // Delegate to the default behaviour
24 return delegate.resolveTypename(context)
25 }
26}
27
28@Test
29fun test() {
30 val data = HeroForEpisodeQuery.Data(resolver = MyFakeResolver()) {
31 hero = buildHuman {
32 firstName = "John"
33 }
34 }
35
36 // Unspecified Int field is -1
37 assertEquals(-1, data.hero.age)
38}
Using fragments
Because fragments may be defined on interfaces and unions, you need to explicit the concrete type you want to model. For an example, if you have an Animal
interface, create a Lion
fragment data using the following:
1val data = AnimalDetailsImpl.Data(Lion) {
2 // you can access roar here
3 roar = "Grrrrr"
4}
Sometimes, you may want to test new types defined in your server that are not known to the client yet. To do so, use the abstract type (Animal
here) as first parameter for the Data()
constructor function.
In those cases, you need to specify the __typename
of the returned object type explicitly:
1val data = AnimalDetailsImpl.Data(Animal) {
2 // the client doesn't know about this type yet, and you need to specify it explicitly
3 __typename = "Brontaroc"
4 species = "alien"
5}