Writing tests with Jest and Typescript - Part 7


Jest VueJS Typescript stutorial

Other articles in the series

  • Making our UI user friendly - Part 15
  • Deploying Phoenix VueJS application using Docker - Part 14
  • Fixing failing Elixir tests - Part 13
  • Adding new features to Order Management - Part 12
  • Add Order Management - Part 11
  • Refactoring and adding tests for Phoenix - Part 10
  • Refactoring VueJS with Typescript- Part 9
  • Add Customer Management - Part 8
  • Writing tests with Jest and Typescript - Part 7
  • Adding Vuex to Vue using Typescript - Part 6
  • Building our Homepage view component - Part 5
  • Add Multi-language support to Vue Typescript - Part 4
  • Generate Vue Typescript Application - Part 3
  • Setting up Models with Ecto and Adding Routing . Part 2
  • Setting up Your Phoenix Application - Part 1
  • Tutorial Series for building a VueJS (Typescript) and Phoenix(Elixir) Shop Management Application - Part 0

  • The front-end related code for this post is available at here.

    Testing Our Components

    We will be using jest and Vue Test Utils to test our .vue components. Since we are just starting and since the amount of logic is minimal, it a good time to write some tests. Since we are using vue-cli all the configurations are already done for us. Vue-cli even provides us with a simple test case for both unit and e2e testing, in our project setup. In this post, we will concentrate on unit tests. So lets get started.

    Vue Test Utils is the official Vue unit testing library. Vue Test Utils mounts a Vue component, mocks necessary inputs and returns a Wrapper for the component. Wrapper as docs put it is an object that contains a mounted component or vnode and methods to test the component or vnode. It provides many useful helper functions for making testing easier.

    Lets look at our example.spec.js test in tests/unit directory. If there is a .spec or .test name in filename jest automatically detects it and runs the tests in there.

    // shallowMount only renders the chosen component and avoid rendering its child components.
    import { shallowMount } from '@vue/test-utils';
    import HelloWorld from '@/components/HelloWorld.vue';
    
    // describe can group a suite of tests under a name (here it is HelloWorld.vue).
    // The second argument is the function which is our test case. 
    describe('HelloWorld.vue', () => {
      it('renders props.msg when passed', () => {
        const msg = 'new message';
        const wrapper = shallowMount(HelloWorld, {
          propsData: { msg },
        });
        // expect is used to assert that our values match.
        // wrapper.text() return text content of wrapper.
        expect(wrapper.text()).toMatch(msg);
      });
    });

    We can run the unit tests using

    npm run test:unit

    Now we got a basic idea on how to write a test case, lets write a simple one for our AddProduct.vue, which tests if our input fields are sent correctly when we click the submit button. The implementation is as given below.

    import 'jest';
    import { mount } from '@vue/test-utils';
    import AddProduct from '@/components/product/AddProduct.vue';
    import { Product } from '@/types/types';
    import products from '@/store/modules/products';
    
    // We mock the whole store/modules/products module, with jest.
    jest.mock('@/store/modules/products', () => ({
      service: {
        // jest.fn creates mock function which replaces actually implementation of function.
        // It captures all calls to function with arguments and more.
        createProduct: jest.fn(() => async (p: Product) => {
        }),
        getEmpty: () => {
          return {};
        },
      },
    }));
    
    describe('AddProduct.vue', () => {
      test('Checks if the product is sent correctly when clicking submit button', async () => {
        // mount, mounts the component AddProduct in isolation and returns a wrapper with helper functions.
        const wrapper = mount(AddProduct, {
          mocks: {
            // Here we pass a mock for global function $t. $t is the translation function from vue-i18n
            $t: () => { },
          },
        });
        // Finds the element with id productName
        const inputV = wrapper.find('#productName');
        // We manually set the value of input field to testNamer
        inputV.setValue('testNamer');
        // And we trigger a click event to check if required functions are getting called.
        wrapper.find('[type=\'submit\']').trigger('click');
    
        // We need to cast to HTMLInputElement, because Typescript by default provides a generic HTMLElement,
        // which lacks some function from an input field.
        const t = inputV.element as HTMLInputElement;
        // Check if the value we set before clicking submit is sent to createProduct function.
        expect(products.service.createProduct).toBeCalledWith({ name: 'testNamer' });
      });
    });

    Adding Notifications

    Now we will add notifications when Products are created or updated and write a few tests for those.

    <template>
      <div class="container">
        <div v-if="this.showNotification" class="notification is-primary">{{$t('productCreated.label')}}</div>
        <form>
          <ProductView :currentProduct="this.currentProduct">
            <input class="button is-black" value="Send" type="submit" v-on:click.prevent="onSubmit" />
          </ProductView>
        </form>
      </div>
    </template>
    
    <script lang='ts'>
    import Vue from 'vue';
    import { Product } from '@/types/types.ts';
    import ProductView from '@/components/product/ProductView.vue';
    import { Component, Prop } from 'vue-property-decorator';
    import products from '@/store/modules/products';
    import { setTimeout } from 'timers';
    
    @Component({
      components: {
        ProductView,
      },
    })
    export default class AddProduct extends Vue {
      private currentProduct: Product = products.service.getEmpty();
      private showNotification = false;
    
      public async onSubmit() {
        const response = await products.service.createProduct(this.currentProduct);
    
        // If status is 201, which stands for content created we show a notification
        if (response.status == 201) {
          this.showNotification = true;
          // Hide notification after 3 seconds
          setTimeout(() => {
            this.showNotification = false;
          }, 3000);
        }
      }
    }
    </script>
    
    <i18n>
    {
      "de": {
        "productCreated": {
          "label": "Produkt erstellt"
        }
      },
      "en": {
        "productCreated": {
          "label": "Product created"
        }
      }
    }
    </i18n>

    Here we just added a few translations and added a new showNotification variable, which keeps tracks of whether we should display a notification. We also turn off the notification after 3 seconds using setTimeout function. As logic doesn’t require more explanation, lets write a test which checks if notification is displayed when showNotification is true.

      test('Check if product created notification displayed when showNotification is true', async () => {
        // mount, mounts the component AddProduct in isolation and returns a wrapper with helper functions.
        const wrapper = mount(AddProduct, {
          mocks: {
            // Here we pass a mock for global function $t. $t is the translation function from vue-i18n
            $t: () => { }
          },
        });
        // We set showNotification to true and wait for Vue to update DOM by waiting for vm.nextTick
        wrapper.vm.$data.showNotification = true;
        await wrapper.vm.$nextTick();
        // Search for element with class=notification
        const t = wrapper.find('.notification');
        // Assert the element is visible.
        expect(t.isVisible()).toBe(true)
      })

    Thats all for this post. In next post, we will start implementing our customer management section.