terraform-cdk icon indicating copy to clipboard operation
terraform-cdk copied to clipboard

Unit testing - toHaveResource() - found no resources instead

Open sebolabs opened this issue 2 years ago • 12 comments

Community Note

  • Please vote on this issue by adding a 👍 reaction to the original issue to help the community and maintainers prioritize this request
  • Please do not leave "+1" or other comments that do not add relevant new information or questions, they generate extra noise for issue followers and do not help prioritize the request
  • If you are interested in working on this issue or have submitted a pull request, please leave a comment

cdktf & Language Versions

cdktf 0.11.0 (type-script)

Affected Resource(s)

Unit tests not working like I would expect. Could be me though :)

Debug Output

N/A

Expected Behavior

Test should pass unless I'm doing something wrong.

Actual Behavior

> [email protected] test
> jest

 FAIL  __tests__/main-test.ts (9.099 s)

  ● Configuration › should contain an application

    Expected azuread_application with properties {} to be present in synthesised stack.
    Found no azuread_application resources instead

      28 |         new PortalStack(scope, 'test');
      29 |       })
    > 30 |     ).toHaveResource(Application);
         |       ^
      31 |   });

      at Object.<anonymous> (__tests__/main-test.ts:30:7)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 0 todo, 0 passed, 1 total
Snapshots:   0 total
Time:        4.642 s

Steps to Reproduce

import "cdktf/lib/testing/adapters/jest";
import { Testing } from "cdktf";
import { PortalStack } from "../main";
import { Application } from "@cdktf/provider-azuread";

describe("Configuration", () => {
  it("should contain an application", () => {
    expect(
      Testing.synthScope((scope) => {
        new PortalStack(scope, 'test');
      })
    ).toHaveResource(Application);
  });
});

Important Factoids

  • cdktf synth/diff/deploy work perfectly fine
  • cdk.tf.json under cdktf.out contains the following:
[
  [...]
  "resource": {
    "azuread_application": {
      "xxxauthportal_portalapp_3876DFED": {
        "//": {
          "metadata": {
  [...]
]

References (main.ts)

import { Construct } from "constructs";
import {
  App,
  TerraformStack,
  RemoteBackend,
} from "cdktf";
import {
  AzureadProvider,
  ApplicationFeatureTags,
  Application,
} from "./.gen/providers/azuread";

export class PortalStack extends TerraformStack {
  constructor(scope: Construct, name: string) {
    super(scope, name);

    new AzureadProvider(this, "AzureAd", {});

    this.AzureAdApp();
  }
  
  AzureAdApp(this: PortalStack) {

    const samlIdUri = "";
    const samlReplyUrl = "";
    const samlLogoutUrl = "";

    const appFeatureTags: ApplicationFeatureTags = {
      enterprise: true,
    };

    new Application(this, "portal_app", {
      displayName: `AuthPortal`,
      featureTags: [appFeatureTags],
      identifierUris: [samlIdUri],
      web: {
        redirectUris: [samlReplyUrl],
        logoutUrl: samlLogoutUrl,
      },
      preventDuplicateNames: true,
    });
  }
}

const app = new App();
const stack = new PortalStack(app, "auth-portal");
new RemoteBackend(stack, {
  hostname: "app.terraform.io",
  organization: "xxx",
  workspaces: {
    name: `auth-portal`,
  }
});
app.synth();

sebolabs avatar Jun 07 '22 12:06 sebolabs

@sebolabs Currently you can't pass a stack instance to Testing.synthScope. You can work around by making your class extend from Construct instead. Take a look here for more info.

jsteinich avatar Jun 13 '22 12:06 jsteinich

While we seem to have answered the question I'd like to keep this issue open and add a proper warning / error if someone passes a stack into synthScope.

DanielMSchmidt avatar Jun 16 '22 11:06 DanielMSchmidt

While we seem to have answered the question I'd like to keep this issue open and add a proper warning / error if someone passes a stack into synthScope.

If not too difficult, actually just allowing a stack would be more user friendly.

jsteinich avatar Jun 17 '22 02:06 jsteinich

Can you folks help understand the solution here?

If I pass in a Construct instead of a Stack, I still don't get any generated resources from synthScope.

This is how I've modified the above example –

import "cdktf/lib/testing/adapters/jest";
import { Testing } from "cdktf";
+import { Construct } from "constructs";
import { PortalStack } from "../main";
import { Application } from "@cdktf/provider-azuread";

+ class PortalConstruct extends Construct {
+  constructor(scope: Construct, options: Record<PropertyKey, unknown>) {
+   super(scope, `${options.environment}-construct`)
+    new PortalStack(scope, environment)
+   }
+}

describe("Configuration", () => {
  it("should contain an application", () => {
    expect(
      Testing.synthScope((scope) => {
+        new PortalConstruct(scope, 'test');
-        new PortalStack(scope, 'test');
      })
    ).toHaveResource(Application);
  });
});

paambaati avatar Jul 20 '22 08:07 paambaati

This is because your construct contains a stack. A stack does not synthesize into JSON like a construct would, but it writes a file to disk with it's childrens JSON in it. This hides the content from the testing marchers. You should abstract the things you do in a stack in one or more constructs and test how they work together here

DanielMSchmidt avatar Jul 20 '22 09:07 DanielMSchmidt

Running into the same issue. Is there any complete example that can be posted by the team to make it easier for users who run into this issue with testing?

thanks!

pchaganti avatar Jul 29 '22 12:07 pchaganti

Running into the same issue. Is there any complete example that can be posted by the team to make it easier for users who run into this issue with testing?

thanks!

Hopefully this helps. There are also internal tests of the testing framework now (for example: https://github.com/hashicorp/terraform-cdk/tree/main/test/python/testing-matchers)

jsteinich avatar Aug 02 '22 04:08 jsteinich

@jsteinich Thanks, that does help a bit, although it could use some more clarity –

  1. The inline comment in the linked example says // Could be a class extending from Construct but in other parts of the guide, we're always shown TerraformStack and not Construct as examples.

Additionally, not all of us use Jest, even though it is the most popular testing framework. For folks that use other frameworks, it would help if there was an example that shows how to use Constructs. I for one use tap, and I've managed to make this work with a combination of reading the original Jest plugin implementation and comments from this issue –

import tap from 'tap'
import expect from 'expect'
import { Testing, testingMatchers } from 'cdktf'
import { MyConstruct } from '../main'
import { Record as CFDNSRecord } from '../.gen/providers/cloudflare'
import type { TerraformConstructor } from 'cdktf/lib/testing/matchers'

tap.Test.prototype.addAssert(
  'toHaveResource',
  3,
  // NOTE: has to be a regular function and not an arrow function
  // for the `this` binding to work correctly.
  function toHaveResource(
    hcl: string,
    message: string,
    resource: {
      ctor: TerraformConstructor
      props: Record<string, string>
    },
  ) {
    const msg = message || 'Terraform stack is missing property'
    const toHaveResourceWithProperties =
      testingMatchers.getToHaveResourceWithProperties(
        (items: Array<string>, assertedProperties: Record<string, unknown>) => {
          if (Object.entries(assertedProperties).length === 0) {
            return items.length > 0
          }
          return expect
            .arrayContaining([expect.objectContaining(assertedProperties)])
            .asymmetricMatch(items)
        },
      )
    const self = this as unknown as typeof tap
    return self.ok(
      toHaveResourceWithProperties(hcl, resource.ctor, resource.props).pass,
      msg,
      resource,
    )
  },
)

void tap.test('infra > should include all resources', (t) => {
  t.plan(1)
  const hclJson = Testing.synthScope((scope) => {
    new MyConstruct(scope)
  })

  t.toHaveResource(hclJson, 'should have the Cloudflare DNS record', {
    ctor: CFDNSRecord,
    props: { type: 'CNAME' },
  })
  t.end()
})

paambaati avatar Aug 02 '22 05:08 paambaati

  1. The inline comment in the linked example says // Could be a class extending from Construct but in other parts of the guide, we're always shown TerraformStack and not Construct as examples.

https://www.terraform.io/cdktf/concepts/constructs has some more information on constructs, including an example and a comparison to stacks.

I for one use tap, and I've managed to make this work

That's great. You can also use the Testing class directly which could possibly reduce the amount of adapter code needed.

jsteinich avatar Aug 02 '22 22:08 jsteinich

I just ran across this issue. I used cdktf init --template=typescript to create a project. I then started populating my generated project with resources and started populating my generated test examples with tests.

Then I ran into Found no github_issue_label resources instead even though it was.

Ran into this issue from a search. I didn't see anything from docs I came across of the need to use Construct if I want to do unit testing.

Seems cdktf init generates project using TerraformStack which sounds like from this issue is not compatible with the unit test examples it generates for unit testing.

As a new user the docs and generated code don't seem to talk about this issue ... and would be nice if initial setup worked more out of the box.

jmccann avatar Feb 07 '23 15:02 jmccann

I've read all the links here and I'm still confused about what a MyApplicationsAbstraction looks like. I also don't understand why my example isn't working? Thanks for the awesome library!

it("should contain a container", () => {
  const stack = new OnPremStack(Testing.app(), "learn-cdktf-docker");
  const synthesized = Testing.synth(stack);
  expect(synthesized).toHaveResource(Container);
});

iot-resister avatar Aug 06 '23 18:08 iot-resister

Hey @iot-resister!

I've read all the links here and I'm still confused about what a MyApplicationsAbstraction looks like.

MyApplicationsAbstraction is a bit ambiguous as it is written in the Typescript documentation as it at times refers to a Construct, and at other times a Stack. What's important to note is that in your usage (i.e. OnPremStack) it refers to a Stack, as the instance of OnPremStack is being passed into Testing.synth directly.

I also don't understand why my example isn't working? Thanks for the awesome library!

Hard to say exactly what the issue is from the snippet you've provided, could you provide a bit more context on what OnPremStack looks like and the specific issue you're running into? One thing that comes to mind is that the Container instance is within another Stack that is nested in OnPremStack.

Maed223 avatar Aug 08 '23 23:08 Maed223