Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🚀[FEATURE]: How to mock @Select in ngxs when using a mock store #482

Open
wouterv opened this issue Jul 6, 2018 · 33 comments
Open

🚀[FEATURE]: How to mock @Select in ngxs when using a mock store #482

wouterv opened this issue Jul 6, 2018 · 33 comments

Comments

@wouterv
Copy link

wouterv commented Jul 6, 2018

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?

PS: I also asked this on stackoverflow, but no real answer was given: https://stackoverflow.com/questions/51082002/how-to-mock-select-in-ngxs-when-using-a-mock-store

@amcdnl
Copy link
Member

amcdnl commented Jul 15, 2018

Sorry but you HAVE to inject ngxs module for this to work :(

@amcdnl amcdnl closed this as completed Jul 15, 2018
@markwhitfeld
Copy link
Member

@amcdnl We could look at making a testability Helper for this. It's one of the items on my list.

@wouterv
Copy link
Author

wouterv commented Jul 24, 2018

@amcdnl @markwhitfeld I can see it cannot be done now, but I would think it would be a good addition to make the framework a bit more open for mocking, so that we dont have to mock all our services that are used withing the states. Can this maybe be re-opened and converted to a feature request?

@BradleyHill
Copy link

Love the simplicity & clarity of ngxs vs redux. However, totally agree should not have to inject real dependencies to test components. Haven't dug into source code but was able to 'hack' around it by redefining the property in the test.
Object.defineProperty(component, 'selectPropertyName', { writable: true });

Where 'component' is the component under test and 'selectPropertyName' is the name of the property decorated with '@select'.

Once redefined, can simply provide observable you control in test flow:
component.selectPropertyName = of(myData);

Not the ideal solution but feels like spirit of what i'm testing remains unsullied.

@wouterv
Copy link
Author

wouterv commented Aug 2, 2018

@BradleyHill thanks! you saved my day

@regexer-github
Copy link

@BradleyHill, @wouterv where exactly in the testing flow do you redefine your component properties?
Does not seem to work inside my beforeEach since the error occurs inside the createComponent method :/

beforeEach(() => {  
       fixture = TestBed.createComponent(RequestStartAllowanceComponent);
       component = fixture.componentInstance;  
       Object.defineProperty(component, 'prop$', { writable: true });  
       component.prop$ = of('value');  
       fixture.detectChanges();  
  });

@wouterv
Copy link
Author

wouterv commented Aug 17, 2018

@regexer-github Your code is exactly how we test it and it works for us. Maybe you can share your error?

@BradleyHill
Copy link

As @wouterv says, we have same thing save the override is in the test and not the beforeEach but that should not matter. Error would help. Maybe it's misdirection to something else.

The only other thing to note is if you are binding to the @Select observable in the html with async pipe, you are good to go. However, if you are doing something more intricate in the code behind with a subscription, you need to re-establish the subscription as the subscription typically occurs in the constructor or the ngOnInit. By the time you access the component from the TestBed, the subscription has been established with the original observable. When you set the observable in your test, there is no subscription to new observable. Although it violates the 'do not change code to accommodate tests' mantra, you can easily remedy this by moving the subscription to a method that the constructor or ngOnInit invokes, You can then just manually invoke same method after setting your new observable to establish the subscription.

Example:

constructor() {
  this.subscribeToProp();
}

subscribeToProp() {
  prop$.subscribe(value =>{ /* whatever crazy antics you enjoy */ });
}

// Then in test land
beforeEach(() => {  
       fixture = TestBed.createComponent(RequestStartAllowanceComponent);
       component = fixture.componentInstance;  
       Object.defineProperty(component, 'prop$', { writable: true });  
       component.prop$ = of('value');
       component.subscribeToProp();
       fixture.detectChanges();  
  });

(Authoring code inline so possible errors) Ideally @Selectable more mock friendly (haven't dug into source yet) but this is easy work-around and, IMO, doesn't sully the integrity of the test. Hope this helps.

@regexer-github
Copy link

Well, I somehow cannot get this to fail anymore... I assume ng test did recompile correctly :/
Thank you for the advice anyway.
The idea of moving the initialization of subscriptions to a separate method will definitely come in handy.

@beyondsanity
Copy link

beyondsanity commented Sep 20, 2018

I'm in a similar situation but the @Select is inside a State. Is it possible to use a similar approach starting from a State TestBed?

 TestBed.configureTestingModule({
        providers: [
         ...
        ],
        imports: [
            NgxsModule.forRoot([MyAwesomeState])
        ]
    }).compileComponents();

@beyondsanity
Copy link

beyondsanity commented Sep 20, 2018

Sorry, I just found a solution:

state = TestBed.get(MyAwesomeState);
Object.defineProperty(state, 'prop$', { writable: true });
state.prop$ = of('value);

@gforceg
Copy link

gforceg commented Jan 22, 2019

How would you do this when your component template references another component that contains a public @Select() Observable?

Would you just mock the component?

@gforceg
Copy link

gforceg commented Jan 23, 2019

It feels like I'm trading ngrx boiler plate for Angular services that sit between my components and ngxs in order to make my code testable.

@markwhitfeld
Copy link
Member

Hi everyone. We would like to add some test helpers for ngxs soon to make testing easier. Would any of you like to assist us in this effort?
Discussion, ideas or code welcome!

@KurtGokhan
Copy link

I found this way makes it easy to write unit tests for components without calling actions.

Firstly I write a plugin to ignore actions and store them in an array to later assert if that action was called:

import { Inject, Injectable, InjectionToken } from '@angular/core';
import { NgxsPlugin } from '@ngxs/store';
import { of } from 'rxjs';

export const NGXS_ACTIONS = new InjectionToken('NGXS_ACTIONS');

@Injectable()
export class NgxsTestPlugin implements NgxsPlugin {
  constructor(@Inject(NGXS_ACTIONS) private actions: any[]) { }

  handle(state, action, next) {
    console.log('Action called in test', action);
    this.actions.push(action);
    return of(state);
  }
}

Then I use it in my testing module:

  ...
  providers: [
    {
      provide: NGXS_PLUGINS,
      useClass: NgxsTestPlugin,
      multi: true
    },
    {
      provide: NGXS_ACTIONS,
      useValue: [],
    }
  ],
  ...

Now I can assert if actions were called in my tests without causing any side effects:

    const actions = TestBed.get(NGXS_ACTIONS);
    expect(getActionTypeFromInstance(actions[1])).toEqual('Logout');

There is no need to write mock for Selectors this way and we can use store.reset to skip ahead in unit tests.

@BradleyHill
Copy link

We have espoused the philosophy that components are dumb as possible. They render values from state and communicate user actions to state via actions. That's it. Logic in components is bad.

We write tests against rendering by seeding different states to our bound @Select variables via the hack above (the only ngxs work-around we've needed). Make property writable, set it to observable of desired value, fixture detect changes, assert whatever you expect to be rendered.

To test the user interaction, we simply spy on store dispatch in setup:
storeSpy = spyOn(TestBed.get(Store), 'dispatch').and.returnValue(of(true));
and then asset on combinations of jest (or jasmine):
toHaveBeenCalled, not.toHaveBeenCalled, toHaveBeenCalledTimes, toHaveBeenCalledWith
e.g.
expect(storeSpy).toHaveBeenCalledWith(expect.objectContaining({ propOfInterest: someValue }));

Ridiculously easy. Has gotten us a long ways.

@BradleyHill
Copy link

@markwhitfeld Love the product. Ngrx sans all the ceremony. With redux pattern implementations, found great developers authored meh change and meh developers authored horrible code. This package strips down to the essence.

We haven't had many issues save the hack mentioned above but if have ideas and i can see value in them, may be able to throw some resources at them to solve them on our project and then push the code back to the github repo. Ngxs has been wonderful addition to project so would love to give back if can find use cases. I'm hug test advocate also so anything to help promote or simplify testing, no sell necessary.

@BradleyHill
Copy link

@gforceg As for your first question, typically mock out second component. It's a whole "unit" thing. You should only care about the primary component, not any transitive effects down the line. Law Of Demeter is testing.

Second question is little more nebulous. Not sure you are making a concession for testability. However, ngxs (or ngrx or any cqrs solution) is no panacea. You can accomplish your goal with ngxs, rolled services, or raw http client calls for that matter). Just a decision need to make depending on many factors.

@theyCallMeJay
Copy link

Hi Bradley, I did exactly what you suggested. However it keeps telling me that my “prop$” is undefined. Any hints? Thanks.

@splincode
Copy link
Member

@theyCallMeJay we are now working on a new package @ngxs/store/testing

@splincode splincode reopened this Apr 11, 2019
@theyCallMeJay
Copy link

theyCallMeJay commented Apr 11, 2019

@theyCallMeJay we are now working on a new package @ngxs/store/testing

@splincode That is amazing. May I ask the estimate of your release date for that package? I'm really scratching my head at this moment since my company is asking for unit testing coverage for the application but I can't provide none atm.

@theyCallMeJay
Copy link

@wouterv is it possible to share your piece of unit testing code that worked for you? Thanks

@wouterv
Copy link
Author

wouterv commented Apr 11, 2019

It was stated above, but for clarity, here goes:

describe('SelectPlatformsourceComponent', () => {
  let component: SelectPlatformsourceComponent;
  let fixture: ComponentFixture<SelectPlatformsourceComponent>;
  let store: Store;
  let sourceProducer: BehaviorSubject<string>;
  let storeSpy;

  beforeEach(async(() => {
    storeSpy = jasmine.createSpyObj<Store>(['dispatch', 'selectSnapshot']  as any);
    TestBed.configureTestingModule({
      imports: [NgbModule, ReactiveFormsModule],
      declarations: [SelectPlatformsourceComponent],
      providers: [{provide: Store, useValue: storeSpy}]
    }).compileComponents();
    store = TestBed.get(Store);
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(SelectPlatformsourceComponent);
    component = fixture.componentInstance;
    Object.defineProperty(component, 'platformSourceNamesList$', {writable: true});
    component.platformSourceNamesList$ = of(['foo', 'bar']);
    Object.defineProperty(component, 'source$', {writable: true});
    sourceProducer = new BehaviorSubject<string>(null);
    component.source$ = sourceProducer;
    fixture.detectChanges();
  });

@theyCallMeJay
Copy link

@wouterv, thanks! that still did not work for me though i did the same thing. Do you mind sharing what is inside your testbed configure? Thanks.

@wouterv
Copy link
Author

wouterv commented Apr 11, 2019

@theyCallMeJay I put in all we have.

@theyCallMeJay
Copy link

theyCallMeJay commented Apr 11, 2019

@wouterv , my bad. I somehow solved it by removing store from provide. Here is my snippet in case others have same issues as well.

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
DashboardComponent,
],
imports: [
NgxPaginationModule,
NgxsModule.forRoot([SessionState, InitiativeState]),
NgxsReduxDevtoolsPluginModule,
NgxsLoggerPluginModule,
RouterTestingModule,
SharedModule
],
providers: [
/* {provide: Store, useValue: storeSpy}, */
{provide: SessionService, useValue: mockSessionService},
{provide: ApiService, useValue: apiService}
]
}).compileComponents();

@philly-vanilly
Copy link

@theyCallMeJay NgxsModule is debatable, but you really shouldn't (have a reason to) have NgxsReduxDevtoolsPluginModule or NgxsLoggerPluginModule as a dependency. Those are not meant to be used in production and should not be in your dependencies except when running in dev (not test) environment.

@splincode
Copy link
Member

This problem is now considered here: ngxs-labs/testing#3

@arturovt arturovt self-assigned this Oct 14, 2019
@arturovt arturovt removed their assignment Oct 23, 2019
@splincode splincode changed the title How to mock @Select in ngxs when using a mock store 🚀[FEATURE]: How to mock @Select in ngxs when using a mock store Nov 4, 2019
@FortinFred
Copy link
Contributor

Hello there!

At work, we also need to mock the selects while testing our components. I've seen in a recent commit that @select decorator will be considered deprecated in future releases so I am not going to focus on this here. (Because it won't work with this approach)

I've used NGXS in the past and was able to convince my team to use it over NGRX. The main argument was that the programming style of NGXS is more in line with Angular. I've seen the internal NgxsTestBed and I'm not a fan mainly for the same reason. It's not the Angular way.

Inspired by the HttpTestingModule, I have quickly created an NgxsTestingModule. I would like some feedback.

import { Inject, Injectable, InjectionToken, NgModule } from '@angular/core';
import {NgxsModule, Store} from '@ngxs/store'
import { concat, Observable, ReplaySubject, Subscription } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
 
const ORIGINAL_STORE = new InjectionToken<Store>('OriginalStore')
 
@Injectable()
class MockedSelectors {
    private mockedSelectors: { key: any; value: ReplaySubject<any> }[] = []
    
    getMockedSelector(key: any) {
        let match = this.mockedSelectors.find( s => s.key === key)
 
        if(!match) {
            match = {key, value: new ReplaySubject<any>(1)}
            this.mockedSelectors.push(match)
        }
 
        return match.value
    }
}
 
@Injectable()
class StoreInterceptor {
 
    constructor(
        private mockedSelectors: MockedSelectors,
        @Inject(ORIGINAL_STORE) private store: Store
    ){}
 
    // interceptors
 
    dispatch(actionOrActions: any | any[]): Observable<any> {
        return this.store.dispatch(actionOrActions)
    }
 
    select(selector: any): Observable<any> {
 
        const mockedSelector = this.mockedSelectors.getMockedSelector(selector)
 
        return concat(
            this.store.select(selector).pipe(takeUntil(mockedSelector)),
            mockedSelector
        )
    }
 
    selectOnce(selector: any): Observable<any> {
        return this.store.selectOnce(selector)
    }
 
    selectSnapshot(selector: any): any {
        return this.store.selectSnapshot(selector)
    }
 
    subscribe(fn?: (value: any) => void): Subscription {
        return this.store.subscribe(fn)
    }
 
    snapshot(): any {
        return this.store.snapshot()
    }
 
    reset(state: any) {
        this.store.reset(state)
    }
 
}
 
@Injectable()
export class NgxsTestingController {
 
    constructor(private mockedSelector: MockedSelectors) {}
 
    mockSelector(selector: any) {
        return this.mockedSelector.getMockedSelector(selector)
    }
}
 
@NgModule()
export class NgxsTestingModule {
 
    static forRoot(...args) {
        const ngxsModule = NgxsModule.forRoot(...args)
 
        return {
            ...ngxsModule,
            providers: [
                {provide: Store, useClass: StoreInterceptor},
                {provide: ORIGINAL_STORE, useClass: Store},
                MockedSelectors,
                NgxsTestingController,
                ...ngxsModule.providers.filter(p => p !== Store)
            ]
        }
 
    }
 
}

Usage:

import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { Store } from '@ngxs/store';
import { SessionState } from '@sdum/core/store';
import {NgxsTestingModule, NgxsTestingController} from './ngxs-testing.module'
 

@Component({
    selector: 'test-component',
    template: ''
})
export class TestComponent  {

    authorities$ = this.store.select(SessionState.authorities)
    constructor(private store: Store) { }
    
}

describe('NgxsTestingModule', () => {
 
    let fixture: ComponentFixture<TestComponent>
    let component: TestComponent
    let ngxsTestingController: NgxsTestingController
  
    
    beforeEach(async () => {
        await TestBed.configureTestingModule({
          imports: [
            NgxsTestingModule.forRoot(),
          ],
          declarations: [
            TestComponent
          ]
    
        }).compileComponents()
        
        ngxsTestingController = TestBed.inject(NgxsTestingController)
        fixture = TestBed.createComponent(TestComponent)
        component = fixture.componentInstance
    })
 
    describe('SessionState.authorities', () => {
        it('should be mocked', async () => {
            ngxsTestingController.mockSelector(SessionState.authorities).next(['READ', 'WRITE', 'DELETE'])
            expect(await component.authorities$.toPromise()).toEqual(['READ', 'WRITE', 'DELETE'])
        });
    });
    
})

@weslleysilva
Copy link

Hello there!

At work, we also need to mock the selects while testing our components. I've seen in a recent commit that @select decorator will be considered deprecated in future releases so I am not going to focus on this here. (Because it won't work with this approach)

I've used NGXS in the past and was able to convince my team to use it over NGRX. The main argument was that the programming style of NGXS is more in line with Angular. I've seen the internal NgxsTestBed and I'm not a fan mainly for the same reason. It's not the Angular way.

Inspired by the HttpTestingModule, I have quickly created an NgxsTestingModule. I would like some feedback.

import { Inject, Injectable, InjectionToken, NgModule } from '@angular/core';
import {NgxsModule, Store} from '@ngxs/store'
import { concat, Observable, ReplaySubject, Subscription } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
 
const ORIGINAL_STORE = new InjectionToken<Store>('OriginalStore')
 
@Injectable()
class MockedSelectors {
    private mockedSelectors: { key: any; value: ReplaySubject<any> }[] = []
    
    getMockedSelector(key: any) {
        let match = this.mockedSelectors.find( s => s.key === key)
 
        if(!match) {
            match = {key, value: new ReplaySubject<any>(1)}
            this.mockedSelectors.push(match)
        }
 
        return match.value
    }
}
 
@Injectable()
class StoreInterceptor {
 
    constructor(
        private mockedSelectors: MockedSelectors,
        @Inject(ORIGINAL_STORE) private store: Store
    ){}
 
    // interceptors
 
    dispatch(actionOrActions: any | any[]): Observable<any> {
        return this.store.dispatch(actionOrActions)
    }
 
    select(selector: any): Observable<any> {
 
        const mockedSelector = this.mockedSelectors.getMockedSelector(selector)
 
        return concat(
            this.store.select(selector).pipe(takeUntil(mockedSelector)),
            mockedSelector
        )
    }
 
    selectOnce(selector: any): Observable<any> {
        return this.store.selectOnce(selector)
    }
 
    selectSnapshot(selector: any): any {
        return this.store.selectSnapshot(selector)
    }
 
    subscribe(fn?: (value: any) => void): Subscription {
        return this.store.subscribe(fn)
    }
 
    snapshot(): any {
        return this.store.snapshot()
    }
 
    reset(state: any) {
        this.store.reset(state)
    }
 
}
 
@Injectable()
export class NgxsTestingController {
 
    constructor(private mockedSelector: MockedSelectors) {}
 
    mockSelector(selector: any) {
        return this.mockedSelector.getMockedSelector(selector)
    }
}
 
@NgModule()
export class NgxsTestingModule {
 
    static forRoot(...args) {
        const ngxsModule = NgxsModule.forRoot(...args)
 
        return {
            ...ngxsModule,
            providers: [
                {provide: Store, useClass: StoreInterceptor},
                {provide: ORIGINAL_STORE, useClass: Store},
                MockedSelectors,
                NgxsTestingController,
                ...ngxsModule.providers.filter(p => p !== Store)
            ]
        }
 
    }
 
}

Usage:

import { Component } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { Store } from '@ngxs/store';
import { SessionState } from '@sdum/core/store';
import {NgxsTestingModule, NgxsTestingController} from './ngxs-testing.module'
 

@Component({
    selector: 'test-component',
    template: ''
})
export class TestComponent  {

    authorities$ = this.store.select(SessionState.authorities)
    constructor(private store: Store) { }
    
}

describe('NgxsTestingModule', () => {
 
    let fixture: ComponentFixture<TestComponent>
    let component: TestComponent
    let ngxsTestingController: NgxsTestingController
  
    
    beforeEach(async () => {
        await TestBed.configureTestingModule({
          imports: [
            NgxsTestingModule.forRoot(),
          ],
          declarations: [
            TestComponent
          ]
    
        }).compileComponents()
        
        ngxsTestingController = TestBed.inject(NgxsTestingController)
        fixture = TestBed.createComponent(TestComponent)
        component = fixture.componentInstance
    })
 
    describe('SessionState.authorities', () => {
        it('should be mocked', async () => {
            ngxsTestingController.mockSelector(SessionState.authorities).next(['READ', 'WRITE', 'DELETE'])
            expect(await component.authorities$.toPromise()).toEqual(['READ', 'WRITE', 'DELETE'])
        });
    });
    
})

Hello, where did you see that @select in future versions will be deprecated?
I would like to read more about it, as I am starting a new project and using ngxs to store.

@koraxos
Copy link

koraxos commented Jul 20, 2023

Is there any news or plan to upgrade the tools in the library to mock ngxs properly,
For me it's really bad, i'm in a team with lot of boilerplate in our .spec ( we have over 8000 unit test).
Most of the time where we need Ngxs, the store has been injected and it really is memory and time consuming in ours unit tests.
The fact is that it's what is recommended in the documentation.
but there is no tools to really mock the store or mock functions like dispatch.

If you take NgRx for example they have done somehting good , https://ngrx.io/guide/store/testing ,
Another example is the way the HttpTestingModule from Angular works.

It's really a major downgrade for ngxs,
Is there any discussion somewhere about the specification of the tooling we need ?
I would like to contribute on this subject.
I think it would improve greatly Ngxs usability in unit testing.

@sebimarkgraf
Copy link

@koraxos Unsure if you already tried the approach from @FortinFred but it works with the exception of one case wonderfully:
When using dynamic selectors, they selector is given as memoized function without any further metadata. Therefore, it is not possible to give a mocked selector for the case.

Maybe, you have another idea to solve this issue. If that's the case, the approach could be developed into a ngxs testing package that provides that functionality.

This could be an discussion point for the design of the metadata API of version 4.0 as well, but as this issue ranks as low in priority, I am unsure how much this is going to influence the decisions by the NGXS core team.

@sebimarkgraf
Copy link

Found a solution for anyone interested:
We simply use a decorator to patch the selectorName and arguments on the memoized function to have them available in testing.
We did not evaluate the performance impace yet, but as long as you are not excessively creating selectors this should be acceptable.

export function TestSelectorName(name?: string) {
  return function innerFunction(target, propertyKey: string, descriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      const selector = originalMethod.apply(this, args);
      (selector as any).selectorName = name || propertyKey;
      (selector as any).selectorArgs = args;
      return selector;
    };
  };
}

We just annotate our static methods with @TestSelectorName() and use the method name as default name.

For the tests you can then use the same selector with your specified arguments and FortinFreds approach:

ngxsTestingController.mockSelector(CustomState.customSelector('example-uuid')).next({property: a})

This required small adjustments to the implementation of FortinFreds mock store, but this is manageable.
I hope this could be more streamlined with Ngxs 4.0 but this fullfills our requirements for the moment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests