Skip to content

Commit cc499d6

Browse files
author
Lukas Ruebbelke
authored
Merge pull request onehungrymind#6 from onehungrymind/feature/tests
Feature/tests
2 parents b98e9f1 + 161bc9c commit cc499d6

File tree

10 files changed

+379
-42
lines changed

10 files changed

+379
-42
lines changed

apps/dashboard/src/app/app.component.spec.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import 'hammerjs';
2-
31
import { ComponentFixture, TestBed } from '@angular/core/testing';
42
import { DebugElement } from '@angular/core';
53
import { AppComponent } from './app.component';

apps/dashboard/src/app/items/items.component.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { TestBed, async, ComponentFixture } from '@angular/core/testing';
44

55
import { ItemsComponent } from './items.component';
6-
import { ItemsService } from '../core';
6+
import { ItemsService } from '@workspace/common-data';
77

88
class ItemsServiceStub {}
99

libs/common-data/src/lib/core/items/items.service.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
/* tslint:disable:no-unused-variable */
22

33
import { TestBed, inject } from '@angular/core/testing';
4-
import { Http } from '@angular/http';
4+
import { HttpClient } from '@angular/common/http';
55

66
import { ItemsService } from './items.service';
77

8-
class HttpStub {}
8+
class HttpClientStub {}
99

1010
describe('Service: Items', () => {
1111
beforeEach(() => {
1212
TestBed.configureTestingModule({
1313
providers: [
1414
ItemsService,
15-
{provide: Http, useClass: HttpStub}
15+
{provide: HttpClient, useClass: HttpClientStub}
1616
]
1717
});
1818
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { of } from 'rxjs';
2+
3+
export class ItemsServiceStub {
4+
all() {return of([])}
5+
create(item) {return of({})}
6+
update(item) {return of({})}
7+
delete(item) {return of({})}
8+
}

libs/common-data/src/lib/state/items/items.effects.spec.ts

Lines changed: 103 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,127 @@ import { TestBed } from '@angular/core/testing';
22
import { provideMockActions } from '@ngrx/effects/testing';
33
import { StoreModule } from '@ngrx/store';
44
import { DataPersistence } from '@nrwl/nx';
5-
import { hot } from '@nrwl/nx/testing';
6-
import { Observable } from 'rxjs';
5+
import { cold, hot } from '@nrwl/nx/testing';
6+
import { Observable, of, throwError } from 'rxjs';
7+
import { finalize } from 'rxjs/operators';
78

8-
import { ItemsLoaded, LoadItems } from './items.actions';
9+
import { AddItem, DeleteItem, ItemAdded, ItemDeleted, ItemsLoaded, ItemUpdated, LoadItems, UpdateItem } from './items.actions';
910
import { ItemsEffects } from './items.effects';
11+
import { ItemsService } from '../../core/items/items.service';
12+
import { Item } from '../../core/items/item.model';
13+
import { ItemsServiceStub } from '../../core/items/items.service.stub';
1014

11-
describe('ItemsEffects', () => {
15+
fdescribe('ItemsEffects', () => {
1216
let actions$: Observable<any>;
1317
let effects$: ItemsEffects;
18+
let itemsService: ItemsService;
1419

1520
beforeEach(() => {
1621
TestBed.configureTestingModule({
1722
imports: [StoreModule.forRoot({})],
1823
providers: [
1924
ItemsEffects,
2025
DataPersistence,
21-
provideMockActions(() => actions$)
26+
provideMockActions(() => actions$),
27+
{provide: ItemsService, useClass: ItemsServiceStub}
2228
]
2329
});
2430

2531
effects$ = TestBed.get(ItemsEffects);
32+
itemsService = TestBed.get(ItemsService);
2633
});
2734

28-
describe('someEffect', () => {
29-
it('should work', () => {
30-
actions$ = hot('-a-|', { a: new LoadItems({}) });
31-
expect(effects$.loadItems$).toBeObservable(
32-
hot('-a-|', { a: new ItemsLoaded({}) })
33-
);
35+
describe('`loadItems$`', () => {
36+
it('should trigger `ItemsLoaded` action with data from `ItemsService.all`', () => {
37+
const items = [{id: 'csa-132', name: 'Test', description: 'Testing', price: 4313}];
38+
spyOn(itemsService, 'all').and.returnValue(of(items));
39+
40+
actions$ = hot('-a-|', { a: new LoadItems() });
41+
const expected$ = cold('-a-|', { a: new ItemsLoaded(items) });
42+
43+
expect(effects$.loadItems$).toBeObservable(expected$);
44+
expect(itemsService.all).toHaveBeenCalled();
45+
});
46+
47+
it('should log errors', () => {
48+
spyOn(itemsService, 'all').and.returnValue(throwError('That did not go well...'));
49+
spyOn(console, 'error').and.callThrough();
50+
51+
actions$ = hot('-a-|', { a: new LoadItems() });
52+
effects$.loadItems$
53+
.pipe(finalize(() => expect(console.error).toHaveBeenCalledWith('Error', 'That did not go well...')))
54+
.subscribe();
55+
});
56+
});
57+
58+
describe('`addItem$`', () => {
59+
it('should trigger `ItemAdded` action with data from `ItemsService.create`', () => {
60+
const item = {id: null, name: 'Test', description: 'Testing', price: 4313};
61+
const createdItem = {...item, id: 'jhh14-created'};
62+
spyOn(itemsService, 'create').and.returnValue(of(createdItem));
63+
64+
actions$ = hot('-a-|', { a: new AddItem(item) });
65+
const expected$ = cold('-a-|', { a: new ItemAdded(createdItem) });
66+
67+
expect(effects$.addItem$).toBeObservable(expected$);
68+
expect(itemsService.create).toHaveBeenCalledWith(item);
69+
});
70+
71+
it('should log errors', () => {
72+
spyOn(itemsService, 'create').and.returnValue(throwError('That did not go well...'));
73+
spyOn(console, 'error').and.callThrough();
74+
75+
actions$ = hot('-a-|', { a: new AddItem({} as Item) });
76+
effects$.addItem$
77+
.pipe(finalize(() => expect(console.error).toHaveBeenCalledWith('Error', 'That did not go well...')))
78+
.subscribe();
79+
});
80+
});
81+
82+
describe('`updateItem$`', () => {
83+
it('should trigger `ItemUpdated` action with data from `ItemsService.update`', () => {
84+
const item = {id: 'jhh14-updated', name: 'Test', description: 'Testing', price: 4313};
85+
const updatedItem = {...item, name: 'Updated', description: 'Different'};
86+
spyOn(itemsService, 'update').and.returnValue(of(updatedItem));
87+
88+
actions$ = hot('-a-|', { a: new UpdateItem(item) });
89+
const expected$ = cold('-a-|', { a: new ItemUpdated(updatedItem) });
90+
91+
expect(effects$.updateItem$).toBeObservable(expected$);
92+
expect(itemsService.update).toHaveBeenCalledWith(item);
93+
});
94+
95+
it('should log errors', () => {
96+
spyOn(itemsService, 'update').and.returnValue(throwError('That did not go well...'));
97+
spyOn(console, 'error').and.callThrough();
98+
99+
actions$ = hot('-a-|', { a: new UpdateItem({} as Item) });
100+
effects$.updateItem$
101+
.pipe(finalize(() => expect(console.error).toHaveBeenCalledWith('Error', 'That did not go well...')))
102+
.subscribe();
103+
});
104+
});
105+
106+
describe('`deleteItem$`', () => {
107+
it('should trigger `ItemDeleted` action with data from `ItemsService.delete`', () => {
108+
const item = {id: 'jhh14-deleted', name: 'Test', description: 'Testing', price: 4313};
109+
spyOn(itemsService, 'delete').and.returnValue(of(item));
110+
111+
actions$ = hot('-a-|', { a: new DeleteItem(item) });
112+
const expected$ = cold('-a-|', { a: new ItemDeleted(item) });
113+
114+
expect(effects$.deleteItem$).toBeObservable(expected$);
115+
expect(itemsService.delete).toHaveBeenCalledWith(item);
116+
});
117+
118+
it('should log errors', () => {
119+
spyOn(itemsService, 'delete').and.returnValue(throwError('That did not go well...'));
120+
spyOn(console, 'error').and.callThrough();
121+
122+
actions$ = hot('-a-|', { a: new DeleteItem({} as Item) });
123+
effects$.deleteItem$
124+
.pipe(finalize(() => expect(console.error).toHaveBeenCalledWith('Error', 'That did not go well...')))
125+
.subscribe();
34126
});
35127
});
36128
});
Lines changed: 157 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,164 @@
1-
import { inject, TestBed } from '@angular/core/testing';
2-
1+
import { TestBed } from '@angular/core/testing';
2+
import { Store, StoreModule } from '@ngrx/store';
3+
import { readFirst } from '@nrwl/nx/testing';
4+
import { NgModule } from '@angular/core';
5+
import { EffectsModule } from '@ngrx/effects';
6+
import { NxModule } from '@nrwl/nx';
7+
import { Item } from '../../core/items/item.model';
8+
import { ItemsService } from '../../core/items/items.service';
9+
import { ItemsServiceStub } from '../../core/items/items.service.stub';
10+
import { itemsReducer, ItemsState } from './items.reducer';
311
import { ItemsFacade } from './items.facade';
12+
import { ItemsEffects } from './items.effects';
13+
import * as ItemsActions from './items.actions';
14+
15+
interface TestSchema {
16+
'items' : ItemsState
17+
}
18+
19+
fdescribe('ItemsFacade', () => {
20+
let facade: ItemsFacade;
21+
let store: Store<TestSchema>;
22+
let createItem;
23+
let itemsService: ItemsService;
424

5-
describe('ItemsFacade', () => {
625
beforeEach(() => {
7-
TestBed.configureTestingModule({
8-
providers: [ItemsFacade]
26+
createItem = ( id:string, name = '', description = '', price = 0 ): Item => ({
27+
id,
28+
name: name ? `name-${id}` : id,
29+
description: description,
30+
price: price,
931
});
1032
});
1133

12-
it('should be created', inject([ItemsFacade], (service: ItemsFacade) => {
13-
expect(service).toBeTruthy();
14-
}));
34+
describe('used in NgModule', () => {
35+
36+
beforeEach(() => {
37+
@NgModule({
38+
imports: [
39+
NxModule.forRoot(),
40+
StoreModule.forRoot({items: itemsReducer}),
41+
EffectsModule.forRoot([ItemsEffects]),
42+
],
43+
providers: [
44+
{provide: ItemsService, useClass: ItemsServiceStub},
45+
]
46+
})
47+
class RootModule {}
48+
TestBed.configureTestingModule({ imports: [RootModule] });
49+
50+
itemsService = TestBed.get(ItemsService);
51+
store = TestBed.get(Store);
52+
facade = TestBed.get(ItemsFacade);
53+
});
54+
55+
it('allItems$ should return the current list', async (done) => {
56+
try {
57+
let list = await readFirst(facade.allItems$);
58+
59+
expect(list.length).toBe(0);
60+
61+
store.dispatch(new ItemsActions.ItemsLoaded([
62+
createItem('AAA'),
63+
createItem('BBB')
64+
]));
65+
66+
list = await readFirst(facade.allItems$);
67+
68+
expect(list.length).toBe(2);
69+
70+
done();
71+
} catch (err) {
72+
done.fail(err);
73+
}
74+
});
75+
76+
it('currentItem$ should return the currently selectedItem', async (done) => {
77+
try {
78+
let current = await readFirst(facade.currentItem$);
79+
80+
expect(current.id).toBeNull();
81+
82+
store.dispatch(new ItemsActions.ItemsLoaded([
83+
createItem('AAA'),
84+
createItem('BBB')
85+
]));
86+
87+
store.dispatch(new ItemsActions.ItemSelected('BBB'));
88+
89+
current = await readFirst(facade.currentItem$);
90+
91+
expect(current.id).toBe('BBB');
92+
93+
done();
94+
} catch (err) {
95+
done.fail(err);
96+
}
97+
});
98+
99+
it('mutations$ should only stream mutative actions', () => {
100+
const addItemAction = new ItemsActions.AddItem({} as Item),
101+
updateItemAction = new ItemsActions.UpdateItem({} as Item),
102+
deleteItemAction = new ItemsActions.DeleteItem({} as Item);
103+
104+
const actions = [];
105+
facade.mutations$.subscribe(mutation => {
106+
actions.push(mutation);
107+
});
108+
109+
store.dispatch(addItemAction);
110+
store.dispatch(new ItemsActions.LoadItems());
111+
store.dispatch(updateItemAction);
112+
store.dispatch(new ItemsActions.ItemSelected({}));
113+
store.dispatch(deleteItemAction);
114+
115+
const expectedActions = [
116+
addItemAction,
117+
updateItemAction,
118+
deleteItemAction
119+
];
120+
121+
expect(actions).toEqual(expectedActions);
122+
});
123+
124+
describe('dispatchers', () => {
125+
126+
beforeEach(() => {
127+
spyOn(store, 'dispatch');
128+
});
129+
130+
it('#selectItem should dispatch `ItemSelected` action with an item ID', () => {
131+
const expectedAction = new ItemsActions.ItemSelected('AAA');
132+
facade.selectItem('AAA');
133+
expect(store.dispatch).toHaveBeenCalledWith(expectedAction);
134+
});
135+
136+
it('#loadAll should dispatch `LoadItems` action', () => {
137+
const expectedAction = new ItemsActions.LoadItems();
138+
facade.loadAll();
139+
expect(store.dispatch).toHaveBeenCalledWith(expectedAction);
140+
});
141+
142+
it('#addItem should dispatch `AddItem` action with an item', () => {
143+
const item = createItem('AAA');
144+
const expectedAction = new ItemsActions.AddItem(item);
145+
facade.addItem(item);
146+
expect(store.dispatch).toHaveBeenCalledWith(expectedAction);
147+
});
148+
149+
it('#updateItem should dispatch `UpdateItem` action with an item', () => {
150+
const item = createItem('AAA');
151+
const expectedAction = new ItemsActions.UpdateItem(item);
152+
facade.updateItem(item);
153+
expect(store.dispatch).toHaveBeenCalledWith(expectedAction);
154+
});
155+
156+
it('#deleteItem should dispatch `DeleteItem` action with an item', () => {
157+
const item = createItem('AAA');
158+
const expectedAction = new ItemsActions.DeleteItem(item);
159+
facade.deleteItem(item);
160+
expect(store.dispatch).toHaveBeenCalledWith(expectedAction);
161+
});
162+
});
163+
});
15164
});

libs/common-data/src/lib/state/items/items.facade.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@ export class ItemsFacade {
1414
allItems$ = this.store.pipe(select(selectAllItems));
1515
currentItem$ = this.store.pipe(select(selectCurrentItem));
1616

17-
mutations$ = this.actions$.pipe(
18-
filter(action =>
19-
action.type === ItemsActionTypes.AddItem
20-
|| action.type === ItemsActionTypes.UpdateItem
21-
|| action.type === ItemsActionTypes.DeleteItem
22-
)
23-
);
17+
mutations$ = this.actions$
18+
.pipe(
19+
filter(action =>
20+
action.type === ItemsActionTypes.AddItem
21+
|| action.type === ItemsActionTypes.UpdateItem
22+
|| action.type === ItemsActionTypes.DeleteItem
23+
)
24+
);
2425

2526
constructor(private store: Store<ItemsState>, private actions$: ActionsSubject) {}
2627

0 commit comments

Comments
 (0)