Ivar Reukers
Ivar Reukers

Reputation: 7719

Angular 2 (Mock Ionic2) -- No provider for App

I'm trying to create a spec.ts for my application. Sadly this is using the LoadingController from ionic-angular. Now when I'm trying to configure the module, I will need to provide it with the LoadingController (since it's in the constructor of the module).

The problem I'm currently running into is that the LoadingController wants to be provided with a App object/instance. (_app: App params)

I was desperate so I asked Ionic themselves. github #8539

But they closed my question because it was a question and not an issue, although I'm having issues realizing it which they haven't responded to. It would be a shame if this is impossible/no one knows how, since it's a pretty cool feature and it affects not just the LoadingController, f.e. the AlertController and ToastController are affected by this as well.

My testbed configuration atm:

TestBed.configureTestingModule({
    declarations: [EventsPage],
    schemas: [CUSTOM_ELEMENTS_SCHEMA],
    providers: [
      {provide: APICaller, useValue: mockAPICaller},
      {provide: NavController, useValue: mockNavController },
      {provide: LoadingController, useValue: ???????}, //it seems like everything I try to enter here fails.
    ],
    imports: [FormsModule]
  });

And the EventsPage constructor:

constructor(public apiCaller: APICaller, public navCtrl: NavController,
             public loadingCtrl: LoadingController){}

EDIT: usage of LoadingController

getEvents() {
    // setup a loadingscreen
     let loading = this.loadingCtrl.create({
      content: "Loading..."
    }); 
   // present the loadingscreen
    loading.present();

    // reset the noEvents boolean.
    this.noEvents = true;

    // call the api and get all events
    this.apiCaller.getEvents()
    .subscribe(response => {
      // response is list of events
      this.events = response;
      // if the event is not empty, set noEvents to false.
      if (this.events.length > 0) {
        this.noEvents = false;
      }
      // close the loading message.
     loading.dismiss();
    });
  }

Which will then result in this loadingspinner (with different text)

ionic's Loading

Upvotes: 3

Views: 1134

Answers (1)

Paul Samsotha
Paul Samsotha

Reputation: 208964

With this type of thing, you probably don't want to test anything in the UI (in regards to using the LoadingController). What you should be testing is the behavior of the component. So when you create the mock for the LoadingController what you want to do is spy on the critical methods, and your expectations should test to make sure that you are calling the methods on the LoadingController. Doing this you can write tests like

expect(loadingController.someMethod).toHaveBeenCalled();
// or
expect(loadingController.someMethod).toHaveBeenCalledWith(args);

Your mocks don't have to follow the actual structure of the item being mocked either. For instance LoadingController.create returns a Loading object. In your mock, you don't need to have this. If you want you can just return the mock itself, when calling create, and in the mock just have the methods that Loading would have.

Remember that you are just testing the behavior of the controller. It doesn't matter what the mock LoadingController actually does, just that you are able to called the methods, and check in the test to make sure they are called when they are expected to be called. Other than that, you should just assume the real LoadingController works.

So you could have something like

let mockLoadingController = {
  // Tried `create: jasmine.createSpy('create').and.returnValue(this)
  // seem this doesn't work. We just create the spy later
  create: (args: any) => { return this; },
  present: jasmine.createSpy('present'),
  dismiss: jasmine.createSpy('dismiss')
};
spyOn(mockLoadingController, 'create').and.returnValue(mockLoadingController);

{ provide: LoadingController, useValue: mockLoadingController }

Then in you tests you can just do something like

it('should create loader with args ...', () => {
  ...
  expect(mockLoadingController.create).toHaveBeenCalledWith({...})
})
it('should present the loader', () => {
  ...
  expect(mockLoadingController.present).toHaveBeenCalled();
})
it('should dismiss the loader when the api call returns', async(() => {
  ..
  expect(mockLoadingController.dismiss).toHaveBeenCalled();
}))

Here's what I used right now to test

class LoadingController {
  create(args: any) { return this; }
  present() {}
  dismiss() {}
}

@Component({
  template: ''
})
class TestComponent {
  constructor(private loadingController: LoadingController) {}

  setEvents() {
    let loading = this.loadingController.create({hello: 'world'});

    loading.present();
    loading.dismiss();
  }
}

describe('component: TestComponent', () => {
  let mockLoadingController;

  beforeEach(async(() => {
    mockLoadingController = {
      create: (args: any) => { return this; },
      present: jasmine.createSpy('present'),
      dismiss: jasmine.createSpy('dismiss')
    };
    spyOn(mockLoadingController, 'create').and.returnValue(mockLoadingController);

    TestBed.configureTestingModule({
      declarations: [TestComponent],
      providers: [
        { provide: LoadingController, useValue: mockLoadingController }
      ]
    });
  }));

  it('should calling loading controller', () => {
    let comp = TestBed.createComponent(TestComponent).componentInstance;
    comp.setEvents();

    expect(mockLoadingController.create).toHaveBeenCalledWith({ hello: 'world'});
    expect(mockLoadingController.present).toHaveBeenCalled();
    expect(mockLoadingController.dismiss).toHaveBeenCalled();
  });
});

See Also:

Upvotes: 6

Related Questions