angular / angular-cli

CLI tool for Angular
https://cli.angular.dev
MIT License
26.79k stars 11.98k forks source link

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

Open catfireparty opened 4 years ago

catfireparty commented 4 years ago

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"
alan-agius4 commented 4 years ago

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

catfireparty commented 4 years ago

@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.

tvsbrent commented 3 years ago

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:

https://github.com/angular/angular-cli/blob/37a06a7c37f5b4286d58b475e6e12c86f00fac5b/packages/angular_devkit/schematics/testing/schematic-test-runner.ts#L113

Eventually that empty schematic wends its way here:

https://github.com/angular/angular-cli/blob/37a06a7c37f5b4286d58b475e6e12c86f00fac5b/packages/angular_devkit/schematics/src/engine/engine.ts#L254-L265

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

https://github.com/angular/angular-cli/blob/c1512e42742c17ace82e783e8e9c919ae925d269/packages/angular_devkit/schematics/tools/file-system-engine-host-base.ts#L288

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

clusterb93 commented 2 years ago

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.