I'm trying to test a component which edits shopping list items.
When first loaded, the pertinent state values it receives via subscribing to store.select
are:
editedIngredient: null,
editedIngredientIndex: -1
With these values, it'll ensure that the class' editMode
property is set to false
.
During testing, I am trying to update the state once the component has loaded.
What i'm trying to achieve is updating the editedIngredient
and editedIngredientIndex
properties to a truthy value in my component, hence allowing the editMode
prop to be set to true
.
When trying the below code, I am able to get the component to render and editMode
is initally set to false.
Once the state is updated inside my test however, the store.select
subscriber does not update, meaning the test just finishes without editMode
ever being set to true.
Component code (ShoppingEditComponent)
ngOnInit() {
this._subscription = this.store.select('shoppingList').subscribe(stateData => {
if (stateData.editedIngredientIndex > -1) {
this.editMode = true; // I want to update my state later so that I set this value
return;
}
this.editMode = false; // I start with this value
});
}
Test code
let store: MockStore<State>;
const initialState: State = {
editedIngredient: null,
editedIngredientIndex: -1
};
const updatedShoppingListState: State = {
editedIngredient: seedData[0],
editedIngredientIndex: 0
};
let storeMock;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [FormsModule],
declarations: [
ShoppingEditComponent
],
providers: [
provideMockStore({ initialState }),
]
});
});
Test Attempt 1
it('should have \'editMode = true\' when it receives a selected ingredient in updated state',
fakeAsync(() => {
const fixture = TestBed.createComponent(ShoppingEditComponent);
const componentInstance = fixture.componentInstance;
// no change detection occurs, hence the subscribe callback does not get called with state update
store.setState(updatedShoppingListState);
expect(componentInstance['editMode']).toEqual(true);
})
);
Test Attempt 2
it('should have \'editMode = true\' when it receives a selected ingredient in updated state',
fakeAsync((done) => {
const fixture = TestBed.createComponent(ShoppingEditComponent);
const componentInstance = fixture.componentInstance;
fixture.whenStable().then(() => {
store.setState(updatedShoppingListState);
expect(componentInstance['editMode']).toEqual(true);
done();
});
})
);
done()
callback.done()
callback, I get an error stating that done() is not a function
For reference: I found the example for mocking the store from the NgRx docs (i'm on Angular 8, so this example is most relevant for me)
I am using Karma/Jasmine for my tests.
Any guidance would be really helpful.
After some research, I think I've found the problem.
Let's have a look at provideMockStore
's implementation:
export function provideMockStore<T = any>(
config: MockStoreConfig<T> = {}
): Provider[] {
return [
ActionsSubject,
MockState,
MockStore,
{ provide: INITIAL_STATE, useValue: config.initialState || {} },
{ provide: MOCK_SELECTORS, useValue: config.selectors },
{ provide: StateObservable, useClass: MockState },
{ provide: ReducerManager, useClass: MockReducerManager },
{ provide: Store, useExisting: MockStore },
];
}
The config
object that can be given to provideMockStore
has this shape:
export interface MockStoreConfig<T> {
initialState?: T;
selectors?: MockSelector[];
}
As you can see, the value at config.initialState
is assigned to the INITIAL_STATE
token, which is further injected in Store
(MockStore
in this case).
Notice how you're providing it:
const initialState: State = {
editedIngredient: null,
editedIngredientIndex: -1
};
provideStore({ initialState })
This means that INITIAL_STATE
will be this:
{
editedIngredient: null,
editedIngredientIndex: -1
};
This is how MockStore
looks like:
constructor(
private state$: MockState<T>,
actionsObserver: ActionsSubject,
reducerManager: ReducerManager,
@Inject(INITIAL_STATE) private initialState: T,
@Inject(MOCK_SELECTORS) mockSelectors: MockSelector[] = []
) {
super(state$, actionsObserver, reducerManager);
this.resetSelectors();
this.setState(this.initialState);
this.scannedActions$ = actionsObserver.asObservable();
for (const mockSelector of mockSelectors) {
this.overrideSelector(mockSelector.selector, mockSelector.value);
}
}
Notice it injects INITIAL_STATE
. MockState
is simply a BehaviorSubject
.
By calling super(state$, actionsObserver, reducerManager);
you're making sure that when you do this.store.pipe()
in your component, you'll receive the value of the MockState
.
This is how you're selecting from the store:
this.store.select('shoppingList').pipe(...)
but your initial state looks like this:
{
editedIngredient: null,
editedIngredientIndex: -1
};
With this in mind, I think you could solve the problem if you do:
const initialState = {
editedIngredient: null,
editedIngredientIndex: -1
};
provideMockStore({ initialState: { shoppingList: initialState } })
Also, if you'd like to dive deeper into ngrx/store
, you could check out this article.