How to mock @Select in ngxs when using a mock store
Asked Answered
H

4

11

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?

Horsepower answered 28/6, 2018 at 11:39 Comment(0)
K
16

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.

Komi answered 2/4, 2019 at 11:57 Comment(5)
What if you have multiple store.selectSnapshot or store.select using the memoized selectors? Do you know if this can be achieved?Carnal
I think you can use .and.callFake() and return values based on your callback. That should do the trick.Komi
thanks for that. I end up finding an alternative. Since I'm using jest, I had to use the .mockImplementation(), which receives a function. However, I'm using a facade pattern that allows me to nest selectors, which then makes the implementation of the mock quite extensive. The alternative was to use store.reset({ ... }), which allows me to pass a single object that mocks the state I needed. Maybe this can help the original question :)Carnal
store.reset() should work as well, of course! We do use that for most of our test cases currently - but it does not prevent the original State Object from being instantiated accidentally when using memoized selectors (which is why I did not reccomend that). It is still absolutely valid if it works for you! :)Komi
@Carnal would you mind creating an answer for this with an example? I am having difficulty implementing your suggestion.Matriculation
M
4

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();  
});
Matriculation answered 1/12, 2022 at 10:33 Comment(0)
C
3

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.

Carnal answered 21/12, 2022 at 23:53 Comment(0)
B
-4

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])],
});
Bronco answered 28/6, 2018 at 13:58 Comment(1)
Thank you for your suggestions, but as I said in my last paragraphs, this is not what I want, because then I need to start mocking stuff within the state classes, I want to test the component in complete isolation.Horsepower

© 2022 - 2024 — McMap. All rights reserved.