obliviousfella
obliviousfella

Reputation: 445

How to test nuxt.js asyncData and fetch hooks

I've been trying to test files where I am using nuxt's (asyncData and fetch hooks) , I have no problem testing vue.js normal lifecycle but I noticed that vue/test-utils doesn't give clear instructions on how to test nuxt's hooks.

login.vue

asyncData() {
 const email = localStorage.getItem("email") || ""
 const password = localStorage.getItem("password") || ""
 return { email, password }
},
mounted() {
 this.setMaxStep()
}

signup.vue

async fetch({ store, redirect, query }) {
  const res = await store.dispatch("getSavedFormData")
  if (res) {
    store.dispatch("setNotification", {
      message: "Previous application is loaded"
    })
  }
},

tried testing it like the following but I get no luck(tried other various things too but I don't know where to look for information)


import {
  shallowMount,
  config
} from "@vue/test-utils"
import Login from "../../../pages/login

describe("Login", () => {

  let wrapper

  beforeEach(() => {
    wrapper = shallowMount(Login)
  })

  it("gets asyncData", async () => {
    await wrapper.vm.asyncData
  })
})

Upvotes: 3

Views: 6779

Answers (3)

Sergiu Mare
Sergiu Mare

Reputation: 1724

Context: Vue is initialized client-side. Nuxt is initialized server-side. This means that every lifecycle hook from Nuxt, like asyncData or fetch, will NOT BE TRIGGERED by default with Vue test utils.

Inspiration: https://murani.nl/blog/2021-12-21/how-to-test-nuxtjs-asyncdata-and-fetch-hooks/

UNIT TEST: To test them you have to trigger them yourself. Remember that this hook will just be a normal function, at this point. In order for the test to work, you have to mock every function which asyncData/fetch is triggering. Afterward, you have to check that the mocked functions are called.

Extract the logic from inside the asyncData/Fetch hook into a function. That way it's easier to be tested, and your code is more solid and secure, when you do unit tests.

My file The bellow file it's a mixin which is attached on all of my nuxt pages.

import { mapGetters, mapActions, mapMutations } from "vuex";
import { retrievePageData, retrieveJwt } from "@/utils/client/helpers";

function strapiVue(pageEndPoint, pageName, objProperty) {
  return {
    computed: {
      ...mapGetters("jwt", ["getJwt"])
    },
    methods: {
      ...mapActions("jwt", ["setJwt"]),
      ...mapMutations("error", ["SET_ERROR"])
    },
    async asyncData({ store }) {
      const jwtToken = await retrieveJwt(store.state.jwt.jwt);
      if (Object.keys(store.state.pages[pageName]).length === 0) {
        return retrievePageData(pageEndPoint, jwtToken, objProperty);
      }
    },
    created() {
      if (this.error) {
        this.SET_ERROR(this.error);
        this.$router.push({ name: "error" });
      }
    },
    mounted() {
      if (this.getJwt === "") this.setJwt(this.jwt);
    }
  };
}

export default strapiVue;

My unit test file:

import strapiVue from "@/mixins/strapiVue";
import { shallowMount, createLocalVue } from "@vue/test-utils";
import * as helpers from "@/utils/client/helpers";
import Vuex from "vuex";
import { makeErrorObj } from "@/service/error";

// eslint-disable-next-line no-import-assign
helpers.retrieveJwt = jest.fn(() => "jwt");
// eslint-disable-next-line no-import-assign
helpers.retrievePageData = jest.fn(() => ({
  contactPageStrapiData: {},
  jwt: "jwt"
}));

const localVue = createLocalVue();
localVue.use(Vuex);

describe("strapiVue", () => {
  let modules;
  let store;
  let mocks;
  let wrapperOptions;
  let wrapper;

  const Component = {
    render() {},
    mixins: [strapiVue("/contact", "contactPage", "contactPageStrapiData")],
    asyncData: strapiVue("/contact", "contactPage", "contactPageStrapiData")
      .asyncData
  };

  beforeEach(() => {
    modules = {
      jwt: {
        state: {
          jwt: "jwt"
        },
        getters: {
          getJwt: jest.fn(() => "")
        },
        actions: {
          setJwt: jest.fn()
        },
        namespaced: true
      },
      error: {
        mutations: {
          SET_ERROR: jest.fn()
        },
        namespaced: true
      },
      pages: {
        state: {
          contactPage: []
        },
        namespaced: true
      }
    };
    store = new Vuex.Store({ modules });
    mocks = {
      $router: {
        push: jest.fn()
      }
    };
    wrapperOptions = {
      localVue,
      store,
      mocks,
      data() {
        return {
          error: makeErrorObj(),
          jwt: "jwt"
        };
      }
    };
  });

  afterEach(() => {
    jest.clearAllMocks();
    jest.restoreAllMocks();
  });

  describe("life cycle hooks", () => {
    describe("mounted", () => {
      it("should be defined", () => {
        wrapper = shallowMount(Component, wrapperOptions);
        expect(wrapper).toBeDefined();
      });

      it("should set the jwt token, if it hasn't been set already", () => {
        wrapper = shallowMount(Component, wrapperOptions);
        expect(modules.jwt.actions.setJwt).toHaveBeenCalledWith(
          expect.any(Object),
          "jwt"
        );
      });

      it("should not set the jwt token, if it has been initially set", () => {
        modules.jwt.getters.getJwt = jest.fn(() => "mockJwt");
        wrapper = shallowMount(Component, {
          ...wrapperOptions,
          store: new Vuex.Store({ modules })
        });
        expect(modules.jwt.actions.setJwt).not.toHaveBeenCalled();
      });
    });

    describe("created", () => {
      describe("an error is present", () => {
        it("should call SET_ERROR with a payload", () => {
          wrapper = shallowMount(Component, wrapperOptions);
          expect(modules.error.mutations.SET_ERROR).toHaveBeenCalledWith(
            expect.any(Object),
            {
              error: {
                appCode: "G00000",
                message: "This is generic error.",
                status: null,
                type: "Generic Error"
              }
            }
          );
        });

        it("should trigger a router push", () => {
          wrapper = shallowMount(Component, wrapperOptions);
          expect(mocks.$router.push).toHaveBeenCalledWith({
            name: "error"
          });
        });
      });

      describe("no error", () => {
        it("should not call SET_ERROR", () => {
          wrapper = shallowMount(Component, {
            ...wrapperOptions,
            data() {
              return {};
            }
          });
          expect(modules.error.mutations.SET_ERROR).not.toHaveBeenCalled();
        });

        it("should trigger a router push", () => {
          wrapper = shallowMount(Component, {
            ...wrapperOptions,
            data() {
              return {};
            }
          });
          expect(mocks.$router.push).not.toHaveBeenCalled();
        });
      });
    });

    describe("asyncData", () => {
      const context = {
        store: {
          state: {
            jwt: {
              jwt: "jwt"
            },
            pages: {
              contactPage: []
            }
          }
        }
      };

      it("should call retrieveJwt", async () => {
        wrapper = shallowMount(Component, {
          ...wrapperOptions,
          data() {
            return {};
          }
        });
        await Component.asyncData(context);
        expect(helpers.retrieveJwt).toHaveBeenCalledWith("jwt");
      });

      it("should call retrievePageData if store module pages is empty", async () => {
        wrapper = shallowMount(Component, {
          ...wrapperOptions,
          data() {
            return {};
          }
        });
        await Component.asyncData(context);
        expect(helpers.retrievePageData).toHaveBeenCalledWith(
          "/contact",
          "jwt",
          "contactPageStrapiData"
        );
      });

      it("should return retrievePageData if store module pages is empty", async () => {
        wrapper = shallowMount(Component, {
          ...wrapperOptions,
          data() {
            return {};
          }
        });
        const result = await Component.asyncData(context);
        expect(result).toMatchObject({ contactPageStrapiData: {}, jwt: "jwt" });
      });

      it("should not call retrievePageData if store module pages is not empty", async () => {
        modules.pages.state.contactPage = [{}];
        wrapper = shallowMount(Component, {
          ...wrapperOptions,
          store: new Vuex.Store({ modules }),
          data() {
            return {};
          }
        });
        await Component.asyncData(context);
        expect(helpers.retrievePageData).not.toHaveBeenCalledWith();
      });
    });
  });
});

E2E: In order to test that the hook is called you need to do an end-to-end test. This way you can be 100% sure that the hook is called because the data is presented on the screen.

Upvotes: 1

Gyen Abubakar's answer can be a solution if you want to test only the behavior of the component with a mocked data. But keep in mind that the asyncData and fetch hook are not tested, and you may need to test them for a better unit test.

If you want to test your asyncData and fetch hook, you need to add this after mounting the component:

  1. AsyncData

    wrapper = shallowMount(Login);
    wrapper.setData({
      ...(await wrapper.vm.$options.asyncData({ store })) // add more context here
    });
    
  2. Fetch Hook

    wrapper = shallowMount(Login);
    await Login.fetch.call(wrapper.vm); // using `call` to inject `this`
    

Upvotes: 3

Gyen Abubakar
Gyen Abubakar

Reputation: 21

Late answer but I hope this helps someone new to this question.

What I did to solve this issue when I faced it, was simply adding the data() method when mounting (or shallow-mounting) the component:

// import shallowMount & component/page to be tested

const wrapper = shallowMount(Component, {
   data() {
      // assuming username data is the data needed by component
      username: 'john.doe'
   }
})

Since asyncData() runs on the server, our test fails because asyncData() never runs and hence, the data expected from it is never gotten.

So, it makes sense to provide a client-side data using the data() method so the component/page has the necessary data at the time the test is ran.

Upvotes: 2

Related Questions