Open bethcarslaw opened 5 years ago
Did you ever figure anything out? The syntax/usage of dynamic modules is terrific, but there is 0 info on how to test (not even tests in the "real world" examples)
@pjo336 I didn't. I was able to get my store running inside of my tests so I could commit dummy data to the mutations. This isn't ideal and it'd be much better to be able to stub the store methods completely. I also had to use nock to mock the endpoints being called by my actions.
If you commit dummy data like @dalecarslaw does, it remains, even if test case is finished. So. you must clear data manually in like beforeEach function.
Any updates on this? running into the same issue and I would rather not use store functionality to test my component behavior...
I got mine to work by using store.hotUpdate()
to "swap" the store in following tests.
Can you show an example test? Never even had heard of that method
Here is an example of using store.hotUpdate()
:
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex, { Store, ActionTree } from 'vuex';
import { getModule } from 'vuex-module-decorators';
import FooModule from '@/foo-module.ts';
import FooComponent from '@/components/FooComponent.vue';
describe('FooComponent', () => {
const localVue = createLocalVue();
localVue.use(Vuex);
let actions: ActionTree<ThisType<any>, any>;
let store: Store<unknown>;
beforeEach(() => {
actions = {
setName: jest.fn(),
};
store = new Vuex.Store({
modules: {
fooModule: FooModule,
},
});
getModule(FooModule, store); // required
});
it('`setName` getter was called', () => {
store.hotUpdate({
modules: {
fooModule: {
namespaced: true,
actions,
},
},
});
const wrapper = shallowMount(FooComponent, {
localVue,
});
const button = wrapper.find('button');
button.trigger('click');
expect(actions.setName).toHaveBeenCalled();
});
});
sorry for the delay! been a bit busy :/
so lets assume you use getModule(FooModule, this.$store)
from inside a component computed property like
get fooModule() {
return getModule(FooModule, this.$store)
}
then all you have to do is override fooModule
in your computed attributes when creating your wrapper, i.e.
const wrapper = shallowMount(FooComponent, {
computed: {
fooModule: () => ({}),
},
localVue,
});
You can also return your own mock version of the store if you like. personally I don't like testing store logic in my components so I override it with an empty object, and mock the other getters that I use to access the store getters, and methods used to access the store mutations and actions.
The alternative to this pattern is let's say you have a singleton:
// foo.ts
export default getModule(Foo, store);
...which you call in your component:
import Foo from './foo';
export default class Component extends Vue {
doSomething() {
let someArg;
// Complex logic
Foo.doSomething(someArg);
}
}
...then in tests you could stub the singleton:
describe('Component', () => {
let component;
beforeEach(() => {
sinon.stub(Foo, 'doSomething').callsFake(() => {});
component = shallowMount(Component);
});
afterEach(() => {
sinon.restore();
});
it('does something complex', () => {
component.vm.doSomething();
expect(Foo.doSomething).to.have.been.calledWith('abc');
});
});
@alecgibson I was very excited to test your proposal, since that is exactly my pattern. But sadly this didnot work for me :( sinon.stub(..) is not stubbing/mocking my StoreModule and the test is still suing the original StoreModule :(
@jubairsaidi I dont have the computed property in my mock options... :(
@souphuhn I've since changed away from this pattern. We're now adding the store singleton to the Vue prototype:
Vue.prototype.$foo = getModule(Foo, store);
...which means that in the components, you can just mock it using mocks:
const component = shallowMount(Component, {
$foo: {doSomething: sinon.spy()},
});
@alecgibson Thank you so much for the fast reply. Firstly, your approach of
Vue.prototype.$foo = getModule(Foo, store)
seems nice. I've adopted my code for this and it works great.
Secondly I am still struggling of mocking this global property this.$foo
:
I have added the mock to shallowMount mock options as
const mocks = { $foo: myMockedFoo };
const component = shallowMount(Component, {mocks});
My component is accessing someData
of FooModule
like
get store() : FooModule {
return this.$foo;
get someData (): any {
return this.store.someData;
}
In this way, my component is accessing correctly someData
when I ran the test without my mockedModule inside shallowMount. But when I ran my test with my mocked module, someData
is suddenly undefined
now. Something is still wrong of my mockedModule I guess. Or something else.. :(
I used the module syntax
const myMockedFoo = {
state: {
someData: 'mockedValue'
}
}
Shouldn't you have:
const myMockedFoo = {
someData: 'mockedValue',
};
?
Pretty sure this would become obvious if you inspect the runtime value of this.$foo
inside your component.
@alecgibson Thank you! It works like a charm
Oh man, I love this pattern, I was using
export const overviewModule = getModule(OverviewModule);
which made my code untestable.
Now I'm using:
Vue.prototype.$overview = getModule(OverviewModule);
And it works great.
My tests are working with this pattern, but something is still not correct the way I register the store, so that when I serve my application I get that $overview is undefined, here is my store index.ts
:
import Vue from "vue";
import Vuex from "vuex";
import { OverviewState } from "./overview-module";
Vue.use(Vuex);
export interface RootState {
overview: OverviewState;
}
// Declare empty store first, dynamically register all modules later.
export default new Vuex.Store<RootState>({});
Here is my module:
export interface OverviewState {
items: GlossaryEntry[];
filter: OverviewFilterOptions;
}
@Module({ dynamic: true, store, name: "overview" })
export class OverviewModule extends VuexModule implements OverviewState {
public items: GlossaryEntry[] = [];
public filter: OverviewFilterOptions = {
contains: "",
page: 0,
pageSize: 20,
desc: false
};
@Mutation
private async UPDATE_OVERVIEW(filter: OverviewFilterOptions) {
this.filter = Object.assign({}, filter);
await overviewService
.filter(this.filter)
.then((response: Response<Overview>) => {
this.items = Object.assign({}, response.data.items);
});
}
@Action
public async updateOverview(filter: OverviewFilterOptions) {
this.UPDATE_OVERVIEW(filter);
}
}
Vue.prototype.$overview = getModule(OverviewModule);
Could please someone help?
@dgroh it looks like you have a circular dependency here? You import store
into OverviewModule
, but store
itself also has a dependency on OverviewModule
.
I suspect if you remove the RootState
interface (and its dependency on OverviewState
, everything should work? Also, why do you even need RootState
? Isn't the whole point of this library/pattern that you can access the store through these type-safe modules anyway?
This was a good point. It "works" now, but when I use this.$overview
in one specific component, my entire application breaks. I don't get why.
private updateOverview(value: string) {
this.$overview.updateOverview({ contains: value }); // this breaks my entire app
}
This private updateOverview
is an event, it only gets invoked on button click. So I don't understand why commenting out it brings everything to work again.
I use this.$overview.updateOverview
in other components, too and it works, but only when I use in this specific one everything breaks. I assume this is something related with the app hooks.
Is there still not a better solution or any documentation related to this ? We're nearing 2021 and this issue is so far still the best source of documentation I can find on testing dynamic Vuex modules.
What works best for me:
Assuming you have a dynamic module declared as follows:
// @/store/modules/my-module.ts
import { getModule, Module, VuexModule } from "vuex-module-decorators";
import store from "@/store";
@Module({ name: "myModule", store, dynamic: true})
export class MyModule extends VuexModule {
// state, getters, mutations, actions
}
export const myModule = getModule(MyModule);
Importing this module in a component as follows
// @/components/MyComponent.vue
import { myModule } from "@/store/modules/my-module";
In a test I set up mocks as following
// @/components/__tests__/MyComponent.spec.ts
import { mocked } from "ts-jest";
import { myModule } from "@/store/modules/my-module";
jest.mock("@/store/modules/my-module")
// Provide mock typing. This does seem to wrongly assume that getters have also been mocked by jest.mock, but it does work nicely for actions and mutations
const myModuleMock = mocked(myModule);
// All mutations and actions are now mocked with default `jest.fn` (which just returns `undefined`)
// Mocking state (you should probably only access state through getters though)
myModuleMock.someStateProp = //Whatever I want to mock as a value
// Mocking a getter, Jest does not mock getters. @ts-expect-error required for suppresing TS2540 (can't assign to read-only prop)
// @ts-expect-error
myModuleMock.someGetter = //whatever I want to mock as a value
// Mocking a getter that returns a function
// @ts-expect-error
myModuleMock.someGetter =() => { /* whatever I want to mock as a return value */ }
// Mocking a mutation for test suite
myModuleMock.someMutation.mockImplementation(() => /* Some implementation */)
// Mocking a mutation with a specific implementation in one test
myModuleMock.someMutation.mockImplementationOnce(() => /* Some implementation */)
// Mocking an action for test suite, best to use promises as actions are async
myModuleMock.someAction.mockImplementation(() => Promise.resolve())
// Mocking an action with a specific implementation in one test
myModuleMock.someAction.mockImplementationOnce(() => Promise.resolve())
@Robin-Hoodie I tried to use this way but the getter keeps returning undefined to the component and then it gives an error because it tries to access a getter property and the getter is undefined.
@Robin-Hoodie Thank you it's been a week i've been struggling with this lib! Weirdly, I didn't have any issue on mocking my getter. But, I can't change the value once the component mounted (but it kind be ok for unit testing)
Also, for people in future reading this and wanting to make an app with this lib and having call to actions/mutations/getters in other modules, don't try to do it with a static modules, it doesn't work. Make your store with dynamic modules and test it the way Robin Hoodie does it.
Also, here are some interesting links that helped me: A better doc for this lib A review of other libs wich gave me some example
How would you go about stubbing actions and other methods inside of your store when using the getModule functionality inside of a component for testing?
MyComponent.vue
ExampleStore.ts
Test
In the above example I would need to stub the fetchSomeData action