Skip to content

Application Spy

The ApplicationSpy class provides a way to mock making method calls from within contracts. This is particularly useful when testing contracts that deploy and interact with other contracts in a type-safe manner. It can be used with all the approaches for making method calls supported by algorand-typescript.

Deploying other contracts with explicit create method

Section titled “Deploying other contracts with explicit create method”

1. itxn.applicationCall

const compiled = compile(Hello)
const helloApp = itxn
  .applicationCall({
    appArgs: [methodSelector(Hello.prototype.create), encodeArc4('hello')],
    approvalProgram: compiled.approvalProgram,
    clearStateProgram: compiled.clearStateProgram,
    globalNumBytes: 1,
  })
  .submit().createdApp

2. Strongly typed create method call

const compiled = compileArc4(Hello)
const app = compiled.call.create({
  args: ['hello'],
}).itxn.createdApp

Mock result can be set up for both snippets above as follows:

// create an application and register it with test execution context.
// pass `{ approvalProgram: ctx.any.bytes() }` parameter to distinguish
// the contract being deployed if the test involves multiple contracts
// with the same method selector for the create method.
const helloApp = ctx.any.application()

// set to return helloApp when `compile(Hello)`
ctx.setCompiledApp(Hello, helloApp.id)

// create a new spy for `Hello` contract
const spy = new ApplicationSpy(Hello)

// register a callback for the method of the contract.
// the callback is registered against the method selector.
// all callbacks for multiple methods with same method selector are invoked
// when any one of those methods is called.
// in those cases, you can check for `itxnContext.approvalProgram` or `itxnContext.appId`
// to see if the callback needs to handle a particular method call.
// e.g.
// ```
// if (itxnContext.approvalProgram === helloApp.approvalProgram) {
//   itxnContext.createdApp = helloApp
// }
// ```
// `itxnContext` is provided as a parameter to the callback method and
// it allows reading and setting of the properties of `itxn.ApplicationCallInnerTxn` interface.
// it also maps and encodes the arguments to the `appArgs` collection as bytes values,
// and provides consistent access to those arguments.
spy.on.create((itxnContext) => {
  itxnContext.createdApp = helloApp
})

Deploying other contracts without explicit create method

Section titled “Deploying other contracts without explicit create method”

1. itxn.applicationCall

const compiled = compile(Hello)
const helloApp = itxn
  .applicationCall({
    approvalProgram: compiled.approvalProgram,
    clearStateProgram: compiled.clearStateProgram,
    extraProgramPages: compiled.extraProgramPages,
    globalNumBytes: compiled.globalBytes,
  })
  .submit().createdApp

2. Strongly typed bare create method call

const compiled = compileArc4(Hello)
const appId = compiled.bareCreate().createdApp

Mock result can be set up for both of the snippets above as

const helloApp = ctx.any.application()
ctx.setCompiledApp(Hello, helloApp.id)

const spy = new ApplicationSpy(Hello)

// The mock setup is the same as using the explicit create method except
// `onBareCall` method is used instead of `on.{methodName}` or `onAbiCall` methods
// to register the callback
spy.onBareCall((itxnContext) => {
  itxnContext.createdApp = helloApp
})

1. itxn.applicationCall

const txn = itxn
  .applicationCall({
    appArgs: [methodSelector(Hello.prototype.greet), encodeArc4('world')],
    appId: helloApp,
  })
  .submit()
const result = decodeArc4<string>(txn.lastLog, 'log')

2. Strongly typed contract method call

const result = compiled.call.greet({
  args: ['world'],
  appId: app,
}).returnValue
assert(result === 'hello world')

Mock result can be set up for both snippets above as

// `itxnContext.setReturnValue` is added as the last entry to the logs of the constructed `itxn.ApplicationCall`
// so that it can be accessed via the `txn.lastLog` property.
// `setReturnValue` should only be called as the last statement of the callback and
// especially no further manipulations of logs should take place afterwards.
// `appArgs` collection holds method selector and method arguments encoded as `bytes` values.
// They need to be decoded if the original argument values are needed.
// You can check `itxnContext.appId` if there are multiple callbacks registered for the same method selector
// e.g.
// ```
// if (itxnContext.appId === helloApp) {
//   itxnContext.returnValue = `hello ${decodeArc4<string>(itxnContext.appArgs(0))}`
// }
// ```
spy.on.greet((itxnContext) => {
  itxnContext.returnValue = `hello ${decodeArc4<string>(itxnContext.appArgs(0))}`
})

You can also use the alternative approach below to set up the mock result. It is especially useful if you do not have a Contract subclass available and only the method signature and application id are available to make the method call.

// Create a spy without the contract type provided
const spy = new ApplicationSpy()

spy.onAbiCall(methodSelector('greet(string)string'), (itxnContext) => {
  // Check for a well-known appId or the appId provided to the contract under test in some other manner
  if (itxnContext.appId === appId) {
    itxnContext.setReturnValue(`hey ${decodeArc4<string>(itxnContext.appArgs(1))}`)
  }
})

3. Strongly typed ABI calls

const result = abiCall<typeof Hello.prototype.greet>({
  appId: app,
  args: ['abi'],
}).returnValue

Mock result can be set up for the snippet above as

// The setup is the same as in the previous case.
spy.on.greet((itxnContext) => {
  itxnContext.setReturnValue(`hello ${decodeArc4<string>(itxnContext.appArgs(0))}`)
})
  1. Reset Between Tests

    afterEach(() => {
      ctx.reset() // Clears all spies
    })
  2. Validate App IDs or approvalProgram

    spy.on.increment((itxnContext) => {
      // Only handle calls to specific app instance
      if (itxnContext.appId === counterApp) {
        itxnContext.setReturnValue(1n)
      }
    })
  3. Handle Method Arguments

    spy.on.setValue((itxnContext) => {
      // Arguments provided to the method are encoded as bytes values
      // and available via the `itxnContext.appArgs` method
      itxnContext.setReturnValue(`hello ${decodeArc4<string>(itxnContext.appArgs(0))}`)
    })