Skip to content

Schematics: Unit testing a rule that uses mergeWith/apply fails due to missing context #17205

Description

@catfireparty

Addendum

In creating the test case I've realised that I'm not passing a partialContext to callRule, so it is definitely missing context, however, it is not at all clear what context is necessary for mergeWith to succeed and the error does not indicate that context is actually missing.

Happy for this to be marked as a question instead. Guidance on how to provide the context and what context to provide would be amazingly useful.

🐞 Bug report

Description

Using callRule to unit test rules within a schematic fails when it comes to rules which return mergeWith

🔬 Minimal Reproduction

See: https://github.com/jdpearce/schematics-apply-problem

Given a rule which applies template files to the tree, this unit test will fail with TypeError: Cannot read property 'path' of undefined:

describe('applyTemplateFiles Rule', () => {
  let tree: UnitTestTree;

  beforeEach(async () => {
    tree = new UnitTestTree(Tree.empty());
  });

  it('should apply the files to the tree', async () => {
    const schema = {};

    tree = (await callRule(applyTemplateFiles(schema), tree)) as UnitTestTree;

    expect(tree.files).toEqual(jasmine.arrayContaining(['/libs/destination/test.txt']));
  });
});

🔥 Exception or Error

Failures:
1) applyTemplateFiles Rule should apply the files to the tree
  Message:
    TypeError: Cannot read property 'path' of undefined
  Stack:
        at <Jasmine>
        at ./apply-problem/node_modules/@angular-devkit/schematics/tools/file-system-engine-host-base.js:216:96
        at ./tmp/apply-problem/node_modules/@angular-devkit/schematics/src/rules/url.js:13:73
        at Object.callSource (./tmp/apply-problem/node_modules/@angular-devkit/schematics/src/rules/call.js:55:20)
        at ./tmp/apply-problem/node_modules/@angular-devkit/schematics/src/rules/base.js:45:60
        at Object.callSource (./tmp/apply-problem/node_modules/@angular-devkit/schematics/src/rules/call.js:55:20)
        at ./tmp/apply-problem/node_modules/@angular-devkit/schematics/src/rules/base.js:53:23
        at MergeMapSubscriber.project (./tmp/apply-problem/node_modules/@angular-devkit/schematics/src/rules/call.js:74:24)
        at MergeMapSubscriber._tryNext (./tmp/apply-problem/node_modules/rxjs/internal/operators/mergeMap.js:69:27)
        at MergeMapSubscriber._next (./tmp/apply-problem/node_modules/rxjs/internal/operators/mergeMap.js:59:18)
        at MergeMapSubscriber.Subscriber.next (./tmp/apply-problem/node_modules/rxjs/internal/Subscriber.js:66:18)

2 specs, 1 failure

🌍 Your Environment

"@angular-devkit/core": "^9.0.6",
"@angular-devkit/schematics": "^9.0.6",
"@types/jasmine": "^3.3.9",
"@types/node": "^8.0.31",
"jasmine": "^3.3.1",
"typescript": "~3.5.3"

Activity

  1. added this to the Backlog milestone on Mar 13, 2020
  2. alan-agius4 commented on Mar 13, 2020

    @alan-agius4
    Collaborator

    Hi, I think the problem here is that the destination folder (libs) doesn't exist.

  3. catfireparty commented on Mar 16, 2020

    @catfireparty
    Author

    @alan-agius4 If you check the linked repo, running the schematic fully succeeds. The problem is in the missing context when using callRule. It's not clear what that missing context should be in the case of a unit test.

  4. tvsbrent commented on Jun 1, 2021

    @tvsbrent

    Having hit the same issue with a rule I'm working on and from digging in, it looks like it is due to the fact that the callRule method in the test runner doesn't create a real Schematic, but just passes in an empty object:

    const context = this._engine.createContext({} as Schematic<{}, {}>, parentContext);

    Eventually that empty schematic wends its way here:

    let context: TypedSchematicContext<CollectionT, SchematicT> = {
    debug: (parent && parent.debug) || false,
    engine: this,
    logger:
    (parent && parent.logger && parent.logger.createChild(schematic.description.name)) ||
    new logging.NullLogger(),
    schematic,
    strategy:
    parent && parent.strategy !== undefined ? parent.strategy : this.defaultMergeStrategy,
    interactive,
    addTask,
    };

    Finally, when the file system engine tries to get the path from the schematic, it blows up, as description is not defined:

    const fileDescription = context.schematic.description as { path?: string };

    I think the callRule method would need to provide a way to override or extend that empty schematic object to make this work.

  5. clusterb93 commented on Mar 30, 2022

    @clusterb93

    We had the same problem described below and the only solution that we found was to create a CustomSchematicTestRunner that extends the angular-devkit SchematicTestRunner. This custom class exposes a new method that allows to pass a test schematic and context as parameters :

    /**
     * Custom Schematic Test Runner
     */
    
    import { Observable, of as observableOf } from 'rxjs';
    import {SchematicTestRunner} from "@angular-devkit/schematics/testing";
    import {callRule, Rule, Schematic, SchematicContext, Tree} from "@angular-devkit/schematics";
    
    export class CustomSchematicTestRunner extends SchematicTestRunner{
        constructor(collectionName: string, collectionPath: string) {
            super(collectionName, collectionPath);
        }
    
        /**
         * Calls a schematics Rule using an isolated context and the schematics passed in as parameter
         * @param rule
         * @param tree
         * @param schematic
         * @param parentContext
         */
        callSchematicsRule(rule: Rule, tree: Tree, schematic: Schematic<{}, {}>,  parentContext?: Partial<SchematicContext>): Observable<Tree> {
            const context = this.engine.createContext(schematic, parentContext);
            return callRule(rule, observableOf(tree), context);
        }
    }
    

    And in the Rule's unit test the schematic and the context were setted in a beforeEach function as follows :

     schematicTest = {
                description : {
                    name: "Test schematic",
                    path: "src/init"
                }
            };
     testContext = {
                debug: true,
                engine: this,
                logger:
                    new logging.NullLogger(),
                schematicTest,
                strategy: MergeStrategy.Default,
            };
    

    And the final test looks like :

    it('should create a jenkinsfile', async () => {
            const tree = await schematicRunner
                .callSchematicsRule(createJenkinsfile(), appTree, schematicTest as Schematic<{}, {}>, testContext)
                .toPromise();
    
            expect(tree.exists("Jenkinsfile")).toBeTrue();
        });
    

    Hope this technique helps and let you unit test your schematic's Rules waiting for an official fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions