Skip to content
This repository has been archived by the owner on Jul 27, 2020. It is now read-only.

How to test Yoga2 (with an entity layer?) #110

Open
Weakky opened this issue Mar 21, 2019 · 7 comments
Open

How to test Yoga2 (with an entity layer?) #110

Weakky opened this issue Mar 21, 2019 · 7 comments

Comments

@Weakky
Copy link
Contributor

Weakky commented Mar 21, 2019

Motivation

I see 4 ways to test a GraphQL API with Yoga:

  • Using graphql-tools (not possible without ejecting atm because we do not expose the GraphQLSchema object)

  • On each test, spawn a new custom-seeded db + spawn a graphql server + run queries against it (mostly for integration test)

  • Mock the prisma-client or any other data-access layer

  • With an entity layer?

Description

When raising the question of tests with Yoga2, the question mostly refers to:

How to test nexus and nexus-prisma? (the underpinning blocks on which Yoga is built)

When looking at the nexus documentation for testing, here's what we can read:

image

You can find some examples with nexus here.

Separating the domain logic from the actual resolvers is not something new. Facebook is using the same concept as part of their "Ent layer", which enables quite a lot of advantages:

  • Testing your GraphQL API becomes way simpler: You just need to mock your classes
  • Your GraphQL API becomes reusable in other contexts (such as tests, but anywhere else)
  • Lots of other concepts can be handled on that layer rather than on the api layer: authorization, authentication, visibility, pretty much everything

type-graphql is another great example that takes advantage of that pattern by actually generating a GraphQLSchema from these classes.

What could it look like with Yoga + Prisma (and an entity layer)

Given the following datamodel:

type User {
  id: String!
  name: String!
  lastName: String!
  age: Int!
}

The scaffold command could generate the following:

  1. A generated BaseEntity. That part should never be edited manually.
abstract class UserEntityBase {
  protected id: string
  protected name: string
  protected lastName: string
  protected age: number

  constructor(protected record: User, protected prisma: Prisma) {
    this.id = record.id
    this.name = record.name
    this.lastName = record.lastName
    this.age = record.age
  }

  static async createUser(args) {
    return prisma.createUser(args)
  }

  static async deleteUser(args) {
    return prisma.createUser(args)
  }

  static async updateUser(args) {
    return prisma.createUser(args)
  }

  static async user(args) {
    return prisma.user(args)
  }

  static async users(args) {
    return prisma.user(args)
  }
}
  1. And a userland mother class that'd extend the base for adding derived fields:
class UserEntity extends UserEntityBase {
  fullName() {
    return this.name + this.lastName
  }

  friends() {
    return UserEntity.user({ id: this.id }).friends()
  }
}

Overall, the tree structure could look like this:

.
├── package.json
├── prisma
│   ├── datamodel.prisma
│   ├── prisma.yml
│   └── seed.ts
└── src
    ├── context.ts
    ├── entities
    │   ├── Comment.ts
    │   ├── Post.ts
    │   ├── User.ts
    │   └── index.ts
    ├── graphql
    │   ├── Mutation.ts
    │   ├── Post.ts
    │   ├── Query.ts
    │   └── User.ts
    └── tests
        ├── Comment.ts
        ├── Post.ts
        └── User.ts

Scaffolding a type would result in generating:

Biggest concerns

  • By abstracting away most of the resolvers logic, we introduce another layer that might cause boilerplate overhead

  • That additional layer now needs to be synced with the GraphQL schema. Meaning that everytime we add a derived field, we also need to update the schema.

  • One way to solve that problem would be to derive the schema from the classes themselves, but then we solve the very same problem type-graphql is trying to solve, and we'd rather just provide an integration for it

Open questions

  • Packaging (i.e. will it be a standalone package or part of Yoga/Prisma?)
  • Is it a valid use case to hide fields?
  • Will we need decorators after all?
  • Where do type definitions come from?
  • Where are type annotations needed?
  • Classes vs objects

We're very curious to hear your thoughts on that perspective 🙌

@Weakky Weakky changed the title How to tests Yoga2 (with an entity layer?) How to test Yoga2 (with an entity layer?) Mar 21, 2019
@Weakky Weakky pinned this issue Mar 21, 2019
@wtrocki
Copy link
Contributor

wtrocki commented Mar 21, 2019

By abstracting away most of the resolvers logic, we introduce another layer that might cause boilerplate overhead

But all of that will be generated right? Any edits will still generate new stuff. Is there any chance that people will need to write that themselves.

@Weakky
Copy link
Contributor Author

Weakky commented Mar 21, 2019

All derived fields such as fullName or anything else will have to be "manually written".
It also adds complexity

@wtrocki
Copy link
Contributor

wtrocki commented Mar 21, 2019

Personally, I do not see too much overhead as long as the structure is stable and properly documented. Once that is done actual structure may become part of the user API - it is being given to the user and they need to accept this format to be compatible with future generated stuff.

Proposed layout is clear and must say I really like it as it will separate responsibilities and make things much cleaner with the way that applications are written. derived field is more a separate corner case for me that can be somehow improved later by docs documenting suggestion above. It can be also specified somehow on nexus level by explicitly providing a reference to implementation etc. and be incorporated into the generated class. For example, users may annotate a method as derived and implementation will be left empty (but schema will contain this field)

While the initial proposal is done to enable testing there are many other benefits from doing things this way.

@DevanB
Copy link
Contributor

DevanB commented Mar 23, 2019

I, personally, have already created a similar structure to the Nexus example (multiple "data-sources" in Context that abstract away business logic and Prisma usage from resolvers). Having said that, I think something like this makes sense.

Would the "BaseEntity" be hidden away from the end-user? Because currently I am ignorant to why I might need to touch that BaseEntity if it is making all requests to Prisma...which I can (and am doing currently) in my own data sources.

It appears, to me, that I can simply create myself a tests directory, wire up Jest, setup some mocks on my data-sources, and write tests against to run against those data-sources. I can be 100% incorrect though as I haven't tried any of this yet. 😄

Either way, Yoga2 is opinionated and I think your proposal has traction. No need to bike shed over it when an average user will simply copy and paste from the example tests in the boilerplates.

@guledali
Copy link

guledali commented Apr 8, 2019

@Weakky How do I test my resolvers for example take the deletePost from https://github.com/prisma/yoga2/blob/master/examples/with-db/src/graphql/Mutation.ts

How do I write a unit-test for it?

@guledali
Copy link

guledali commented Apr 10, 2019

@Weakky Preconfigure easygraphql-tester and jest so that it works out of the box with Yoga2, it will make it easier to test graphql on the server and point people to

https://medium.com/open-graphql/how-i-started-testing-my-queries-and-mutations-on-graphql-f578abc1b424

https://easygraphql.com/docs/getting-started/overview

@Weakky
Copy link
Contributor Author

Weakky commented Apr 26, 2019

Hey everyone 👋,

The main reason why we haven't been active during the past weeks on Yoga2 is because we've been working on the entity topic.

It's now living in its own repository at https://github.com/prisma/ent. I just wrote a quick README for you to read and maybe provide feedback.

The write part wasn't yet thought at all, only the read part. Feel free to chime-in and help us design it 🙏

If it turns out to be powerful/useful enough, that entity layer will probably become the default way to build graphql apis with Yoga2, by providing great integration/code-generation/tests for everything

Thanks 🙌

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

No branches or pull requests

4 participants