Testing

Although the presentation framework itself is thoroughly tested, consumers should still verify they get expected results for their iModel + ruleset combinations. The @bentley/presentation-testing package is delivered purely for that reason.

The package delivers an API that allows creating hierarchies for supplied iModels and rulesets. Consumers can then verify the result using tools of their liking. Our recommendation is to use snapshot testing for 2 reasons:

  1. resulting hierarchies get rather large - testing the them in code might be difficult
  2. snapshots protect against regressions

Example

An example of setting up snapshot tests with the @bentley/presentation-testing package:

describe("RulesetTesting", () => {
  let imodel: IModelConnection;
  const imodelPath = "assets/datasets/Properties_60InstancesWithUrl2.ibim";

  before(async () => {
    // initialize presentation-testing
    await initialize();
  });

  after(async () => {
    // terminate presentation-testing
    await terminate();
  });

  beforeEach(async () => {
    // set up for testing imodel presentation data
    imodel = await SnapshotConnection.openFile(imodelPath);
  });

  afterEach(async () => {
    await imodel.close();
  });

  it("generates correct hierarchy", async () => {
    const ruleset: Ruleset = {
      id: "test",
      rules: [{
        ruleType: RuleTypes.RootNodes,
        autoExpand: true,
        specifications: [{
          specType: ChildNodeSpecificationTypes.InstanceNodesOfSpecificClasses,
          classes: [{
            schemaName: "BisCore",
            classNames: ["Subject"],
          }],
          instanceFilter: "this.Parent = NULL",
          arePolymorphic: false,
          groupByClass: false,
          groupByLabel: false,
        }],
      }, {
        ruleType: RuleTypes.ChildNodes,
        condition: "ParentNode.IsOfClass(\"Subject\", \"BisCore\")",
        onlyIfNotHandled: true,
        specifications: [{
          specType: ChildNodeSpecificationTypes.RelatedInstanceNodes,
          relationshipPaths: [{
            relationship: {
              schemaName: "BisCore",
              className: "SubjectOwnsSubjects",
            },
            direction: RelationshipDirection.Forward,
          }],
          groupByClass: false,
          groupByLabel: false,
        }, {
          specType: ChildNodeSpecificationTypes.InstanceNodesOfSpecificClasses,
          classes: {
            schemaName: "BisCore",
            classNames: ["Model"],
          },
          arePolymorphic: true,
          relatedInstances: [{
            relationshipPath: {
              relationship: {
                schemaName: "BisCore",
                className: "ModelModelsElement",
              },
              direction: RelationshipDirection.Forward,
              targetClass: {
                schemaName: "BisCore",
                className: "InformationPartitionElement",
              },
            },
            alias: "partition",
            isRequired: true,
          }],
          instanceFilter: "partition.Parent.Id = parent.ECInstanceId AND NOT this.IsPrivate",
          groupByClass: false,
          groupByLabel: false,
        }],
      }, {
        ruleType: RuleTypes.ChildNodes,
        condition: "ParentNode.IsOfClass(\"Model\", \"BisCore\")",
        onlyIfNotHandled: true,
        specifications: [{
          specType: ChildNodeSpecificationTypes.RelatedInstanceNodes,
          relationshipPaths: [{
            relationship: {
              schemaName: "BisCore",
              className: "ModelContainsElements",
            },
            direction: RelationshipDirection.Forward,
          }],
          instanceFilter: "this.Parent = NULL",
          groupByClass: false,
          groupByLabel: false,
        }],
      }, {
        ruleType: RuleTypes.ChildNodes,
        condition: "ParentNode.IsOfClass(\"Element\", \"BisCore\")",
        onlyIfNotHandled: true,
        specifications: [{
          specType: ChildNodeSpecificationTypes.RelatedInstanceNodes,
          relationshipPaths: [{
            relationship: {
              schemaName: "BisCore",
              className: "ElementOwnsChildElements",
            },
            direction: RelationshipDirection.Forward,
          }],
          groupByClass: false,
          groupByLabel: false,
        }],
      }],
    };
    const builder = new HierarchyBuilder({ imodel });
    // generate the hierarchy using a ruleset id
    const hierarchy = await builder.createHierarchy(ruleset);
    // verify through snapshot
    expect(hierarchy).to.matchSnapshot();
  });

  // VSTS#156270
  // eslint-disable-next-line prefer-arrow/prefer-arrow-functions
  it.skip("generates correct content", async function () {
    const ruleset: Ruleset = {
      id: "test",
      rules: [{
        ruleType: RuleTypes.Content,
        specifications: [{
          specType: ContentSpecificationTypes.SelectedNodeInstances,
        }],
      }],
    };
    const builder = new ContentBuilder({ imodel });
    // generate content using ruleset id
    const instances = await builder.createContentForInstancePerClass(ruleset);

    // verify through snapshot
    // we loop through each instance and create a separate snapshot file
    // because snapshot engine has difficulties parsing big files
    for (const instance of instances) {
      // not providing filename and snapshot name to the 'matchSnapshot', because it seems
      // to ignore them when ChaiJestSnapshot.setFilename and setTestName is used
      configureSnapshotLocation(this.test!, "ruleset-testing-content-snaps", instance);
      expect(instance.records).to.matchSnapshot();
    }
  });

});

Things to keep in mind

  • Run initialize() before and terminate() after the tests

  • Don't forget to close the iModel connection

  • Ruleset can be provided either as an ID of already registered ruleset or as a Ruleset object. The object can even be imported from a JSON file:

    await builder.createHierarchy(require("rulesets/YourRuleset"))
    

Last Updated: 12 June, 2024