14

Is this even possible?

Hello friends. I'm in the process of building an application using AWS AppSync + DynamoDB and I'm starting to have quite a large pile of resolver mapping templates, all which are written using the Apache Velocity Template Language (VTL).

The concern I'm starting to have is that these vtl files are quite critical to the application (since they define how data is retrieved) and a bug in one of the could wreak havoc. So like any critical part of a system... I would like to write some automated unit tests for them. But I haven't found much about others doing this.

  1. If you're using VTL (with AppSync or API Gateway), how do you test them?
  2. Is it even possible to write automated tests to velocity templates?
  3. Or am I going down the total wrong path and I should just be using Lambdas as my resolvers?

Thanks in advance!

Maurice
  • 1,223
  • 14
  • 21
  • I don't really understand your problem, you have velocity templates ok but what do you want to test ? – Arnaud Claudel Jul 19 '19 at 16:13
  • velocity templates in the context of AWS AppSync can contain logic on how to interact with DynamoDB - what to read, what filter values to apply, how to insert data, etc - it's basically building a query. – Maurice Jul 19 '19 at 16:16
  • here is an example (tho maybe rather extreme) of how complex it could get: https://docs.aws.amazon.com/appsync/latest/devguide/resolver-mapping-template-reference-dynamodb.html#id3 – Maurice Jul 19 '19 at 16:17
  • Oh you have logic in it ok. I don't know if it's usual in AppSync context, but I try to avoid this when I use templates. What I do is simply put conditional flags on my template and I handle the logic outside (in java for example), but maybe it's the way to go in AppSync. – Arnaud Claudel Jul 19 '19 at 16:21
  • 1
    Is the output still a simple file ? If so, you can just write your test, with an input and an expected output file. You process your templates with the input and compare the generated file with expected one – Arnaud Claudel Jul 19 '19 at 16:22
  • There is no explicit output. the .vtl files are uploaded directly to AWS. but you did give me an idea.... I could run the templates through a vtl processor as part of the testing process and just assert the output. – Maurice Jul 19 '19 at 16:31
  • 1
    As a unit test yes, this what I do for mail templating for example – Arnaud Claudel Jul 19 '19 at 16:33
  • See https://cwiki.apache.org/confluence/display/velocity/TestingVelocity – Ori Marko Jul 21 '19 at 08:27
  • Even if it's more expensive and slower on cold starts I decided to use Lambdas for everything. It's a legitimate fear that it can break, (And I've messed up continuously in development). and having to spin up a server just to test the validity of the files it's a little bit too much. I think VTL was used by AWS so Amplify could generate the models (handwriting it is a pain, I've been there) – Jose A Apr 19 '20 at 21:17

3 Answers3

7

It took me a while to figure this out myself, but I found a good way to write unit tests for my VTL request and response templates. I used the amplify-appsync-simulator npm package's VelocityTemplate class. The only caveat I have seen so far is that you need to use $context in your VTL, the abbreviated $ctx is not recognized by the simulators VTL renderer. Check it out:

My VTL:

#set( $timeNow = $util.time.nowEpochMilliSeconds() )
{
    "version" : "2018-05-29",
    "operation" : "PutItem",
    "key": {
        "pk" : { "S" : "game#${util.autoId()}" },
        "sk" : { "S" : "meta#${timeNow}" }
    },
    "attributeValues" : {
        "players": { "L" : [
            { "M" : {   
                ## num and color added at start game             
                "player": $util.dynamodb.toDynamoDBJson($context.args.input.player)    
            }}                        
        ]},
        "createdBy": { "S": "${context.identity.sub}"},
        "gsipk": { "S": "${context.args.input.status}"},
        "gsisk": { "S": "${context.args.input.level}#${context.args.input.maxPlayers}"},
        "gsipk2": {"S": "game"},
        "turns": { "L": [] },
        "nextPlayerNum": { "N": 1 },
        "createdAt": { "N": ${timeNow} },
        "updatedAt": { "N": ${timeNow} }
    }
}

My test:

import { AmplifyAppSyncSimulator } from 'amplify-appsync-simulator'
import { VelocityTemplate } from "amplify-appsync-simulator/lib/velocity"
import { readFileSync } from 'fs'
import path from 'path';

const vtl = readFileSync(path.join(__dirname, '..', 'addGame-request-mapping-template.vtl'))
const template = {
  content: vtl
}
const velocity = new VelocityTemplate(template, new AmplifyAppSyncSimulator)

describe('valid user and request', () => {

  // This is the graphql input
  const validContext = {
    arguments: {
      input: {
        player: 'player#1234',
        maxPlayers: 4,
        status: 'new',
        level: 7
      }
    },
    source: {}
  }

  // This is a logged in user with a JWT
  const requestContext = {
    requestAuthorizationMode: 'OPENID_CONNECT',
    jwt: {
      sub: 'abcd1234'
    }
  }

  const info = {
    fieldNodes: []
  }

  it('works', () => {
    const result = velocity.render(validContext, requestContext, info)
    expect(result).toEqual({
      result: {
        version: "2018-05-29",
        operation: "PutItem",
        key: {
          pk: { S: expect.stringMatching(/^game#[a-f0-9-]*$/) },
          sk: { S: expect.stringMatching(/^meta#[0-9]*$/)}
        },
        attributeValues: {
          players: {
            L: [
              { M: { player: { S: validContext.arguments.input.player }}}
            ]
          },
          createdBy: { S: requestContext.jwt.sub },
          gsipk: { S: validContext.arguments.input.status },
          gsisk: { S: `${validContext.arguments.input.level}#${validContext.arguments.input.maxPlayers}`},
          gsipk2: { S: 'game' },
          turns: { L: [] },
          nextPlayerNum: { N: 1 },
          createdAt: { N: expect.any(Number) },
          updatedAt: { N: expect.any(Number) }
        }
      },
      stash: {},
      errors: [],
      isReturn: false
    })
  })
})
DaKaZ
  • 925
  • 1
  • 7
  • 18
3

Found this project https://github.com/ToQoz/api-gateway-mapping-template which is a bit old but still works.

It is designed to test API Gateway mapping templates so it is missing all the special $util functions you get with AppSync resolvers, but I think one can incrementally add missing utils.

Erez
  • 1,690
  • 8
  • 9
1

Amplify just released the ability to locally test your AppSync apis, including VTL resolvers. You can check out their blog post https://aws.amazon.com/blogs/aws/new-local-mocking-and-testing-with-the-amplify-cli/ which contains a how-to for the local API Mocking functionality; Look for where it says "When I edit a VTL template, the Amplify CLI recognizes that immediately, and generates the updated code for the resolver." You could then build this into a CI or other testing pipeline of your choosing.

Aaron_H
  • 1,623
  • 1
  • 12
  • 26
  • I have gone through the blog post and it seems like this is specific to AWS AppSync VTL templates only. I am using VTL templates for AWS API Gateway's integration with DynamoDB and Step Functions. I tried using the `aws appsync evaluate-mapping-template` with those but it doesn't read my mocked input other than in the GraphQL format (with `arguments` inside). Any clue how to test the API Gateway VTL templates? – Abubakar Mehmood Mar 05 '23 at 09:45