Testing scenarios with relations

I have the following schema

model Tenant {                                                                                                           
   id        Int                @id @default(autoincrement())                                                             
   name      String                                                                                                                                                                              
   users     User[]                                                                                                       
   createdAt DateTime           @default(now())                                                                           
   updatedAt DateTime           @updatedAt                                                                                
 }                                                                                                                        
                                                                                                                          
 model User {                                                                                                             
   id                  Int       @id @default(autoincrement())                                                            
   tenants             Tenant[]                                                                                                
   email               String    @unique                                                                                                                                                         
   hashedPassword      String                                                                                             
   salt                String                                                                                                                                                                                
   createdAt           DateTime  @default(now())                                                                          
   updatedAt           DateTime  @updatedAt                                                                               
}                                       

Where a user can have 1 or more tenants and a tenant can have 1 or more users. This is working fine so far, up until my test started failing. I updated my user and tenant querys to start returning the relations like so:

export const user = async ({ id }) => {                                                                                
    const oneUser = await db.user.findUnique({                                                                           
      where: { id },                                                                                                     
    });                                                                                                                  
    const tenants = await db.user                                                                                        
      .findUnique({                                                                                                      
        where: { id },                                                                                                   
      })                                                                                                                 
      .tenants();                                                                                                        
                                                                                                                        
    return {                                                                                                             
      ...oneUser,                                                                                                        
      tenants,                                                                                                           
    };                                                                                                                   
 };

I have a scenerio setup like this

user: {                                                                                                                      
    one: {                                                                                                                     
      data: {                                                                                                                  
        email: 'test-user-1@example.com',                                                                                      
        hashedPassword: 'my-super-top-secret-passw0rd',                                                                        
        salt: '02EDGWDWOKJH842NZZ',                                                                                            
        updatedAt: new Date('2022-06-18T19:08:06Z'),                                                                           
        tenants: {                                                                                                             
          create: [                                                                                                            
            {                                                                                                                  
              name: 'test-tenant-1',                                                                                                                                                                               
              updatedAt: new Date('2022-06-18T19:08:06Z'),                                                                     
            },                                                                                                                 
          ],                                                                                                    
        },                                                                                                                     
      },                                                                                                                       
    },                                                                                                                         
  },

Here is my test

 scenario('returns a single user', async (scenario) => {                                                                      
    const result = await user({ id: scenario.user.one.id });                                                                   
                                                                                                                               
    expect(result).toEqual(scenario.user.one);                                                                                 
  });                                                                                                                          

result contains a tenant but scenario does not. Any idea why or how to work-around this? I get this error

 ● users › returns a single user

    expect(received).toEqual(expected) // deep equality

    - Expected  - 0
    + Received  + 1

    @@ -5,8 +5,9 @@
        "id": 14,
        "salt": "02EDGWDWOKJH842NZZ",
    +   "tenants": Array [],
        "updatedAt": 2022-06-18T19:08:06.000Z,
        "verification": "test-verification-c0de",
      }

      25 |     const result = await user({ id: scenario.user.one.id });
      26 |
    > 27 |     expect(result).toEqual(scenario.user.one);
         |                    ^
      28 |   });
      29 |
      30 |   scenario('creates a user', async () => {

      at api/src/services/users/users.test.js:27:20
      at Object.<anonymous> (node_modules/@redwoodjs/testing/config/jest/api/jest.setup.js:163:16)

You need to include the tenant relationship in your scenario. When you include it will be available in the scenario. Testing | RedwoodJS Docs

From the docs.

You can also include the post object (or select specific fields from it):

export const standard = defineScenario({
  comment: {
    first: {
      data: {
        name: 'Rob',
        body: 'Something really interesting'
        post: {
          create: {
            title: 'Brand new post',
            body: 'Introducing dbAuth'
          }
        }
      },
      include: {
        post: true
      }
    }
  }
})

Ah, I saw that include and didn’t understand it. That solved my problem, thank you very much. Here is the fix.

export const standard = defineScenario({                                                                                       
  user: {                                                                                                                      
    one: {                                                                                                                     
      data: {                                                                                                                  
        email: 'test-user-1@example.com',                                                                                      
        hashedPassword: 'my-super-top-secret-passw0rd',                                                                        
        salt: '02EDGWDWOKJH842NZZ',                                                                                            
        verification: 'test-verification-c0de',                                                                                
        isVerified: false,                                                                                                     
        updatedAt: new Date('2022-06-18T19:08:06Z'),                                                                           
        tenants: {                                                                                                             
          create: [],                                                                                                          
        },                                                                                                                     
      },                                                                                                                       
      include: {                                                                                                               
        tenants: true,                                                                                                         
      },                                                                                                                       
    },                                                                                                                         
  },                                                                                                                           
});

How can we do this on the create side? The model I’m testing has relations and I want to test that they’re being created properly.

Thanks!

How can we do this on the create side?

Hey :wave:

You could run your create function, then check its output.

const output = await createThing({...})
expect(output.relatedModel.description).toBe('Im a related thing')

Hey @danny, thanks for your reply!

That doesn’t work - any related models come back as undefined. You can see here that the object returned holds the ID of the 1:1 related model, but not the model itself. Additionally, it holds no reference to the 1:many related models (which is expected).

I know on the graphql side when executing a create mutation you can specify the fields of the model that you want to be returned, which I assume is similar to how the include works in scenarios, but I can’t for the life of me figure out how to do this when testing the create function directly.

Thanks!

Ah I understand your problem now :). So a bit of background first, so we’re on the same page…

Redwood service tests (and scenario tests) typically call the resolver functions (in your case recipes). When you receive a query into graphql, if the client is requesting a relation (in this case a chef) - it will use the “relation resolvers”. These are the functions at the bottom of your service file - they probably look something like this:

export const Recipe: RecipeRelationResolvers = {
 chef: () => {...}
}

This Recipe.chef function is called only when your query has chef in it, otherwise it isn’t.

So one way to test this would be the following:

const recipes = await recipes()

const relatedChefs = await Promise.all(recipes.map((recipe) => {
// the first parameter is "arguments" from the query, the second is the "root" object i.e. the recipe for which to lookup the chef for
 return Recipe.chef(null, recipe)
}))

expect(relatedChefs).toEqual(...)

You’re right if you’re thinking that its not super intuitive, and I will bring this up for discussion with the rest of the team see if we can come up with easier syntax!

Ahhhh this makes sense - the issue is that my relations go several steps deep, ie a recipe has steps which has ingredients which has units, etc. Therefore, I can see the included testing of the relation resolvers become pretty verbose.

Because I also need all those relations for testing the scenarios, I’ve taken the include clause from that and exported it, and am then doing this:

const result = await createRecipe(mutationInput)
const resultWithRelations = await db.recipe.findUnique({
  where: { id: result.id },
  include: recipeIncludeAllRelations,
})

I figure that because this test is testing the ability for the create service, it’s idiomatic to not expect the create function to return the full model, and to need to do a separate db query to get that. And it works perfectly!

I’m giving this a try in another area so that I can fully test the resolvers - what do you recommend about all these type issues? Thanks!


(i also want to note that this code does work perfectly, it’s just an issue related to the type system)