Wouter
Wouter

Reputation: 4016

How to mock @Select in ngxs when using a mock store

I am using ngxs for state handling in angular, and I am trying to test our components as units, so preferably only with mock stores, states etc.

What we have in our component is something like:

export class SelectPlatformComponent {

  @Select(PlatformListState) platformList$: Observable<PlatformListStateModel>;

  constructor(private store: Store, private fb: FormBuilder) {
    this.createForm();
    this.selectPlatform();
  }

  createForm() {
    this.selectPlatformForm = this.fb.group({
      platform: null,
    });
  }

  selectPlatform() {
    const platformControl = this.selectPlatformForm.get('platform');
    platformControl.valueChanges.forEach(
      (value: Platform) => {
        console.log("select platform " + value);
        this.store.dispatch(new PlatformSelected(value));
      }
    );
  }

}

And our fixture setup looks like this, so we can check calls on the store:

describe('SelectPlatformComponent', () => {
  let component: SelectPlatformComponent;
  let fixture: ComponentFixture<SelectPlatformComponent>;
  let store: Store;

  beforeEach(async(() => {
    const storeSpy = jasmine.createSpyObj('Store', ['dispatch']);
    TestBed.configureTestingModule({
      imports: [ReactiveFormsModule],
      declarations: [SelectPlatformComponent],
      providers: [{provide: Store, useValue: storeSpy}]

    })
      .compileComponents();
    store = TestBed.get(Store);
  }));

But when we run this, we get the following error:

Error: SelectFactory not connected to store!
    at SelectPlatformComponent.createSelect (webpack:///./node_modules/@ngxs/store/fesm5/ngxs-store.js?:1123:23)
    at SelectPlatformComponent.get [as platformList$] (webpack:///./node_modules/@ngxs/store/fesm5/ngxs-store.js?:1150:89)
    at Object.eval [as updateDirectives] (ng:///DynamicTestModule/SelectPlatformComponent.ngfactory.js:78:87)
    at Object.debugUpdateDirectives [as updateDirectives] (webpack:///./node_modules/@angular/core/fesm5/core.js?:11028:21)
    at checkAndUpdateView (webpack:///./node_modules/@angular/core/fesm5/core.js?:10425:14)
    at callViewAction (webpack:///./node_modules/@angular/core/fesm5/core.js?:10666:21)
    at execComponentViewsAction (webpack:///./node_modules/@angular/core/fesm5/core.js?:10608:13)
    at checkAndUpdateView (webpack:///./node_modules/@angular/core/fesm5/core.js?:10431:5)
    at callWithDebugContext (webpack:///./node_modules/@angular/core/fesm5/core.js?:11318:25)
    at Object.debugCheckAndUpdateView [as checkAndUpdateView] (webpack:///./node_modules/@angular/core/fesm5/core.js?:10996:12)

I could enable the entire ngxs module for this, but then I would need to create services mocks to inject into state objects, which I do not like because I am then not testing the component in isolation anymore. I tried to create a mock SelectFactory, but it seems it is not exported from the module.

Is there a way to mock the SelectFactory, or inject some mocks into the platformList$ directly? Other suggestions?

Upvotes: 11

Views: 10637

Answers (4)

n4nd0_o
n4nd0_o

Reputation: 2998

I'm adding an answer here as requested by @Remi. Since we are using a facade pattern, for whatever reason trying to mock the store methods doesn't work. During test runtime it looses the mock context and end up going to the actual state and tbh, haven't found the actual cause of the issue.

However, what our team end up learning is that instead of mocking the store.dispatch or store.select, you can instead mock the entire state. Please do have in mind that the purpose stated is to test the component and not the state actions and what it does under the hook, it's to test how the component will behave based on the data flowing through it.

Having said that, here's a sample of what we are doing:

// the mock state name should have the same name as the one actually used
@State({ name: 'quote', defaults: {} }) 
@Injectable()
export class MockQuoteState {}
beforeEach(async(() => {
   TestBed.configureTestingModule({
      declarations: [MyComponent]
      imports: [NgxsModule.forRoot([MockQuoteState])] // import your mocks here
   });

   const store:Store = TestBed.get(Store);
   store.reset({
     // populate the object with whatever values you need to test your component or instead of the reset, you can define that as the default value on the state declaration. 
     // We currently use a combination of both 
   })
}));

NOTE: Depending on the complexity of your project, the state can be quite a huge object graph (like ours) so we started to abstract a lot of common state scenarios into re-usable pieces so component testing becomes a bit easier to compose and to read.

Upvotes: 3

Remi
Remi

Reputation: 5367

You could also override the property in the component itself rather than using the selector:

beforeEach(() => {  
  fixture = TestBed.createComponent(SelectPlatformComponent);
  component = fixture.componentInstance;

  // overrides the property
  Object.defineProperty(component, 'platformList$', { writable: true });
  component.platformList$ = of('value');

  fixture.detectChanges();  
});

Upvotes: 4

pascalpuetz
pascalpuetz

Reputation: 5428

I stumbled upon the same problem and I found it's not possible with Angular's DI Mechanism alone. Though, it is possible by getting the actual instance created and then mock it like this:

beforeEach(async(() => {
   TestBed.configureTestingModule({
      declarations: [MyComponent]
      imports: [NgxsModule.forRoot([])] // import real module without state
   });

   const store:Store = TestBed.get(Store);
   spyOn(store, 'select').and.returnValue(of(null)); // be sure to mock the implementation here
   spyOn(store, 'selectSnapshot').and.returnValue(null); // same here
}));

If you are using memoized selectors (e.g. @Select(MyState.selector)) inside your component, be sure to ALWAYS mock the store's select function. If you don't, NGXS will try to instantiate the class MyState regardless of it being not provided to the NgxsModule.forRoot([]). This is not a problem in a lot of cases but as soon as you have dependencies inside the constructor of MyState (Angular DI dependencies) you would also need to provide those to the providers array.

Upvotes: 17

Tiisetso Khalane
Tiisetso Khalane

Reputation: 1

I had the same problem and solved it by removing the Store provider from the providers array and also configured:

TestBed.configureTestingModule({
        imports: [NgxsModule.forRoot([MyState])],
});

Upvotes: -4

Related Questions