26

Suppose I have the following component:

import { mapState } from 'vuex';
import externalDependency from '...';

export default {
  name: 'Foo',
  computed: {
    ...mapState(['bar'])
  },
  watch: {
    bar () {
     externalDependency.doThing(this.bar);
    }
  }
}

When testing, I want to ensure that externalDependency.doThing() is called with bar (which comes from the vuex state) like so:

it('should call externalDependency.doThing with bar', () => {
  const wrapper = mount(Foo);
  const spy = jest.spyOn(externalDependency, 'doThing');

  wrapper.setComputed({bar: 'baz'});

  expect(spy).toHaveBeenCalledWith('baz');
});

Vue test-utils has a setComputed method which allows me to currently test it, but I keep getting warnings that setComputed will be removed soon, and I don't know how else this can be tested:

https://github.com/vuejs/vue-test-utils/issues/331

Daniel Richter
  • 340
  • 1
  • 5
  • 25
yxu296
  • 379
  • 1
  • 6
  • 13
  • 1
    If you commit a change to Vuex, the computed property will update via Vue. I do not know if the watch will trigger, but setting the Vuex state rather than changing the computed directly seems like it should work. – clay Mar 22 '18 at 16:14
  • 1
    but that goes against the spirit of a unit test--- it becomes more of an e2e test. You shouldn't have to mock out vueX functionality just when testing a VueX connected component – yxu296 Mar 22 '18 at 18:42
  • You should try reading this: https://vue-test-utils.vuejs.org/guides/using-with-vuex.html – lucas Aug 02 '19 at 09:29
  • 2
    Given Vuex's `mapState` is an input to your component, that's what you should be mocking. Using `setComputed` does not test your app correctly – Phil Sep 02 '19 at 05:42
  • 1
    there's nothing wrong with importing/mocking the VueX store for unit tests. normally you'd mock them in `beforeEach()`, so your `it()` blocks are lean. as your component grows, you'll be doing `dispatch` and watching the state itself possibly, so it'll become very awkward mutating/setting those attributes for every test. – ierdna Nov 26 '19 at 11:14

4 Answers4

10

From you're trying to achieve

When testing, I want to ensure that externalDependency.doThing() is called with bar (which comes from the vuex state) like so:

(and this is indeed pure unit test approach), you can just force change of this watcher, which basically is a function. There's no need to track if watcher is changing in case of computed or data value change - let Vue handle it. So, to change a watcher in a mounted Vue instance, just call it like

wrapper.vm.$options.watch.bar.call(wrapper.vm)

Where bar is name of your watcher. This way you will be able to test exact functionality that you're aiming to test.

Idea taken from this comment https://github.com/vuejs/vue-test-utils/issues/331#issuecomment-382037200, on a vue-test-utils issue, mentioned by you in a question.

Andrew Miroshnichenko
  • 2,015
  • 15
  • 21
  • 1
    thank you for the idea and reference. been searching for vue3/ts compatible means to test the watchers. its the same technique as a computed property in vue3 for others looking for a straight TS answer: `;(MyComponent.watch!.appLoaded as Function).call(localThis, moreArgs, etc)` – steven87vt Jun 28 '22 at 20:46
2

The Vue Test Utils documentation points at a different approach where you use a very simple Vuex store:

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'

// use a localVue to prevent vuex state from polluting the global Vue instance
const localVue = createLocalVue();
localVue.use(Vuex);

describe('Foo.vue', () => {
  let state;
  let store;

  beforeEach(() => {
    // create a new store for each test to prevent pollution
    state = { bar: 'bar' };
    store = new Vuex.Store({ state });
  })

  it('should call externalDependency.doThing with bar', () => 
  {
    shallowMount(MyComponent, { store, localVue });
    const spy = jest.spyOn(externalDependency, 'doThing');
    // trigger the watch
    state.bar = 'baz';
    expect(spy).toHaveBeenCalledWith('baz');
  });
})
flup
  • 26,937
  • 7
  • 52
  • 74
0

You will need some sort of mutator on the VueX instance, yes this does introduce another unrelated unit to the test but personally by your test including the use of Vuex, that concept has already been broken.

Modifying the state in an unexpected way is more prone to cause behaviour that differs from the actual usage.

rosscooper
  • 1,976
  • 3
  • 16
  • 29
  • Hi rosscooper and everyone else. I face a similar issue, and I have quite a complex setup of different components, that communicate via vuex. One component watches, like the title says a state property (in my case the return value of a vuex getter). But I don't want to just test if the watcher function is called, I want to ensure that the component's UI shows the correct state, after a watcher has been triggered with a specific value. Very simplified example: I have a vuex getter `currentPosition` and if they return an empty object, I want to show an additional UI element. – Merc Nov 18 '19 at 15:37
0

You can set the value straight at the source, i.e. VueX. so you'd have something like this in your store.js:

const state = {
  bar: 'foo',
};
const mutations = {
  SET_BAR: (currentState, payload) => {
    currentState.bar = payload;
  },
};
const actions = {
  setBar: ({ commit }, payload) => {
    commit('SET_BAR', payload);
  },
};

export const mainStore = {
  state,
  mutations,
  actions,
};

export default new Vuex.Store(mainStore);

and then in your component.spec.js you'd do this:

import { mainStore } from '../store';
import Vuex from 'vuex';

//... describe, and other setup functions
it('should call externalDependency.doThing with bar', async () => {
  const localState = {
    bar: 'foo',
  };
  const localStore = new Vuex.Store({
      ...mainStore,
      state: localState,
  });
  const wrapper = mount(Foo, {
    store: localStore,
  });
  const spy = jest.spyOn(externalDependency, 'doThing');
  localStore.state.bar = 'baz';
  await wrapper.vm.$nextTick();
  expect(spy).toHaveBeenCalledWith('baz');
});

You can also call the dispatch('setBar', 'baz') method on the store and have the mutation occur properly instead of directly setting the state.

NB It's important to re-initialize your state for every mount (i.e. either make a clone or re-declare it). Otherwise one tests can change the state and the next test will start with that dirty state, even if wrapper was destroyed.

ierdna
  • 5,753
  • 7
  • 50
  • 84