vuex-module-decorators icon indicating copy to clipboard operation
vuex-module-decorators copied to clipboard

How to test components using getModule with vue-test-util

Open bethcarslaw opened this issue 5 years ago • 22 comments

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

<template>
  <div>
    {{ this.exampleStore.someData  }}
    <button v-on:click="handleClick()">Do An Action</button>
    <LoadingIcon
      :v-if="this.exampleStore.isLoading"
    ></LoadingIcon>
  </div>
</template>

<script lang="ts">
// imports.....

@Component({
  components: {
    LoadingIcon
  }
})
export default class MyComponent extends Vue {
  private exampleStore: ExampleStore = getModule(ExampleStore)
  
  private created() {
   this.exampleStore.fetchSomeData()
  }

  private handleClick() {
    this.exampleStore.fetchSomeData()
  }
}
</script>

ExampleStore.ts

// imports...
@Module({ dynamic: true, namespaced: true, store, name: 'exampleStore' })
export default class ExampleStore extends VuexModule {
  public someData: any = ''

  @Action({ commit: 'someMutation')}
  public async fetchSomeData() {
  // async stuff

   return data
  }

  @Mutation
  public someMutation(payload: any) {
    return this.someData = payload
  }
}

Test

const localVue = createLocalVue()
localVue.use(Vuex)
let store: Vuex.Store<any>

beforeEach(() => {
  store = new Vuex.Store({})
})

describe('test component', () => {
  it('should have the correct html structure', () => {
    const component = shallowMount(MyComponent, {
     store, localVue
    } as any)
    expect(component).toMatchSnapshot()
  })
})

In the above example I would need to stub the fetchSomeData action

bethcarslaw avatar Apr 04 '19 15:04 bethcarslaw

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 avatar Apr 26 '19 16:04 pjo336

@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.

bethcarslaw avatar Apr 26 '19 17:04 bethcarslaw

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.

gring2 avatar Jun 19 '19 07:06 gring2

Any updates on this? running into the same issue and I would rather not use store functionality to test my component behavior...

ziazon avatar Aug 02 '19 23:08 ziazon

I got mine to work by using store.hotUpdate() to "swap" the store in following tests.

ziazon avatar Aug 20 '19 21:08 ziazon

Can you show an example test? Never even had heard of that method

pjo336 avatar Aug 20 '19 21:08 pjo336

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();
  });
});

Javalbert avatar Sep 08 '19 00:09 Javalbert

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.

ziazon avatar Sep 21 '19 05:09 ziazon

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 avatar Nov 21 '19 19:11 alecgibson

@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 avatar Apr 02 '20 08:04 souphuhn

@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 avatar Apr 02 '20 08:04 alecgibson

@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'
  }
}

souphuhn avatar Apr 02 '20 14:04 souphuhn

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 avatar Apr 02 '20 15:04 alecgibson

@alecgibson Thank you! It works like a charm

souphuhn avatar Apr 03 '20 06:04 souphuhn

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.

dgroh avatar May 20 '20 05:05 dgroh

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 avatar May 20 '20 05:05 dgroh

@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?

alecgibson avatar May 20 '20 07:05 alecgibson

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.

image

dgroh avatar May 26 '20 06:05 dgroh

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.

Robin-Hoodie avatar Nov 11 '20 13:11 Robin-Hoodie

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 avatar Jan 29 '21 07:01 Robin-Hoodie

@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.

itspauloroberto avatar Apr 08 '21 01:04 itspauloroberto

@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

FlorentinBurgeat avatar Sep 16 '21 13:09 FlorentinBurgeat