Skip to content

Commit

Permalink
docs(effects): document defining effects as function (#1212)
Browse files Browse the repository at this point in the history
Closes #1198
  • Loading branch information
timdeschryver authored and brandonroberts committed Aug 5, 2018
1 parent 0af3c11 commit d34b88a
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 43 deletions.
40 changes: 40 additions & 0 deletions docs/effects/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,43 @@ describe('My Effects', () => {
});
});
```

### Effects as functions

Effects can be defined as functions as well as variables. Defining an effect as a function allows you to define default values while having the option to override these variables during tests. This without breaking the functionality in the application.

The following example effect debounces the user input into from a search action.

```ts
@Effect()
search$ = this.actions$.pipe(
ofType<Search>(BookActionTypes.Search),
debounceTime(300, asyncScheduler),
switchMap(...)
```
The same effect but now defined as a function, would look as follows:
```ts
@Effect()
// refactor as input properties and provide default values
search$ = ({
debounce = 300,
scheduler = asyncScheduler
} = {}) => this.actions$.pipe(
ofType<Search>(BookActionTypes.Search),
debounceTime(debounce, scheduler),
switchMap(...)
```
Within our tests we can now override the default properties:
```ts
const actual = effects.search$({
debounce: 30,
scheduler: getTestScheduler(),
});
expect(actual).toBeObservable(expected);
```
Doing this has the extra benefit of hiding implementation details, making your tests less prone to break due to implementation details changes. Meaning that if you would change the `debounceTime` inside the effect your tests wouldn't have to be changed,these tests would still pass.
25 changes: 19 additions & 6 deletions example-app/app/books/effects/book.effects.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { empty, Observable } from 'rxjs';
import { GoogleBooksService } from '../../core/services/google-books.service';
import { Search, SearchComplete, SearchError } from '../actions/book.actions';
import { Book } from '../models/book';
import { BookEffects, SEARCH_DEBOUNCE, SEARCH_SCHEDULER } from './book.effects';
import { BookEffects } from './book.effects';

describe('BookEffects', () => {
let effects: BookEffects;
Expand All @@ -23,8 +23,6 @@ describe('BookEffects', () => {
useValue: { searchBooks: jest.fn() },
},
provideMockActions(() => actions$),
{ provide: SEARCH_SCHEDULER, useFactory: getTestScheduler },
{ provide: SEARCH_DEBOUNCE, useValue: 30 },
],
});

Expand All @@ -46,7 +44,12 @@ describe('BookEffects', () => {
const expected = cold('-----b', { b: completion });
googleBooksService.searchBooks = jest.fn(() => response);

expect(effects.search$).toBeObservable(expected);
expect(
effects.search$({
debounce: 30,
scheduler: getTestScheduler(),
})
).toBeObservable(expected);
});

it('should return a new book.SearchError if the books service throws', () => {
Expand All @@ -59,7 +62,12 @@ describe('BookEffects', () => {
const expected = cold('-----b', { b: completion });
googleBooksService.searchBooks = jest.fn(() => response);

expect(effects.search$).toBeObservable(expected);
expect(
effects.search$({
debounce: 30,
scheduler: getTestScheduler(),
})
).toBeObservable(expected);
});

it(`should not do anything if the query is an empty string`, () => {
Expand All @@ -68,7 +76,12 @@ describe('BookEffects', () => {
actions$ = hot('-a---', { a: action });
const expected = cold('---');

expect(effects.search$).toBeObservable(expected);
expect(
effects.search$({
debounce: 30,
scheduler: getTestScheduler(),
})
).toBeObservable(expected);
});
});
});
60 changes: 23 additions & 37 deletions example-app/app/books/effects/book.effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,6 @@ import {
SearchError,
} from '../actions/book.actions';
import { Book } from '../models/book';
import { Scheduler } from 'rxjs/internal/Scheduler';

export const SEARCH_DEBOUNCE = new InjectionToken<number>('Search Debounce');
export const SEARCH_SCHEDULER = new InjectionToken<Scheduler>(
'Search Scheduler'
);

/**
* Effects offer a way to isolate and easily test side-effects within your
Expand All @@ -40,41 +34,33 @@ export const SEARCH_SCHEDULER = new InjectionToken<Scheduler>(
@Injectable()
export class BookEffects {
@Effect()
search$: Observable<Action> = this.actions$.pipe(
ofType<Search>(BookActionTypes.Search),
debounceTime(this.debounce || 300, this.scheduler || asyncScheduler),
map(action => action.payload),
switchMap(query => {
if (query === '') {
return empty();
}
search$ = ({ debounce = 300, scheduler = asyncScheduler } = {}): Observable<
Action
> =>
this.actions$.pipe(
ofType<Search>(BookActionTypes.Search),
debounceTime(debounce, scheduler),
map(action => action.payload),
switchMap(query => {
if (query === '') {
return empty();
}

const nextSearch$ = this.actions$.pipe(
ofType(BookActionTypes.Search),
skip(1)
);
const nextSearch$ = this.actions$.pipe(
ofType(BookActionTypes.Search),
skip(1)
);

return this.googleBooks.searchBooks(query).pipe(
takeUntil(nextSearch$),
map((books: Book[]) => new SearchComplete(books)),
catchError(err => of(new SearchError(err)))
);
})
);
return this.googleBooks.searchBooks(query).pipe(
takeUntil(nextSearch$),
map((books: Book[]) => new SearchComplete(books)),
catchError(err => of(new SearchError(err)))
);
})
);

constructor(
private actions$: Actions,
private googleBooks: GoogleBooksService,
@Optional()
@Inject(SEARCH_DEBOUNCE)
private debounce: number,
/**
* You inject an optional Scheduler that will be undefined
* in normal application usage, but its injected here so that you can mock out
* during testing using the RxJS TestScheduler for simulating passages of time.
*/
@Optional()
@Inject(SEARCH_SCHEDULER)
private scheduler: Scheduler
private googleBooks: GoogleBooksService
) {}
}

0 comments on commit d34b88a

Please sign in to comment.