-
-
Notifications
You must be signed in to change notification settings - Fork 2k
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
docs: add section to Store testing guide on using mock selectors #1797
Changes from 6 commits
65b5331
24cd217
6b1f729
4801047
15b975a
4329f17
c408c38
5c1f8c9
1061467
14e20a5
2679b0f
cf56e40
9b66485
f5d6d25
976090c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
import { createAction } from '@ngrx/store'; | ||
|
||
export const login = createAction('[Auth] Login'); | ||
export const logout = createAction('[Auth] Logout'); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import * as AuthActions from './auth.actions'; | ||
|
||
export { AuthActions }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { TestBed } from '@angular/core/testing'; | ||
import { Store, MemoizedSelector } from '@ngrx/store'; | ||
import { provideMockStore, MockStore } from '@ngrx/store/testing'; | ||
import { cold } from 'jasmine-marbles'; | ||
import { AuthGuard } from './auth-guard.service'; | ||
import * as fromAuth from './reducers'; | ||
|
||
describe('Auth Guard', () => { | ||
let guard: AuthGuard; | ||
let store: MockStore<fromAuth.State>; | ||
let loggedIn: MemoizedSelector<fromAuth.State, boolean>; | ||
|
||
beforeEach(() => { | ||
TestBed.configureTestingModule({ | ||
providers: [AuthGuard, provideMockStore()], | ||
}); | ||
|
||
store = TestBed.get(Store); | ||
guard = TestBed.get(AuthGuard); | ||
|
||
loggedIn = store.overrideSelector(fromAuth.getLoggedIn, false); | ||
}); | ||
|
||
it('should return false if the user state is not logged in', () => { | ||
const expected = cold('(a|)', { a: false }); | ||
|
||
expect(guard.canActivate()).toBeObservable(expected); | ||
}); | ||
|
||
it('should return true if the user state is logged in', () => { | ||
const expected = cold('(a|)', { a: true }); | ||
|
||
loggedIn.setResult(true); | ||
|
||
expect(guard.canActivate()).toBeObservable(expected); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { Injectable } from '@angular/core'; | ||
import { CanActivate } from '@angular/router'; | ||
import { Store, select } from '@ngrx/store'; | ||
import { take } from 'rxjs/operators'; | ||
import * as fromAuth from './reducers'; | ||
|
||
@Injectable({ | ||
providedIn: 'root', | ||
}) | ||
export class AuthGuard implements CanActivate { | ||
constructor(private store: Store<fromAuth.State>) {} | ||
|
||
canActivate() { | ||
return this.store.pipe( | ||
select(fromAuth.getLoggedIn), | ||
take(1) | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,20 @@ | ||||||||
import { createReducer, on } from '@ngrx/store'; | ||||||||
import { AuthActions } from '../actions'; | ||||||||
|
||||||||
export interface State { | ||||||||
loggedIn: boolean; | ||||||||
} | ||||||||
|
||||||||
export const initialState: State = { | ||||||||
loggedIn: false, | ||||||||
}; | ||||||||
|
||||||||
export const reducer = createReducer<State>( | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The export const reducer = createReducer<State>(
initialState,
on(AuthActions.login, (): State => ({ loggedIn: true })),
on(AuthActions.logout, (): State => ({ loggedIn: false }))
);
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To resolve the stackblitz error, add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||
[ | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
on(AuthActions.login, (): State => ({ loggedIn: true })), | ||||||||
on(AuthActions.logout, (): State => ({ loggedIn: false })) | ||||||||
], | ||||||||
initialState | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove initialState as the last argument |
||||||||
); | ||||||||
|
||||||||
export const getLoggedIn = (state: State) => state.loggedIn; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { | ||
createSelector, | ||
createFeatureSelector, | ||
Action, | ||
combineReducers, | ||
} from '@ngrx/store'; | ||
import * as fromAuth from './auth.reducer'; | ||
|
||
export interface AuthState { | ||
status: fromAuth.State; | ||
} | ||
|
||
export interface State { | ||
auth: AuthState; | ||
} | ||
|
||
export const selectAuthState = createFeatureSelector<AuthState>('auth'); | ||
|
||
export const selectAuthStatusState = createSelector( | ||
selectAuthState, | ||
(state: AuthState) => state.status | ||
); | ||
|
||
export const getLoggedIn = createSelector( | ||
selectAuthStatusState, | ||
fromAuth.getLoggedIn | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<html> | ||
<head> | ||
<title>Angular App</title> | ||
</head> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add |
||
<body> | ||
<!-- Intentionally empty --> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import './testing/global-jasmine'; | ||
import 'jasmine-core/lib/jasmine-core/jasmine-html.js'; | ||
import 'jasmine-core/lib/jasmine-core/boot.js'; | ||
|
||
declare var jasmine; | ||
|
||
import './polyfills'; | ||
|
||
import 'zone.js/dist/async-test'; | ||
import 'zone.js/dist/fake-async-test'; | ||
import 'zone.js/dist/long-stack-trace-zone'; | ||
import 'zone.js/dist/proxy.js'; | ||
import 'zone.js/dist/sync-test'; | ||
import 'zone.js/dist/jasmine-patch'; | ||
|
||
import { getTestBed } from '@angular/core/testing'; | ||
import { | ||
BrowserDynamicTestingModule, | ||
platformBrowserDynamicTesting | ||
} from '@angular/platform-browser-dynamic/testing'; | ||
|
||
// Spec files to include in the Stackblitz tests | ||
import './test-files.ts'; | ||
|
||
// | ||
|
||
bootstrap(); | ||
|
||
// | ||
|
||
function bootstrap () { | ||
if (window['jasmineRef']) { | ||
location.reload(); | ||
return; | ||
} else { | ||
window.onload(undefined); | ||
window['jasmineRef'] = jasmine.getEnv(); | ||
} | ||
|
||
// First, initialize the Angular testing environment. | ||
getTestBed().initTestEnvironment( | ||
BrowserDynamicTestingModule, | ||
platformBrowserDynamicTesting() | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
// Import spec files individually for Stackblitz | ||
import './app/auth-guard.service.spec'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import jasmineRequire from 'jasmine-core/lib/jasmine-core/jasmine.js'; | ||
|
||
window['jasmineRequire'] = jasmineRequire; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"description": "Store Testing Tutorial", | ||
"files": ["!**/*.d.ts", "!**/*.js", "**/*.spec.ts"] | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -58,6 +58,24 @@ describe('Auth Guard', () => { | |||||
}); | ||||||
</code-example> | ||||||
|
||||||
#### Using Mock Selectors | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
`MockStore` also provides the ability to mock individual selectors to return a passed value using the `overrideSelector()` method. When the selector is invoked by the `select` method, the returned value is overridden by the passed value, regardless of any state snapshot provided in `provideMockStore()`. | ||||||
jtcrowson marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
`overrideSelector()` returns a `MemoizedSelector`. To update the mock selector to return a different value, use the `MemoizedSelector`'s `setResult()` method. | ||||||
|
||||||
`overrideSelector()` supports mocking the `select` method (used in RxJS pipe) and the `Store` `select` instance method using a string or selector. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you wanted to say
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I intended to use overrideSelector and summarize the note from #1688:
|
||||||
|
||||||
Usage: | ||||||
|
||||||
<code-example header="auth-guard.service.ts" path="testing-store/src/app/auth-guard.service.ts"></code-example> | ||||||
|
||||||
<code-example header="auth-guard.service.spec.ts" path="testing-store/src/app/auth-guard.service.spec.ts"></code-example> | ||||||
|
||||||
In this example, we mock the `getLoggedIn` selector by using `overrideSelector`, passing in the `getLoggedIn` selector with a default mocked return value of `false`. In the second test, we use `setResult()` to update the mock selector to return `true`. | ||||||
|
||||||
Try the <live-example name="testing-store"></live-example>. | ||||||
|
||||||
### Using Store for Integration Testing | ||||||
|
||||||
Use the `StoreModule.forRoot` in your `TestBed` configuration when testing components or services that inject `Store`. | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove?