How to mock vuex functions with no direct access? - vue.js

So I have run into quite a test problem.
To Summarize:
A while back I created a private npm Module, that manages and create a vuex store and injects it as a module into the main vuex store.
Now I want to mock a specific function of that vuex module in my test but that doesn't work.
I tried this:
store.getters.getSomething = jest.fn(() => "stuff);
But that results in this error:
TypeError: Cannot set property getSomething of #<Object> which has only a getter
Now did a little digging, and I here you can see where this getter comes from:
First there is a simple vuex module definition:
//vuexModule this is not accessible outside of the npm module
getters:{
getSomething: (state)=>{ return "stuff"}
}
export { getters }
Now this is imported into this class:
//storeProvider //this is exposed by the npm package
import vuexModule from "./vuexModule";
export default class StoreProvider {
constructor(vuexStore, vuexModuleName = "someStore") {
this.vuexStore = vuexStore;
this.vuexStore.registerModule(vuexModuleName, VuexConfigStore);
}
So this class will generate a vuex module using the previously defined module.
Now the whole thing is added to the main vuex store in the actual application.
Note: this code is now the actual application and not in the npm package.
import StoreProvider from "someNpmModule";
import mainStore from "vuexPlugin";
const storeProvider = new StoreProvider(store);
//later in main:
new Vue({ mainStore })
So the question is: How can I mock getSomething?
I know the code is quite abstracted, but I can't show all the code, and its probably too much for stackoverflow.
The main point is, that I can just import the getter and mock it like this:
import {getSomething} from "...";
getSomething = jest.fn(() => "stuff);
//Or this:
store.getters.getSomething = jest.fn(() => "stuff);
I found this stackoverflow question: TypeError during Jest's spyOn: Cannot set property getRequest of #<Object> which has only a getter
But as the npm package is rather big I can't just mock the whole package for the unit tests, as it provides some necessary pre-conditions. I tried genMockFromModule(), but I still need to target this specific function.
And as mentioned I can't simply import the function.
Any ideas?

Related

An updated approach to testing vuex actions

In the "Testing Actions" documentation, there is a recommendation to use inject-loader in order to mock entire module imports, and their respective dependencies, when unit testing.
For example, you can mock the ../api/shop import found inside the ./actions file with the following:
const actionsInjector = require('inject-loader!./actions')
// create the module with our mocks
const actions = actionsInjector({
'../api/shop': {
getProducts (cb) {
setTimeout(() => {
cb([ /* mocked response */ ])
}, 100)
}
}
})
Unfortunately, that package does not support Webpack 5.
Is there an updated approach, or alternative package, that is recommended?
Thanks #Estus Flask.
The clarification/mixup was that, whilst I was using vi.mock, and it's auto-mocking algorithm :
"...will mock the module itself by invoking it and mocking every
export."
i.e. it still runs the import statements. To prevent this, a factory must be provided (see documentation), e.g.:
vi.mock('module', <factory-here>)
Which then replaces the entire file import.

How to use Pinia inside of an npm package?

I have similar issue as mentioned here, but with Pinia in my case. It's much harder to get Pinia to work outside of Vue components, because of "Uncaught Error: [🍍]: getActivePinia was called with no active Pinia. Did you forget to install pinia?", but in this case it is even harder.
Not the cleanest solution, because it requires to have Pinia installed and initialized in the project where your package will be used, but if you're doing this for internal use it is totally okay.
So, in my package it looks like this:
install(app, options = {}) {
greetings()
const { $pinia } = options
if (!$pinia) {
throw new Error(`No active Pinia instance was passed to your package`)
}
let core_store = useCoreStore($pinia)
// moar code
And in other project:
import MyPackage from '#rusinas/my-package'
app.use(ModernEditor)
const pinia = createPinia()
app.use(MyPackage, {
$pinia: pinia
})
app.use(pinia)
app.mount('#app')
I noticed that in SPA mode you may not need to provide active pinia to your package, it could figure it out itself, you just need to make sure to app.use(pinia) before you initialize you package. But this doesn't work in Nuxt SSR mode, so yeah, this workaround required :(
I think we should raise this question in Pinia's repository. I don't see why it have to work this way. Cases where you need stores outside of setup() are so often and even crucial sometimes for applications, so it should be much easier.
P.S.
Also, keep in mind the possibility of store names collisions. Names should be unique across entire application
P.P.S.
SSR solution:
plugins/MyPackage.plugin.js:
import { defineNuxtPlugin } from '#app'
import MyPackage from '#rusinas/my-package'
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.use(MyPackage, {
$pinia: nuxtApp.$pinia,
})
})

How to access Vuex from Vue Plugin?

How can I access my store from my plugin? Console returns undefined.
import store from './store';
export default {
install(vue, opts){
Vue.myGlobalFunction = function(){
console.log(store);
}
}
}
I recently had to do this too to make a pouchDb plugin, and came up with a new way.
When you create your first Vue object, you can do this.
import PouchDb from '#/pouch_db/PouchDbPlugin'
let DefaultVue = Vue.extend({
components: {App},
store,
created () {
Vue.use(PouchDb, this.$store) // Create it by passing in the store you want to use
}
})
My plugin adds an additional store, and it's own mutations and getters.
export default {
install (Vue, store) {
store.registerModule('PouchDb', pds)
const pouchDb = new PouchDb(store)
Vue.pouchDb = pouchDb
Vue.prototype.$pouchDb = pouchDb
}
}
Inside the constructor, I store the store
class PouchDb {
constructor (store) {
this.store = store
// ... etc.
}
// ... more functions
}
And then use it in other functions
class PouchDb {
// ... constructor and other functions
async addSync (docId) {
this.store.dispatch('PouchDb/addSync', docId)
}
}
It's a bit of a cheat to pass in the store, but seems to work nicely. It's usable throughout the app like this
// Inside vuex store
Vue.pouchDb.addSync(// ...etc)
// inside component
this.$pouchDb.removeSync(// ...etc)
See official guide here where it states
A Vue.js plugin should expose an install method. The method will be called with the Vue constructor as the first argument, along with possible options:
So you can do this, very easily.
Vue.use( {
install(Vue){
Vue.prototype.$something = function (){
this.$store...etc
}
}
} )
To use, simply do this.$something() in a components methods/computed etc, or directly in the component markup as {{$something()}}
This will remove the plugin needing to know where the store actually resides, while still allowing you to utilize the store within the plugin.
This is because it will inherit the scope of whatever component utilizes it, thus providing access to all of the components instance properties, including things like $store, $router as well any of it's local properties such as computed properties, parents etc. Essentially the plugin functions as if it is directly a part of the component (eg if you used it as a mixin).
For Vue 3
Incase if you wonder, how to do it in Vue 3, You can use the following.
plugin.js
export default {
install(app) { // app instance
console.log(app.config.globalProperties.$store)
}
}
main.js
import store from './pathtostore'
import plugin from './plugin'
createApp(...).use(store).use(plugin)
When app starts, you import your store and "append" it to Vue, globally.
Now, if you use() your plugin, the first parameter of install() is always Vue itself, and in this moment Vue already has access to the store, in the install method you can simply start
install(vue, opts) {
... here your can acces to vue.$store ....
}

Access Vue.js plugin from main.js

I'm creating a plugin and I just wonder why I can't access it in main.js file. Here's how Auth.js looks like:
const Auth = {
install(Vue) {
Vue.prototype.$isGuest = function () {
console.log('This user is a guest.');
}
Vue.prototype.$getAuthToken = function () {
console.log('Auth token will be returned.');
}
}
}
export default Auth
This is main.js:
import Auth from '#/helper/Auth'
Vue.use(Auth)
However, when I execute console.log(this.$isGuest()), it doesn't work. It actually returns the following:
main.js?1c90:25 Uncaught TypeError: this.$isGuest is not a function
The problem is that this method works when I call it in components such as Dashboard.vue and things like that.
I have a way to avoid calling isGuest method within main.js (I can call it in Layout.vue), but I'm more curious why it doesn't work in main.js.
Maybe because Vue hasn't been initialized yet, but even if I put the console.log() line at the end of the file, still doesn't work.
Thanks,
N.
If you are calling this.$isGuest() outside of Vue, you will get the error you describe. That's because this is not a Vue object. What this is depends on how you are building your code, but given you are using import it's probably the module.
Also, you are adding $isGuest to the prototype of Vue. That means that the function is only going to be available on actual instances of Vue objects. That is why it is available in your components.
If you want to use it in the main script, the only place you will be able to get to it is inside the Vue object in a lifecycle handler, method, or computed. For example:
new Vue({
mounted(){
console.log(this.$isGuest()) // this should work
}
})

Testing: mocking node-fetch dependency that it is used in a class method

I have the following situation:
A.js
import fetch from 'node-fetch'
import httpClient from './myClient/httpClient'
export default class{
async init(){
const response = await fetch('some_url')
return httpClient.init(response.payload)
}
}
A_spec.js
import test from 'ava'
import sinon from 'sinon'
import fetch from 'node-fetch'
import httpClient from './myClient/httpClient'
import A from './src/A'
test('a async test', async (t) => {
const instance = new A()
const stubbedHttpInit = sinon.stub(httpClient, 'init')
sinon.stub(fetch).returns(Promise.resolve({payload: 'data'})) //this doesn't work
await instance.init()
t.true(stubbedHttpInit.init.calledWith('data'))
})
My idea it's check if the httpClient's init method has been called using the payload obtained in a fetch request.
My question is: How I can mock the fetch dependency for stub the returned value when i test the A's init method?
Finally I resolved this problem stubbing the fetch.Promise reference like this:
sinon.stub(fetch, 'Promise').returns(Promise.resolve(responseObject))
the explanation for this it's that node-fetch have a reference to the native Promise and when you call fetch(), this method returns a fetch.Promise. Check this out
You can stub fetch() as decribed in the manual
import sinon from 'sinon'
import * as fetchModule from 'node-fetch'
import { Response } from 'node-fetch'
// ...
const stub = sinon.stub(fetchModule, 'default')
stub.returns(new Promise((resolve) => resolve(new Response(undefined, { status: 401 }))))
// ...
stub.restore()
Note, that fetch() is the default export from node-fetch so you neeed to stub default.
sinon.stub(fetch) can't stub a function itself. Instead you need to stub the node-fetch dependency from inside ./src/A, perhaps using something like proxyquire:
import proxyquire from 'proxyquire`
const A = proxyquire('./src/A', {
'node-fetch': sinon.stub().returns(Promise.resolve({payload: 'data'}))
})
#mrtnlrsn's answer does not work for files generated from TypeScript in NodeJS, because default is a generated property, each module which imports such a dependency has its own, so stubbing one does not affect others.
However, NodeJS gives access to imported modules, so stubbing works this way:
const nodeFetchPath = require.resolve("node-fetch");
const nodeFetchModule = require.cache[nodeFetchPath];
assert(nodeFetchModule);
const stubNodeFetch = sinon.stub(nodeFetchModule, "exports");
Make sure you use the same node-fetch module as the tested module, e.g. using yarn dedupe. Or build your own nodeFetchPath.
In my case, I found it useful to preserve the node-fetch functionality since my mock responses were already being supplied via nock
To accomplish this, I proxyquire'd the dependency as described in the answer above but wrapped a require call in a spy:
import proxyquire from 'proxyquire'
import { spy } from 'sinon'
const fetchSpy = spy(require('node-fetch'))
const moduleA = proxyquire(
'./moduleA',
{ 'node-fetch': fetchSpy }
)
...
expect(fetchSpy.args).toBe(...)