ionic-team / ionic-framework

A powerful cross-platform UI toolkit for building native-quality iOS, Android, and Progressive Web Apps with HTML, CSS, and JavaScript.
https://ionicframework.com
MIT License
51.05k stars 13.51k forks source link

Unable to use ViewController in unit testing with or without mock use #9411

Closed mjosephd closed 6 years ago

mjosephd commented 7 years ago

Ionic version: (check one with "x")

[ ] 1.x [X] 2.x

I'm submitting a ... (check one with "x")

[X] bug report [ ] feature request [ ] support request => Please do not submit support requests here, use one of these channels: https://forum.ionicframework.com/ or http://ionicworldwide.herokuapp.com/

Current behavior:

Injecting and mocking out ViewController for unit tests causes two separate issues.

1) The first error being Failed: Can't resolve all parameters for ViewController: (?, ?, ?). for when there is no mocking out of the ViewController.

2) When creating a jasmine spy or mocking out the ViewController TypeError: viewCtrl._setHeader is not a function.

Expected behavior:

NavController seems to work with and without mocking or using jasmine.

So by the same means I would assume ViewController should work for both scenarios.

Steps to reproduce:

1) Create a sample Unit test, there is no need to even USE ViewController anywhere in the components that are being unit tested. 2) Simply Inject ViewController to the providers array in the TestBed.configureTestingModule function, this would cause the first error mentioned 3) Use a mock or jasmine double like the following for the second error { provide: ViewController, useClass: class { ViewController = jasmine.createSpy("viewController"); } },

Related code:

The below is a sample unit test which uses { provide: ViewController, useClass: class { ViewController = jasmine.createSpy("viewController"); } },

import { ComponentFixture, async } from '@angular/core/testing';
import { TestUtils }               from '../../test';
import {} from 'jasmine';

import { SearchModal } from './SearchModal';
import { SearchService } from '../../services/SearchService';
import { PouchDbService } from '../../services/common/PouchDbService';

import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { TestBed } from '@angular/core/testing';
import { App, MenuController, NavController, Platform, Config, Keyboard, Form, IonicModule, ViewController, GestureController, NavParams }  from 'ionic-angular';
import { ConfigMock } from '../../mocks';
import { TranslateModule } from 'ng2-translate';
import { LoadingController } from 'ionic-angular';

let fixture: ComponentFixture<SearchModal> = null;
let instance: any = null;

describe('LocationSearchModal', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        SearchModal
      ],
      providers: [
        App, Platform, Form, Keyboard, MenuController, NavController, GestureController, SearchService, LoadingController,
        { provide: ViewController, useClass: class { ViewController = jasmine.createSpy("viewController"); } },
        { provide: PouchDbService, useClass: class { PouchDbService = jasmine.createSpy("pouchDbService"); } },
        {provide: Config, useClass: ConfigMock}
      ],
      imports: [
        FormsModule,
        IonicModule,
        ReactiveFormsModule,
        TranslateModule.forRoot(),
      ],
    })
    .compileComponents()
    .then(() => {
      fixture = TestBed.createComponent(SearchModal);
      instance = fixture.debugElement.componentInstance;
      fixture.autoDetectChanges(true);
    });
  }));

  afterEach(() => {
    fixture.destroy();
  });

  it('loads', () => {
    expect(fixture).not.toBeNull();
    expect(instance).not.toBeNull();
  })
})

Other information:

Saw a similar issue mentioned here https://github.com/driftyco/ionic/issues/9331

The exception mentioned in that issue is similar to the first exception mentioned here.

Error: Can't resolve all parameters for NavParams: (?).

However unlike ViewController when I try to create a jasmine spy for NavParams I do not get any errors like the one I am getting when I create a jasmine spy for ViewController.

Plain Injection DOES NOT WORK : providers: [...,NavParams,...] DOES NOT WORK :providers: [...,ViewController,...]

Using Jasmine Spies in providers: [....] array WORKS : { provide: NavParams, useClass: class { NavParams = jasmine.createSpy("navParams"); } }, DOES NOT WORK : { provide: ViewController, useClass: class { ViewController = jasmine.createSpy("viewController"); } },

Referenced the helpful https://github.com/lathonez/clicker repository as a base for setting up unit tests in our project. Additionally before posting the issue here also cloned the clicker repository and tried adding ViewController simply to the providers array in the beforeEach section and got the same issues mentioned.

The function that is said to be not a function in the second error https://github.com/driftyco/ionic/blob/6b3e2ed447340cdd35c328c96aa7cfa5f34eb214/src/navigation/view-controller.ts#L364

Error 1 Log

✖ should create page2
      Chrome 54.0.2840 (Mac OS X 10.9.5)
    Failed: Can't resolve all parameters for ViewController: (?, ?, ?).
    Error: Can't resolve all parameters for ViewController: (?, ?, ?).
        at CompileMetadataResolver.getDependenciesMetadata (http://localhost:9876/_karma_webpack_/0.bundle.js:9046:19)
        at CompileMetadataResolver.getTypeMetadata (http://localhost:9876/_karma_webpack_/0.bundle.js:8947:26)
        at http://localhost:9876/_karma_webpack_/0.bundle.js:9090:41
        at Array.forEach (native)
        at CompileMetadataResolver.getProvidersMetadata (http://localhost:9876/_karma_webpack_/0.bundle.js:9070:19)
        at CompileMetadataResolver.getNgModuleMetadata (http://localhost:9876/_karma_webpack_/0.bundle.js:8829:58)
        at RuntimeCompiler._compileComponents (http://localhost:9876/_karma_webpack_/0.bundle.js:14272:47)
        at RuntimeCompiler._compileModuleAndAllComponents (http://localhost:9876/_karma_webpack_/0.bundle.js:14216:37)
        at RuntimeCompiler.compileModuleAndAllComponentsAsync (http://localhost:9876/_karma_webpack_/0.bundle.js:14207:21)
        at TestingCompilerImpl.compileModuleAndAllComponentsAsync (http://localhost:9876/_karma_webpack_/0.bundle.js:16878:35)

Error 2 Log

TypeError: viewCtrl._setHeader is not a function
        at new Header (webpack:///Users/mjosephd/ionic/testing/clicker/~/ionic-angular/components/toolbar/toolbar.js:14:0 <- src/test.ts:10604:30)
        at new Wrapper_Header (/IonicModule/Header/wrapper.ngfactory.js:7:18)
        at _View_Page20.createInternal (/DynamicTestModule/Page2/component.ngfactory.js:42:22)
        at _View_Page20.AppView.create (webpack:///Users/mjosephd/ionic/testing/clicker/~/@angular/core/src/linker/view.js:84:0 <- src/test.ts:46864:21)
        at _View_Page20.DebugAppView.create (webpack:///Users/mjosephd/ionic/testing/clicker/~/@angular/core/src/linker/view.js:294:0 <- src/test.ts:47074:44)
        at _View_Page2_Host0.createInternal (/DynamicTestModule/Page2/host.ngfactory.js:18:14)
        at _View_Page2_Host0.AppView.create (webpack:///Users/mjosephd/ionic/testing/clicker/~/@angular/core/src/linker/view.js:84:0 <- src/test.ts:46864:21)
        at _View_Page2_Host0.DebugAppView.create (webpack:///Users/mjosephd/ionic/testing/clicker/~/@angular/core/src/linker/view.js:294:0 <- src/test.ts:47074:44)
        at ComponentFactory.create (webpack:///Users/mjosephd/ionic/testing/clicker/~/@angular/core/src/linker/component_factory.js:152:0 <- src/test.ts:29983:36)
        at initComponent (webpack:///Users/mjosephd/ionic/testing/clicker/~/@angular/core/bundles/core-testing.umd.js:855:0 <- src/test.ts:5334:53)
    Error: Uncaught (in promise): Error: Error in ./Page2 class Page2 - inline template:0:0 caused by: viewCtrl._setHeader is not a function
        at resolvePromise (webpack:///Users/mjosephd/ionic/testing/clicker/~/zone.js/dist/zone.js:468:0 <- src/test.ts:65560:31)
        at resolvePromise (webpack:///Users/mjosephd/ionic/testing/clicker/~/zone.js/dist/zone.js:453:0 <- src/test.ts:65545:17)
        at webpack:///Users/mjosephd/ionic/testing/clicker/~/zone.js/dist/zone.js:502:0 <- src/test.ts:65594:17
        at ZoneDelegate.invokeTask (webpack:///Users/mjosephd/ionic/testing/clicker/~/zone.js/dist/zone.js:265:0 <- src/test.ts:65357:35)
        at ProxyZoneSpec.onInvokeTask (webpack:///Users/mjosephd/ionic/testing/clicker/~/zone.js/dist/proxy.js:103:0 <- src/test.ts:26851:39)
        at ZoneDelegate.invokeTask (webpack:///Users/mjosephd/ionic/testing/clicker/~/zone.js/dist/zone.js:264:0 <- src/test.ts:65356:40)
        at Zone.runTask (webpack:///Users/mjosephd/ionic/testing/clicker/~/zone.js/dist/zone.js:154:0 <- src/test.ts:65246:47)
        at drainMicroTaskQueue (webpack:///Users/mjosephd/ionic/testing/clicker/~/zone.js/dist/zone.js:401:0 <- src/test.ts:65493:35)
        at ZoneTask.invoke (webpack:///Users/mjosephd/ionic/testing/clicker/~/zone.js/dist/zone.js:339:0 <- src/test.ts:65431:25)
        at data.args.(anonymous function) (webpack:///Users/mjosephd/ionic/testing/clicker/~/zone.js/dist/zone.js:970:0 <- src/test.ts:66062:25)

Ionic info:


Cordova CLI: 5.0.0
Ionic CLI Version: 2.1.12
Ionic App Lib Version: 2.1.7
ios-deploy version: Not installed
ios-sim version: Not installed
OS: OS X Mavericks
Node Version: v6.9.1
Xcode version: Not installed```
MarkySparky commented 7 years ago

Exactly the same problem right now also. ModalController mocks fine, would have thought it would be similar for ViewController

MarkySparky commented 7 years ago

Also, someone had this issue (unresolved) yesterday over at http://stackoverflow.com/questions/40871317/ionic-2-viewcontroller-unit-testing

MarkySparky commented 7 years ago

Hey, got this figured out. Heres how I got it working:

Create a mock object like this (I keep my all mocks in a mocks.ts file)

export class ViewControllerMock { public _setHeader(): any { return {} }; public _setIONContent(): any { return {} }; public _setIONContentRef(): any { return {} }; }

then provide the mock in the configureTestingModule providers array

{ provide: ViewController, useClass: ViewControllerMock },

Works a charm for me, tests all running fine now.

mjosephd commented 7 years ago

@MarkySparky The stack overflow question would actually be me as well. I'll try out your mock sample.

Though I am not sure if the issue should be closed as it does not work with Jasmine? Correct me if I am wrong but Jasmine's spies are essentially test doubles as well. This is an excerpt from their site. https://jasmine.github.io/2.0/introduction.html#section-Spies "Jasmine has test double functions called spies. A spy can stub any function and tracks calls to it and all arguments.

MarkySparky commented 7 years ago

@mjosephd Yup, I get you. I'd like the option to use jasmine also, I was just wanting it working initially, then i'll maybe refactor once I have more unit tests in place to push out all the possible scenarios that arise with ionic 2 stubbing / mocking. I still use spies in the actual tests to check if the functions are called, so you are right, it would make sense to use it in the mock also. I'll maybe give it a go tomorrow with spies instead of a stubbed mock object.

Ritzlgrmft commented 7 years ago

I solved the problem by just using a ViewController instance:

const viewControllerStub = new ViewController();
...
{ provide: ViewController, useValue: viewControllerStub }

And for the functions I was interested in, I used a spy:

spyOn(viewControllerStub, "dismiss");
kamok commented 7 years ago

@Ritzlgrmft This is interesting. It gets rid of the problem of me mocking out stuff like: ViewController.readReady() and ViewController._setIONcontent().

However, I'm currently doing a set up where I mock viewController.viewEnter inside a "fake" ViewControllerMock like how @MarkySparky has it. Doing useValue: viewControllerStub replaces my ability to do that.

The problem I'm having with @MarkySparky's set up is, with the new release of Ionic 2.0 FINAL, this part is unmockable:

if (viewCtrl) {
            // content has a view controller
            viewCtrl._setIONContent(this);
            viewCtrl._setIONContentRef(elementRef);
            this._viewCtrlReadSub = viewCtrl.readReady.subscribe(function () {
                _this._viewCtrlReadSub.unsubscribe();
                _this._readDimensions();
            });
            this._viewCtrlWriteSub = viewCtrl.writeReady.subscribe(function () {
                _this._viewCtrlWriteSub.unsubscribe();
                _this._writeDimensions();
            });

The this._viewCtrlReadSub part requires a new Observable mock, but even after putting that into my ViewControllerMock, it gives me a "cant unsubscribe _this._viewCtrlReadSub"`. More specifically, "Cannot read property 'unsubscribe' of undefined".

I'm gonna result to your viewControllerStub technique, but I'll have to find a new way to mock places where I call viewCtrl.willEnter.

ghost commented 7 years ago

Right now I'm using a mock class for viewController as below, and it takes care of the "readReady" issue as well:

export class ViewControllerMock {

    public readReady: any = {
        emit(): void {

        },
        subscribe(): any {

        }
    };

    public writeReady: any = {
        emit(): void {

        },
        subscribe(): any {

        }
    };

    public contentRef(): any {
        return new Promise(function (resolve: Function): void {
            resolve();
        });
    }

    public didEnter(): any {
        return new Promise(function (resolve: Function): void {
            resolve();
        });
    }

    public didLeave(): any {
        return new Promise(function (resolve: Function): void {
            resolve();
        });
    }

    public onDidDismiss(): any {
        return new Promise(function (resolve: Function): void {
            resolve();
        });
    }

    public onWillDismiss(): any {
        return new Promise(function (resolve: Function): void {
            resolve();
        });
    }

    public willEnter(): any {
        return new Promise(function (resolve: Function): void {
            resolve();
        });
    }

    public willLeave(): any {
        return new Promise(function (resolve: Function): void {
            resolve();
        });
    }

    public willUnload(): any {
        return new Promise(function (resolve: Function): void {
            resolve();
        });
    }

    public dismiss(): any {
        return true;
    }

    public enableBack(): any {
        return true;
    }

    public getContent(): any {
        return true;
    }

    public hasNavbar(): any {
        return true;
    }

    public index(): any {
        return true;
    }

    public isFirst(): any {
        return true;
    }

    public isLast(): any {
        return true;
    }

    public pageRef(): any {
        return true;
    }

    public setBackButtonText(): any {
        return true;
    }

    public showBackButton(): any {
        return true;
    }

    public _setHeader(): any {
        return true;
    }

    public _setIONContent(): any {
        return true;
    }

    public _setIONContentRef(): any {
        return true;
    }

    public _setNavbar(): any {
        return true;
    }

    public _setContent(): any {
        return true;
    }

    public _setContentRef(): any {
        return true;
    }

    public _setFooter(): any {
        return true;
    }

}

And then I add it in the TestBed module "providers":

{provide: ViewController, useClass: ViewControllerMock},

Now, this is a very simplistic but comprehensive mock. One may need to still add a method or modify it, but it will get rid of that "readyReady" and "writeReady" errors for sure.

beenotung commented 7 years ago

I'm using ionic-angular@3.6.1, there are built-in mock generators.

Source Code:

import {Component, Inject, Input} from "@angular/core";
import {ViewController} from "ionic-angular";

@Component({
  selector: "modal-header",
  templateUrl: "./modal-header.component.html",
  styleUrls: ["./modal-header.component.scss"]
})
export class ModalHeaderComponent {

  @Input()
  title: string;

  constructor(public viewCtrl: ViewController) {
  }

  dismiss() {
    return this.viewCtrl.dismiss();
  }
}

Test Code:

import {async, ComponentFixture, TestBed} from "@angular/core/testing";

import {ModalHeaderComponent} from "./modal-header.component";
import {App, Config, IonicModule, Platform, ViewController} from "ionic-angular";
import {mockApp, mockConfig, mockPlatform, mockView} from "ionic-angular/util/mock-providers";

describe("ModalHeaderComponent", () => {
  let component: ModalHeaderComponent;
  let fixture: ComponentFixture<ModalHeaderComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [IonicModule],
      providers: [
        {provide: ViewController, useValue: mockView()},
        {provide: Config, useValue: mockConfig()},
        {provide: Platform, useValue: mockPlatform()},
        {provide: App, useValue: mockApp()},
      ],
      declarations: [ModalHeaderComponent]
    }).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(ModalHeaderComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it("should be created", () => {
    expect(component).toBeTruthy();
  });
});
mfdeveloper commented 7 years ago

@beenotung i'm using jest, and I tried use the mock*() functions of ionic like you said, but an error happens with import { NgZone } from '@angular/core' of mock-providers.ts

I think that jest add patches into Zone, and problems like this happens when import him explicitly by another file instead the own angular lifecycle.

So, I'm using the mock class approach suggested by @Ritzlgrmft and @Maziar-Fotouhi and it's working fine!! :smile:

ionitron-bot[bot] commented 6 years ago

Thanks for the issue! This issue is being closed due to inactivity. If this is still an issue with the latest version of Ionic, please create a new issue and ensure the template is fully filled out.

Thank you for using Ionic!