getsaf / shallow-render

Angular testing made easy with shallow rendering and easy mocking. https://getsaf.github.io/shallow-render
MIT License
273 stars 25 forks source link

I dont see the point in shallow-renderer #183

Closed spectralLight closed 4 years ago

spectralLight commented 4 years ago

you state that:

- Reuses (and verifies) MyModule contains your component and all its dependencies.
- All components inside MyModule are mocked. This is what makes the rendering "shallow".
- The tests have much less boilerplate which makes the specs easier to follow.

This makes for me no sense. You can do the same without shallow-renderer. You Mock all the dependencies and put them in the declarations in the testbed. And if your not intereseted in certain componetns/dependencies you combine it with NO_ERROR_SCHEMA. Then you also have shallow rendering. So there is not much more boilerplate code than in your examples. I mean you move the boilerplate code into MyModule and recreate everything there instead. So you recreate the testbed there.

And in your example on stackblitz you cannot really distinguish if these are the real components or just the mocked ones you use in the TestModule e.g. shallow(TestComponent, TestModule)

Compare with the nestedComponent article on angular.io: https://angular.io/guide/testing-components-scenarios#nested-component-tests

Please enlighten me. I dont think that your shallow-renderer is needed to test angular shallow, though it has some additional cool features.

getsaf commented 4 years ago

The overarching issue is that NO_ERRORS_SCHEMA allows any html, even for invalid component selectors, inputs and outputs. You can very easily have typos in your template and NO_ERRORS_SCHEMA will pass your test when it should fail. I find this to be unacceptable when all the information necessary to verify selectors/inputs/outputs is right there in your module.

Things you don't get from TestBed/NO_ERRORS_SCHEMA alone:

And in your example on stackblitz you cannot really distinguish if these are the real components or just the mocked ones you use in the TestModule e.g. shallow(TestComponent, TestModule)

Maybe there are some things I could clear up in the examples. If you wouldn't mind pointing me to any confusion in a test, I'll be happy to rework the example to clear it up.

The simple rule for what is mocked and what is not mocked is simple: Everything is mocked except for the component you are testing.

// MyComponent is not mocked, *all* other things from the module are mocked
shallow(MyComponent, MyModule);

Compare with the nestedComponent article on angular.io: https://angular.io/guide/testing-components-scenarios#nested-component-tests

That example has a very large amount of boilerplate.

Their top suggestion is:

@Component({selector: 'app-banner', template: ''}) // duplication from the original component, not type-safe
class BannerStubComponent {
}

@Component({selector: 'router-outlet', template: ''}) // duplication from the original component, not type-safe
class RouterOutletStubComponent {
}

@Component({selector: 'app-welcome', template: ''}) // duplication from the original component, not type-safe
class WelcomeStubComponent {
}

beforeEach(() => {
  TestBed
      .configureTestingModule({
        declarations: [ // Everything here is duplication from the module
          AppComponent, RouterLinkDirectiveStub, BannerStubComponent, RouterOutletStubComponent,
          WelcomeStubComponent
        ]
      })
});

With shallow-render:

beforeEach(() => {
  shallow = new Shallow(AppComponent, AppModule));
})

Less code is not even the main benefit here. If you took Angular's suggestion, then you will now have a test for AppComponent, written against test-double components instead of against the contracts for the actual components used in your code. If you make a contract change to the BannerComponent in a way that breaks your AppComponent, the AppComponent test should fail, forcing you to fix the issue in AppComponent but in Angular's approach, the AppComponent test would continue to pass even though your component is broken unless you remembered to update your test-doubles every time you make changes to real components.

I appreciate the feedback and would suggest converting one or two of your unit tests with shallow-render to see first-hand how it can help. I'm very interested in any deficiencies you have come across in the documentation, any notes would be appreciated.

spectralLight commented 4 years ago

Open Issue

Thanks for your detailed reply. I see how your library can help its like jest and enzyme for react. But im not sure if it is a good thing for the future to depend on your library for the tests that are so essential to my Product.

And i still contradict against the contracts thing. Maybe there is something I don't understand. I'll try to make myself clear.

You say:

If you took Angular's suggestion, then you will now have a test for AppComponent, written against test-double components >instead of against the contracts for the actual components used in your code

I agree with that. But you could mock those components independently from shallow-renderer with jasmine (with spies etc) and then you don't write against the contracts. But i confess writing tests with your library looks clear and elegant and takes off that work of me.

Feedback for Doc

I'm very interested in any deficiencies you have come across in the documentation, any notes would be appreciated.

I think if you take your time in the StackBlitz examples everything gets crisp clear. But what confused me was the statement:

The simple rule for what is mocked and what is not mocked is simple: Everything is mocked except for the component you are testing.

so i thought you mock them in the angular fashion and thought you recreate the testbed in MyModule like the following:

@Component({
  selector: 'app-todo-item',
  template: ''
})
export class TodoItemComponentMock implements OnInit, TodoItemComponent {

  @Input() public todoItem: TODOItem;
  @Input() public readOnlyTODO: boolean;
  @Output() public todoDelete = new EventEmitter();
  @Output() public todoEdit = new EventEmitter();

  constructor() { }

  public ngOnInit() {
  }

  public completeClick() {
  }

  public deleteClick() {
  }

  public editClick() {
  }
}

@NgModule({
  declarations: [TodoItemComponentMock],
  providers: [RedService],
})
class MyModule {}
getsaf commented 4 years ago

so i thought you mock them in the angular fashion and thought you recreate the testbed in MyModule like the following

You're pretty much spot-on here. Shallow mocks your entire module tree automatically in the same way Angular suggest in their docs that you do manually. We mock components/directives/pipes/injection tokens with all the same inputs/outputs defined but with basically empty templates. One difference from your example is that we do not automatically create mocks of prototype functions of the mocked components (completeClick, deleteClick, editClick, ngOnInit in your example) because it's not possible to do this in a type-safe way and since the component is mocked, they often go unused anyway. If for some reason you need to make use of a mocked-component's prototype functions, we make it easy to mock manually and explicitly with:

  shallow.mock(SomeDependentComponent, { getFoo: () => 'mocked foo response' })

Another note here is that under-the-hood, it's all making use of TestBed so we're not really eliminating TestBed, we're just automating the hardest, most error-prone part of testing which is the TestBed module setup and the initial render. After you render, you could technically use TestBed to write the rest of your test (although I don't recommend it).

Let me know if you have any other questions/suggestions or issues. I'll probably be re-evaluating the docs here soon so I can start properly versioning them and It's probably a good time to review those StackBlitz examples too. Thanks for the feedback.