Skip to content
This repository was archived by the owner on Jan 10, 2018. It is now read-only.

Make mocking easier #128

Closed
SebastianStehle opened this issue May 30, 2016 · 26 comments
Closed

Make mocking easier #128

SebastianStehle opened this issue May 30, 2016 · 26 comments

Comments

@SebastianStehle
Copy link

Hi,

I am new to angular, typescript and ngrx. I would like to make testing easier. But when I try to mock the store with TypeMoq I end up in a dependency hell. Can you provide an abstract class for your store with a private implementation?

@frederikschubert
Copy link

What are you trying to test that makes you mock the store?

@SebastianStehle
Copy link
Author

I have some services as wrapper around my store and would like to test these services.

When other component are using this service I need a Noop Store to simplify the creation of mockups for this service.

@frederikschubert
Copy link

IMO testing a service is rather an integration test than a unit test. You can test the reducer functions and effects on their own in solitary unit tests. In those tests it would make sense to mock things.
So I would suggest (for testing the service) to use the normal provideStore function with the reducer of the state on which your service depends.
In this tests you can mock things that would slow down the test or make it brittle like REST clients.
I don't think there is much value in mocking the store.

@SebastianStehle
Copy link
Author

I think if I can isolate the service, it will be a unit test ;) ... and I think it might be helpful to test the service. E.g. I made a lot of stupid mistakes because the payloads are not type safe. In another part of my code I build some complex observable flows and it would be helpful to mock the select function. I often try to reorganize my code so that I do not have to test the integration parts of my application. But in most cases, when I need a debugger, I made a mistakes in these parts.

But furthermore, I think that architectures that use services and DI might get very complex dependency hierarchies and therefore it is always very helpful to have the ability to mock dependencies. I don't know if there is some advice for angular2 in general but for me it seems that mocking services that have other dependencies or do some work in the constructor is very painful. In other languages there is the rule to use interfaces for all services.

@frederikschubert
Copy link

frederikschubert commented May 31, 2016

Ok I see your point. But it should be possible to provide a MockStore by adding this to your beforeEachProviders:

provide(Store, useClass: MockStore)

MockStore should extend Store and overwrite the needed parts for your test. The dependencies of the Store can be provided like this as well.

@SebastianStehle
Copy link
Author

SebastianStehle commented May 31, 2016

The problem is: I need to call super in the constructor, therefore I need at least a State reference and some other dependencies. I can have a look to the implementation, which tells me that all other dependencies, but this I need to check it again with every update. Same problem with TypeMoq ( https://github.com/florinn/typemoq)

@frederikschubert
Copy link

Yes that is true. But again, I think it makes no sense to mock the store. When you tested the reducer for your service and provide the store with only that reducer it should be sufficient.
Mocks make sense if there is a cost in using the real thing. This cost might be that you wrote the thing's code and want to decouple the tests. Or using it would lead to flaky tests or increase the time the test takes.
The store has none of these costs.

@SebastianStehle
Copy link
Author

As I mentioned: I want to test my integration code. I think there are only these options:

  1. Extract the integration logic to separate classes, so you don't need any mockups, e.g. I can pass in a function pointer to the Store.next function. In this scenario you have some very small parts not covered by automated tests, which is fine.
  2. Isolate your integration logic and use mocks.
  3. Make integration tests.

I just don't see why you don't like it to have an interface. Using Interfaces is a well known principle for Java, C# and so on and this gives everybody the freedom to organize the tests how he wants. I think there should be some guidelines how to design services in an angular2 app in general. It is not an good situation when some services can be mocked easily and some services can not.

@frederikschubert
Copy link

There speaks nothing against interfaces. :)
I also think that the Store should implement one because explicit interfaces add flexibility as you said.
I just think that for an integration test it is acceptable to use the normal store as it adds no costs that I can think of.
Sorry if I made my point not clear, it is already pretty late here.

@SebastianStehle
Copy link
Author

Totally agree :)

@frederikschubert
Copy link

Maybe you could send a PR with the Store implementing such an interface?

@robwormald
Copy link
Contributor

I just don't see why you don't like it to have an interface. Using Interfaces is a well known principle for Java, C# and so on and this gives everybody the freedom to organize the tests how he wants.

Store already provides an interface, simply by the virtue of being a typescript class.

We don't typically use interfaces in typescript, because they have no runtime value and thus aren't usable for dependency injection like other languages, so it's unnecessary overhead that provides little benefit.

If you want to implement a mock version (which, IMHO, is entirely unnecessary, but feel free...)

import {Store} from '@ngrx/store'

export class MockStore implements Store {
  //implement your mock methods here.
}


provide(Store, {useClass: MockStore})

I'd suggest starting your mock store by perhaps extending an Rx Subject/BehaviorSubject, because that's what Store already does. At this point though, you're basically reinventing store entirely.

export class MockStore extends Subject implements Store {}

@SebastianStehle
Copy link
Author

Awesome, havent realized this yet, thanks.

@jrspriggs
Copy link

Sorry to be bringing this back up when it's been closed for a while. In the code, I don't see an interface for store to implement, would there be a reason to extend Observable for a mock store over the idea of just extending Store to make a mock?

For my situation, I've got a navigation service that doesn't perform any http interactions, but handles knowing the state via the ngrx/Store, then dispatching an action to change the panels shown in the ui components. For testing, then I would mock a store that I override the dispatch function to spy on and know that the right action was dispatched.

Or would it be better to not mock the store at all, just make a spy, say as

spy = spyOn(store, 'dispatch'); spy.expectToHaveBeenCalledWith(expectedAction);

I guess I'm trying to figure out what would be workable and a better approach overall without rewriting much.

@jonesmac
Copy link

jonesmac commented Dec 6, 2016

Another way I have seen people stub the store in their TestBed.configuration.providers is as follows:

{
   provide: Store,
   useClass: class { dispatch = jasmine.createSpy('dispatch') }
},

Basically, you are telling jasmine to create a spy and handle the Store methods you are using in your Service, Component, etc. Just swap dispatch with whatever store method you want and jasmine will say the method exists for your fake Store, but nothing will happen.

Keep in mind that any effects from your method being called will not happen. In my case this was fine since I wasn't testing the result of a dispatch, only that it was being called.

@karolmie1
Copy link

Maybe you should look at the problem from another perspective, and create one easy testable "dumb" component with all input data handled on silver plate, and "controler" for that service connected with store and providing all necessary data for the dumb component.

@kuncevic
Copy link

kuncevic commented Jan 6, 2017

@jonesmac how would you test the result after fake dispatch has being called?
As you saying using useClass: class { dispatch = jasmine.createSpy('dispatch') } you wont be able to test the result of dispatch action.

@jonesmac
Copy link

jonesmac commented Jan 6, 2017

@kuncevic that is correct.

If you want to see the results of dispatch and you need to set up your actual store with the provideStore method.

@jonesmac
Copy link

jonesmac commented Jan 9, 2017 via email

@nweldev
Copy link

nweldev commented Jan 20, 2017

For my projects, I use the following mock :

export class MockStore<T> extends Store<T> {

  private _fakeData: Object = {};
  private fakeDataSubject: BehaviorSubject<Object> = new BehaviorSubject(this._fakeData);

  select = <T, R>(mapFn: any, ...paths: string[]): Observable<R> => {
    return map.call(this.fakeDataSubject, mapFn);
  }

  constructor() {
    super(dispatcherMock, reducerMock, stateMock);
  }

  nextMock(mock: Object, ...keys: string[]) {
    let curMockLevel = this._fakeData = {};
    keys.forEach((key, idx) => {
      curMockLevel = curMockLevel[key] = idx === keys.length - 1 ? mock : {};
    });
    this.fakeDataSubject.next(this._fakeData);
  }

  get fakeData() {
    return this._fakeData;
  }

}

FYI : I used the 'fakeDataSubject' instead of the store itself in order to directly send the desired data, without being restricted by the typescript Action class. Given that we don't have any reducer here, I don't see the point to use my actions classes, and directly send a data I would expect to get in a select callback argument.

If I need to test dispatch, I simply use :

beforeEach(inject([Store], (_store: Store) => {
      store = _store;
      storeSpy = spyOn(store, 'dispatch').and.callFake((arg) => {
        testArg = arg;
      });
    }));

it('foo bar description', () => {
      // some behaviour including a dispatch
      expect(storeSpy).toHaveBeenCalled();
      expect(testArg).toEqual(new MyAction('foo'));
    });
  });

and if I need to test select, I write a :

beforeEach(()=>{
    store.nextMock(mockData, 'storeName', 'subStoreName')
});

Given that I also test my effects, reducers and services, I never use them when testing my components, and so I don't need to have any "from dispatch to select using an action" behaviour with my MockStore.

Hope I could help. If you think I could do better, please tell me.

And if you think I don't (we can always dream), I think this kind of class could be direcly exported by @ngrx/store, and it would be a pleasure to make a PR with that.

@crain
Copy link

crain commented Mar 21, 2017

@noelmace how do you mock:

super(dispatcherMock, reducerMock, stateMock);

in your example?

@nitoloz
Copy link

nitoloz commented Apr 12, 2017

Hi all!
Could you probably help me mocking the Store?
I'm using store in one of my services, where I use dispatch, select and get store methods.
I've mocked the Store following @noelmace suggestion and used following dispatcher, reducer and state creating the store:

export class ObservableMock implements Observer<any> {
    closed?: boolean = false; // inherited from Observer
    nextVal: any = ''; // variable I made up

    constructor() {
    }

    next = (value: any): void => {
        this.nextVal = value;
    };
    error = (err: any): void => {
        console.error(err);
    };
    complete = (): void => {
        this.closed = true;
    }
}

let _reducer: ObservableMock = new ObservableMock();
let _dispatcher: ObservableMock = new ObservableMock();
let state$: Observable<any> = new Observable<any>();

So my Mockstore class looks following:

export class MockStore<T> extends Store<T> {

    //as given above
    constructor() {
        super(_dispatcher, _reducer, state$);
    }
    //as given above
}

However when I try to define my service in test following way it says

TypeError: _store.select is not a function

This failure is caused by following line in TestedService constructor:

constructor(private _store: Store<TabStore>) {
   let tabStore: Observable<TabStore> = _store.select<TabStore>('myReducer');|
}
beforeEach(() => {
        TestBed.configureTestingModule({
            imports: [
                // StoreModule.provideStore({myReducer: myReducer}),
            ],
            providers: [
                {provide: Store, useClass: MockStore},
                {
                    provide: TestedService,
                    useFactory: (tabStore: Store<TabStore>): TestedService=> {
                        return new TestedService(myStore);
                    },
                    deps: [Store]
                }
            ]
        });
    });

Commenting imports out doesn't help either. Does anyone has ideas what's wrong with mocking?

@Tuizi
Copy link

Tuizi commented May 19, 2017

I have created a Gist to show how I test my store, I used what @noelmace published. It works well for the select, I didn't try to dispatch because we still need to know what is

super(dispatcherMock, reducerMock, stateMock);

like @crain mention

Gist: https://gist.github.com/Tuizi/e2842f6ce8cd8bec58df99ef7ec42f9a

@rupeshtiwari
Copy link

Hi all,
I wish if we have ngrx/store-testing framework which will have mocked store so that we can use it for our testing purpose.

@jecede
Copy link

jecede commented Jul 20, 2017

I did like what @crain was doing, but couldn't get it to work either. So I created an even simpler version that I can subscribe on, and feed some mockdata:

/**
 * Standard mockstore that can be used in unittests to mock a @ngrx/store
 */
export class MockStore<T> {

  reducers = new Map<string, BehaviorSubject<any>>();

  /**
   * simple solution to support selecting/subscribing to this mockstore as usual.
   * @param name reducer name
   * @returns {undefined|BehaviorSubject<any>}
   */
  select(name) {
    if (!this.reducers.has(name)) {
      this.reducers.set(name, new BehaviorSubject({}));
    }
    return this.reducers.get(name);
  }

  /**
   * used to set a fake state
   * @param reducerName name of your reducer
   * @param data the mockstate you want to have
   */
  mockState(reducerName, data) {
    this.select(reducerName).next(data);
  }

  dispatch(data: any) {
    // spy me
  }
}```

@ymjongbin
Copy link

Hi jecede,
Thanks you for your sharing for MockStore source code. It works to me and release pain full unit testing.

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

No branches or pull requests