testing icon indicating copy to clipboard operation
testing copied to clipboard

Feature request: Mocking/stubbing sub-views

Open yacuzo opened this issue 8 years ago • 24 comments

Hi,

I know this is heavily a work-in-progress library, but I also know you want feedback :)

Having used the ComponentTester for a few components we find the need for mocking/stubbing out sub-views which are imported through the html-require tag.

Prehaps a bit of background is helpful: We have a separate suit of tests we call binding-tests. They are a form of regression-tests to avoid the problem of renaming view model properies and forgetting to rename the bindings in the HTML. We want these as unit-tests so that we can get feedback asap.

This creates a problem where we have a component foo:

export class Foo {
  fooData;
  get processedFoo() {
    return processFoo();
  }
}

With html:

<template>
  <require from="some/path/bar" />
  <input value.bind="fooData" />

  <bar some-data.bind="processedFoo"></bar>
</template>

We want to check the binding for bar's 'some-data', but we do not want to parse the possibly complex output of bar in order to check that. Bar's output should not be a part of the test for Foo.

Is there a way to do this currently? Have you considered a use-case like this?

yacuzo avatar Jun 30 '16 11:06 yacuzo

I'd say, the best way to get something like this in would be to submit a PR with the proposed API.

However, for this specific scenario, it seems like you would just want to gain access to the view-model for bar and check it's property. That is possible, though a nicer API could be made available.

EisenbergEffect avatar Jul 10 '16 13:07 EisenbergEffect

I dont want to check the property on the model. I want to check that my binding-expression in foo's template corresponds with something on the model. As I see it the best way to do this would be to mock out the bar-component, and check that the mock renders something other than undefined.

I'm not sure what the API for mocking out 'bar' would be. The ComponentTester.WithResources needs a way to override the require-tag in the template.

yacuzo avatar Aug 02 '16 07:08 yacuzo

We have very complex custom elements which require many other custom elements. To test any one of the complex custom element we had to mock the dependencies of all the other required custom elements which is not ideal. As @yacuzo already mentioned, if the component tester provides a way to only render the top level custom element, then it would greatly reduce our element setup for testing.

suneelv avatar Jul 03 '17 09:07 suneelv

Exactly this. For example, say I have a component Mycomponent:

<template>
    <require from="./mysubcomponent"></require>

    <mysubcomponent if.bind="myData" data.bind="myData"></mysubcomponent>
</template>

I've already written a separate test for Mysubcomponent, mocking out services and the like. I don't want to have to mock out those same services again in my test of Mycomponent just to test whether the if.bind works as expected.

Is there some workaround to mock out mysubcomponent during the test of mycomponent?

RomkeVdMeulen avatar Feb 13 '18 16:02 RomkeVdMeulen

This pretty much sounds like a shallow rendering of the top level component and ditching childs. So essentially If we'd unregister the child elements they should be rendered as normal html.

zewa666 avatar Feb 13 '18 21:02 zewa666

@zewa666 Any way to do that manually with the current API?

RomkeVdMeulen avatar Feb 15 '18 10:02 RomkeVdMeulen

We're currently looking into it to find the manual way. Will post back here and then go for a nicer overall API.

zewa666 avatar Feb 15 '18 10:02 zewa666

@zewa666 Great! Thank you all for the prompt support.

RomkeVdMeulen avatar Feb 15 '18 10:02 RomkeVdMeulen

Any progress on this? I've been stubbing support for services used by subcomponents, and it's starting to become a bit tiresome.

RomkeVdMeulen avatar Mar 08 '18 10:03 RomkeVdMeulen

We can do this, via hooking to afterCompile, but how would the API would be like:

  .withResources('module1', {
    exclude: {
      elements: ['name1', 'name2']
    }
  }) 

or

  .withResources('...')
  .discardElements('name1', 'name2')

?

bigopon avatar Mar 11 '18 00:03 bigopon

How about .shallow() and exclude all internally?

zewa666 avatar Mar 11 '18 07:03 zewa666

What about the <require/> statement, do we skip it entirely ? as when loading a module like foo.js to get custom element <foo/>, we might have additional resource to load ?

bigopon avatar Mar 12 '18 06:03 bigopon

It might be good to have the option of excluding some but not all subcomponents, in addition to .shallow()

@zewa666 Have you found a current workaround for this problem?

RomkeVdMeulen avatar Mar 15 '18 10:03 RomkeVdMeulen

I think it's quite hard to shallow elements by name, and I think it's the loading sub module behavior that we don't want, not the rendering the elements.

For example:

module A --requires--> module B --requires--> module C

When saying the test should ignore b, and c, does it implies (1) do not touch module B and module C, or (2) load module B, and load module C but do not do anything after that ? For me, (1) looks more desirable. Solution for (1) is also easier to create, we only need to patch the loader loadAllModules(...) (as it is called by ViewEngine.prototype.importViewResources) with a set of ignored module. I'm not sure about (2)

bigopon avatar May 25 '18 13:05 bigopon

In theory the module files for subcomponents shouldn't even be loaded. However, modules with custom binding behaviors or value converters do need to be loaded for a proper test. I'm not sure you can distinguish them a priori, so some blacklisting or whitelisting API will probably be necessary.

RomkeVdMeulen avatar Nov 23 '18 10:11 RomkeVdMeulen

There doesn't seem to be any progress on this. Is there anything I can do to help further lock down the API for this?

RomkeVdMeulen avatar Feb 12 '19 11:02 RomkeVdMeulen

@RomkeVdMeulen If this is only stubbing dependencies from <require> element, then couple of override methods related to dependency management in TemplateRegistryEntry will work, as once demonstrated by lazy global resources from @huochunpeng

For static view strategy, either via static $view field/method or @view decorator, we just need to override method loadViewFactory of it to adjust the dependencies before loading, and return it back to normal after loading.

bigopon avatar Feb 12 '19 12:02 bigopon

@bigopon I'd like to try this out on our tests, see if it does what we need. I'm not sure where to start though. Is there a way for me to patch something locally to try out your ideas?

RomkeVdMeulen avatar Feb 14 '19 09:02 RomkeVdMeulen

Here is the answer from @huochunpeng in a discourse thread https://discourse.aurelia.io/t/lazy-load-global-custom-elements/1627/8

It's basically an override on TemplateRegistryEntry.prototype.template setter in aurelia-loader module to automatically add dependency based on a globally available path, locally to that template.

We can apply the same technique, to automatically remove all <require/> that match stubs.

For static view strategy, either via static $view field/method or @view decorator, we just need to override method loadViewFactory of it to adjust the dependencies before loading, and return it back to normal after loading.

This is about our new feature: static view. Normally dependencies is defined like following:

@view({
  template: '<template></template>',
  dependencies: () => [SomeClass, import('some/module')],
  // or
  dependencies: [SomeClass, import('some/module')]
})
export class MyElement {
  ...
}

or

export class MyElement {
  static $view = {
    template: '<template></template>',
    dependencies: () => [SomeClass, import('some/module')],
    // or
    dependencies: [SomeClass, import('some/module')]
  }
}

This is harder to stub, and I think it will require some patching on StaticViewStrategy.prototype.loadViewFactory export of aurelia-templating

StaticViewStrategy.prototype.loadViewFactory = async function() {
  const dependencies = await Promise.all(this.dependencies);
  // now what to do with dependencies ???
  //  they are all classes, or module objects
}

I'm not sure about this.

cc @zewa666 @EisenbergEffect

bigopon avatar Feb 14 '19 10:02 bigopon

@RomkeVdMeulen If this works for your project, perhaps we can extract your code into a helper method that we add to this testing library. We can start with the require-based scenario first and then support static views in a second iteration. If most people are using require-based views then that lets us get the bulk of people a solution sooner.

EisenbergEffect avatar Feb 15 '19 00:02 EisenbergEffect

@bigopon Thanks! I got it to work with our <require> based setup. I adjusted @huochunpeng's approach a bit: there's no need to traverse the whole template's tree, we can simply filter out the unwanted dependencies afterward:

import {TemplateDependency, TemplateRegistryEntry} from "aurelia-loader";

export const STUBBED_DEPENDENCIES: string[] = [];

const templateDescriptor = Object.getOwnPropertyDescriptor(TemplateRegistryEntry.prototype, "template")!;
const oldGet = templateDescriptor.get!;
const oldSet = templateDescriptor.set!;
Object.defineProperty(TemplateRegistryEntry.prototype, "template", {
  get: oldGet,
  set(value: HTMLTemplateElement) {
    oldSet.call(this, value);
    if (STUBBED_DEPENDENCIES.length > 0) {
      this.dependencies = this.dependencies
          .filter((d: TemplateDependency) => !STUBBED_DEPENDENCIES.includes(d.src));
    }
  },
});

Example use (made this up without checking):

describe("Parent component", () => {

  before(() => STUBBED_DEPENDENCIES.push("src/components/child-component/child-component"));
  after(() => STUBBED_DEPENDENCIES.splice(0));

  it("doesn't load dependencies you don't want it to", async () => {
    const component = await StageComponent
        .withResources('parent-component')
        .inView('<parent-component></parent-component>')
        .create(bootstrap);

    expect(component.innerHTML).to.equal("<child-component></child-component>"
        + "<other-child>This does get loaded</other-child>");
  });
});

RomkeVdMeulen avatar Feb 15 '19 16:02 RomkeVdMeulen

@RomkeVdMeulen Yes this way is better, when it's only about dependencies removal. Would you accept the invitation of helping folks out please 😁

bigopon avatar Feb 15 '19 22:02 bigopon

We'd love a pull request @RomkeVdMeulen 😄

EisenbergEffect avatar Feb 16 '19 00:02 EisenbergEffect

I went home for today. I'll take a look at it monday.

RomkeVdMeulen avatar Feb 16 '19 04:02 RomkeVdMeulen