From 820dc946c0c4963c1ea715584471d25ff44e6c60 Mon Sep 17 00:00:00 2001 From: MG Date: Mon, 2 Nov 2020 22:15:48 +0100 Subject: [PATCH] fix: support of ngOnChanges from OnChanges interface --- .codeclimate.yml | 14 ++ README.md | 37 ++++ examples/TestLifecycleHooks/test.spec.ts | 214 +++++++++++++++++++++++ package.json | 35 ++-- 4 files changed, 280 insertions(+), 20 deletions(-) create mode 100644 examples/TestLifecycleHooks/test.spec.ts diff --git a/.codeclimate.yml b/.codeclimate.yml index 44bbd6c93d..d13c22f78a 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,7 +1,21 @@ version: '2' exclude_patterns: + - 'dist/' - 'e2e/' - 'examples/' + - 'node_modules/' + - 'test-reports/' - 'tests/' + - 'tmp/' - '**/*.spec.ts' - '**/*.fixtures.ts' +analysis_paths: + - 'lib/' +plugins: + structure: + enabled: true + duplication: + enabled: true + config: + languages: + typescript: diff --git a/README.md b/README.md index 04fde799cd..a114a64053 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ I'm open to contributions. - [a structural directive](#how-to-test-a-structural-directive) - [a structural directive with a context](#how-to-test-a-structural-directive-with-a-context) - [a pipe](#how-to-test-a-pipe) + - [ngOnChanges lifecycle hook](#how-to-test-ngonchanges-lifecycle-hook) - [a provider](#how-to-test-a-provider) - [a token](#how-to-test-a-token) - [a multi token](#how-to-test-a-multi-token) @@ -1743,6 +1744,9 @@ beforeEach(() => `MockRender` is a simple tool that helps with **shallow rendering in Angular tests** when we want to assert `Inputs`, `Outputs`, `ChildContent` and custom templates. +The best thing about it is that `MockRender` properly triggers all lifecycle hooks, +and allows **to test `ngOnChanges` hook from `OnChanges` interface**. + **Please note**, that `MockRender(MyComponent)` is not assignable to `ComponentFixture`. You should use either @@ -2420,6 +2424,7 @@ Just [contact us](#find-an-issue-or-have-a-question-or-a-request). - [testing a structural directive](#how-to-test-a-structural-directive) - [testing a structural directive with a context](#how-to-test-a-structural-directive-with-a-context) - [testing a pipe](#how-to-test-a-pipe) +- [testing ngOnChanges lifecycle hook](#how-to-test-ngonchanges-lifecycle-hook) - [testing a provider](#how-to-test-a-provider) - [testing a token](#how-to-test-a-token) - [testing a multi token](#how-to-test-a-multi-token) @@ -2678,6 +2683,38 @@ to play with. --- +### How to test `ngOnChanges` lifecycle hook + +`TestBed.createComponent` does not support `ngOnChanges` out of the box. +That is where [`MockRender`](#mockrender) might be helpful. + +Simply use it instead of `TestBed.createComponent`. + +```typescript +const fixture = MockRender(TargetComponent, { + input: '', +}); +// The hook has been already called here. +``` + +Changes of parameters will trigger the hook. + +```typescript +fixture.componentInstance.input = 'change'; +fixture.detectChanges(); // <- triggers the hook again. +// Here we can do desired assertions. +``` + +A source file of this test is here: +[TestLifecycleHooks](https://github.com/ike18t/ng-mocks/blob/master/examples/TestLifecycleHooks/test.spec.ts).
+Prefix it with `fdescribe` or `fit` on +[codesandbox.io](https://codesandbox.io/s/github/satanTime/ng-mocks-cs?file=/src/examples/TestLifecycleHooks/test.spec.ts) +to play with. + +[to the top](#content) + +--- + ### How to test a provider Usually, you don't need `TestBed` if you want to test a simple diff --git a/examples/TestLifecycleHooks/test.spec.ts b/examples/TestLifecycleHooks/test.spec.ts new file mode 100644 index 0000000000..77a5dfd34a --- /dev/null +++ b/examples/TestLifecycleHooks/test.spec.ts @@ -0,0 +1,214 @@ +// tslint:disable:member-ordering + +import { + AfterContentChecked, + AfterContentInit, + AfterViewChecked, + AfterViewInit, + ChangeDetectionStrategy, + Component, + Injectable, + Input, + NgModule, + OnChanges, + OnDestroy, + OnInit, +} from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { MockBuilder, MockRender, ngMocks } from 'ng-mocks'; + +// A dummy service we are going to mock and to use for assertions. +@Injectable() +class TargetService { + protected called = false; + + public ctor() { + this.called = true; + } + + public onInit() { + this.called = true; + } + + public onDestroy() { + this.called = true; + } + + public onChanges() { + this.called = true; + } + + public afterViewInit() { + this.called = true; + } + + public afterViewChecked() { + this.called = true; + } + + public afterContentInit() { + this.called = true; + } + + public afterContentChecked() { + this.called = true; + } +} + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'target', + template: ``, +}) +class TargetComponent + implements OnInit, OnDestroy, OnChanges, AfterViewInit, AfterViewChecked, AfterContentInit, AfterContentChecked { + @Input() public input: string | null = null; + + protected readonly service: TargetService; + + constructor(service: TargetService) { + this.service = service; + this.service.ctor(); + } + + ngOnInit(): void { + this.service.onInit(); + } + + ngOnDestroy(): void { + this.service.onDestroy(); + } + + ngOnChanges(): void { + this.service.onChanges(); + } + + ngAfterViewInit(): void { + this.service.afterViewInit(); + } + + ngAfterViewChecked(): void { + this.service.afterViewChecked(); + } + + ngAfterContentInit(): void { + this.service.afterContentInit(); + } + + ngAfterContentChecked(): void { + this.service.afterContentChecked(); + } +} + +@NgModule({ + declarations: [TargetComponent], + exports: [TargetComponent], + providers: [TargetService], +}) +class TargetModule {} + +describe('TestLifecycleHooks', () => { + ngMocks.faster(); + + beforeEach(() => MockBuilder(TargetComponent, TargetModule)); + + it('triggers lifecycle hooks correctly via MockRender', () => { + // First let's suppress detectChanges. + const fixture = MockRender( + TargetComponent, + { + input: '', + }, + { detectChanges: false } + ); + + const service: TargetService = TestBed.get(TargetService); + + // By default nothing should be initialized, but ctor. + expect(service.ctor).toHaveBeenCalledTimes(1); // changed + expect(service.onInit).toHaveBeenCalledTimes(0); + expect(service.onDestroy).toHaveBeenCalledTimes(0); + expect(service.onChanges).toHaveBeenCalledTimes(0); + expect(service.afterViewInit).toHaveBeenCalledTimes(0); + expect(service.afterViewChecked).toHaveBeenCalledTimes(0); + expect(service.afterContentInit).toHaveBeenCalledTimes(0); + expect(service.afterContentChecked).toHaveBeenCalledTimes(0); + + // Now let's render the component. + fixture.detectChanges(); + + // This calls everything except onDestroy and onChanges. + expect(service.ctor).toHaveBeenCalledTimes(1); + expect(service.onInit).toHaveBeenCalledTimes(1); // changed + expect(service.onDestroy).toHaveBeenCalledTimes(0); + expect(service.onChanges).toHaveBeenCalledTimes(1); // changed + expect(service.afterViewInit).toHaveBeenCalledTimes(1); // changed + expect(service.afterViewChecked).toHaveBeenCalledTimes(1); // changed + expect(service.afterContentInit).toHaveBeenCalledTimes(1); // changed + expect(service.afterContentChecked).toHaveBeenCalledTimes(1); // changed + + // Let's change it. + fixture.componentInstance.input = 'change'; + fixture.detectChanges(); + + // Only OnChange, AfterViewChecked, AfterContentChecked should be triggered. + expect(service.ctor).toHaveBeenCalledTimes(1); + expect(service.onInit).toHaveBeenCalledTimes(1); + expect(service.onDestroy).toHaveBeenCalledTimes(0); + expect(service.onChanges).toHaveBeenCalledTimes(2); // changed + expect(service.afterViewInit).toHaveBeenCalledTimes(1); + expect(service.afterViewChecked).toHaveBeenCalledTimes(2); // changed + expect(service.afterContentInit).toHaveBeenCalledTimes(1); + expect(service.afterContentChecked).toHaveBeenCalledTimes(2); // changed + + // Let's cause more changes. + fixture.detectChanges(); + fixture.detectChanges(); + + // Only AfterViewChecked, AfterContentChecked should be triggered. + expect(service.ctor).toHaveBeenCalledTimes(1); + expect(service.onInit).toHaveBeenCalledTimes(1); + expect(service.onDestroy).toHaveBeenCalledTimes(0); + expect(service.onChanges).toHaveBeenCalledTimes(2); + expect(service.afterViewInit).toHaveBeenCalledTimes(1); + expect(service.afterViewChecked).toHaveBeenCalledTimes(4); // changed + expect(service.afterContentInit).toHaveBeenCalledTimes(1); + expect(service.afterContentChecked).toHaveBeenCalledTimes(4); // changed + + // Let's destroy it. + fixture.destroy(); + + // This all calls except onDestroy and onChanges. + expect(service.ctor).toHaveBeenCalledTimes(1); + expect(service.onInit).toHaveBeenCalledTimes(1); + expect(service.onDestroy).toHaveBeenCalledTimes(1); // changed + expect(service.onChanges).toHaveBeenCalledTimes(2); + expect(service.afterViewInit).toHaveBeenCalledTimes(1); + expect(service.afterViewChecked).toHaveBeenCalledTimes(4); + expect(service.afterContentInit).toHaveBeenCalledTimes(1); + expect(service.afterContentChecked).toHaveBeenCalledTimes(4); + }); + + it('does not trigger onChanges correctly via TestBed.createComponent', () => { + const fixture = TestBed.createComponent(TargetComponent); + fixture.componentInstance.input = ''; + + const service: TargetService = TestBed.get(TargetService); + + // By default nothing should be initialized. + expect(service.onChanges).toHaveBeenCalledTimes(0); + + // Now let's render the component. + fixture.detectChanges(); + + // The hook should have been called, but not via TestBed.createComponent. + expect(service.onChanges).toHaveBeenCalledTimes(0); // failed + + // Let's change it. + fixture.componentInstance.input = 'change'; + fixture.changeDetectorRef.detectChanges(); + + // The hook should have been called, but not via TestBed.createComponent. + expect(service.onChanges).toHaveBeenCalledTimes(0); // failed + }); +}); diff --git a/package.json b/package.json index 523307f4d5..59d26aa393 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ng-mocks", "version": "10.5.1", - "description": "A library mocking angular components, directives, pipes, services, providers and modules in unit tests, which also includes shallow rendering and flexible stubs.", + "description": "A library mocking angular components, directives, pipes, services, providers and modules in unit tests, which also includes shallow rendering and precise stubs to dump child dependencies.", "main": "dist/index.js", "types": "dist/index.d.ts", "files": [ @@ -78,26 +78,21 @@ "url": "github.com:ike18t/ng-mocks.git" }, "keywords": [ - "angular", - "component", - "directive", - "dumb", - "dummy", - "mock", - "mocking", - "module", - "pipe", - "provider", - "render", - "rendering", - "service", - "shallow", - "stub", + "angular unit tests", + "angular testing", + "angular test", + "mocking TestBed how to", + "mock component", + "mock directive", + "mock pipe", + "mock provider", + "mock service", + "shallow render", + "shallow rendering", + "stub child dependency", "stubbing", - "test", - "testbed", - "testing", - "unit" + "dumb", + "dummy" ], "author": { "name": "Isaac Datlof",