How to test Vue watcher that watches a computed pr

2020-07-02 11:57发布

问题:

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

回答1:

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


回答2:

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.



回答3:

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.