Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
bethcarslaw opened this issue Apr 4, 2019 · 22 comments
Open

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

bethcarslaw opened this issue Apr 4, 2019 · 22 comments

Comments

@bethcarslaw
Copy link

bethcarslaw commented Apr 4, 2019

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

@pjo336
Copy link

pjo336 commented Apr 26, 2019

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)

@bethcarslaw
Copy link
Author

bethcarslaw commented Apr 26, 2019

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

@gring2
Copy link

gring2 commented Jun 19, 2019

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.

@ziazon
Copy link

ziazon commented Aug 2, 2019

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

@ziazon
Copy link

ziazon commented Aug 20, 2019

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

@pjo336
Copy link

pjo336 commented Aug 20, 2019

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

@Javalbert
Copy link

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

@ziazon
Copy link

ziazon commented Sep 21, 2019

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.

@alecgibson
Copy link

alecgibson commented Nov 21, 2019

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

@souphuhn
Copy link

souphuhn commented Apr 2, 2020

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

@alecgibson
Copy link

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

@souphuhn
Copy link

souphuhn commented Apr 2, 2020

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

@alecgibson
Copy link

alecgibson commented Apr 2, 2020

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.

@souphuhn
Copy link

souphuhn commented Apr 3, 2020

@alecgibson Thank you! It works like a charm

@dgroh
Copy link

dgroh commented May 20, 2020

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
Copy link

dgroh commented May 20, 2020

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?

@alecgibson
Copy link

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

@dgroh
Copy link

dgroh commented May 26, 2020

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

@Robin-Hoodie
Copy link

Robin-Hoodie commented Nov 11, 2020

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
Copy link

Robin-Hoodie commented Jan 29, 2021

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

@itspauloroberto
Copy link

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

@FlorentinBurgeat
Copy link

@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

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

No branches or pull requests