component test 問題集2(Vue2 + TS + Jest+ vue-test-utils)


Posted by TempuraEngineer on 2022-03-26

1. 為什麼彈窗叫不出來?(有用BootstrapVue b-modal)

因為b-modal沒有設置static props為true,所以不會render。不render就不存在於DOM,自然叫不出來

Modals can be rendered in-place in the document (i.e. where the component is placed in the document) by setting the static prop to true. Note that the content of the modal will be rendered in the DOM even if the modal is not visible/shown when static is true.

b-modal

import { mount, createLocalVue, Wrapper } from '@vue/test-utils';
import { BootstrapVue } from 'bootstrap-vue';
const localVue = createLocalVue();
localVue.use(BootstrapVue);

import MyModal from '../../src/components/MyModal.vue';

const wrapper: Wrapper<MyModal & { [key: string]: any }> = mount(MyModal, {
    localVue,
    propsData:{
        title:'this is new title',
        static:true
    },
    scopedSlots:{
        default:'<h1>this is modal content</h1>'
    }
})

describe('test MyModal', () => {
    beforeAll(() => {
        wrapper.vm.$bvModal.show('my-modal');
    })

    it('invoke $bvModal.show to open modal', () => {
        expect(wrapper.vm.title).toBe('this is new title');
        expect(wrapper.find('h1').text()).toBe('this is modal content');
        expect(wrapper.find('#my-modal').exists()).toBeTruthy();
    })
})



再用v-if和v-show看一個有無render的例子

// 這是組件和wrapper
const testComponent = {
    template:`
        <div>
            <p v-if="show" class="useIf-first">use v-if 123</p>
            <p v-else class="useIf-second">use v-if 456</p>

            <p v-show="show" class="useShow">use v-show</p>

            <button @click="toggle">toggle</button>
        </div>
    `,
    data(){
        return{
            show:false,
        }
    },
    methods:{
        toggle(){
            testComponent.data().show = !testComponent.data().show;
        }
    }
}

const testWrapper:Wrapper<Vue & {[key:string]:any}> = mount(testComponent, {
    localVue,
})
describe('test MyModal', () => {
    it('when show is false, .useShow-first should exist but not show', () => {
        expect(testWrapper.vm.show).toBeFalsy();

        expect(testWrapper.find('.useShow-first').exists()).toBeTruthy(); 
        expect(testWrapper.find('.useShow-first').isVisible()).toBeFalsy(); 
    })

    it('when show is false, .useIf-first should not exist', () => {
        expect(testWrapper.vm.show).toBeFalsy();

        expect(testWrapper.find('.useIf-first').exists()).toBeFalsy();

        // [vue-test-utils]: find did not return .useIf-first, cannot call isVisible() on empty Wrapper
        expect(testWrapper.find('.useIf-first').isVisible()).toBeFalsy(); 
    })    

    it('when show is false, .useIf-second should exist', () => {
        expect(testWrapper.vm.show).toBeFalsy();

        expect(testWrapper.find('.useIf-second').exists()).toBeTruthy(); 
        expect(testWrapper.find('.useIf-second').isVisible()).toBeTruthy(); 
    })       
})

然後試著把彈窗打開

describe('test MyModal', () => {
    beforeAll(() => {
        // 因為toggle操作到的不是wrapper的show,所以下面會failed
        testWrapper.vm.toggle();
    })

    it('when show is true, .useShow-first should exist', () => {
        // expect(received).toBeTruthy()
        // Received: false
        expect(testWrapper.vm.show).toBeTruthy();

        expect(testWrapper.find('.useShow-first').exists()).toBeTruthy(); 

        // Received: false,所以會failed
        expect(testWrapper.find('.useShow-first').isVisible()).toBeTruthy(); 
    })
}

為了操作wrapper的show,所以需要在wrapper加上mocks,裡面放toggle的mock,
記得把testComponent的methods去掉,不然因為同名,還是會叫到非mock的toggle

不用試圖用setMethods()蓋過去,因為已經被棄用了

const testWrapper:Wrapper<Vue & {[key:string]:any}> = mount(testComponent, {
    localVue,
    mocks:{
        toggle:jest.fn(() => {
            testWrapper.vm.show = !testWrapper.vm.show;
        })
    }
})
describe('test MyModal', () => {
    beforeAll(() => {
        testWrapper.vm.toggle();
    })

    it('when show is true, useIf-first should exist.', () => {
        expect(testWrapper.vm.show).toBeTruthy();

        expect(testWrapper.find('.useIf-first').exists()).toBeTruthy();
        expect(testWrapper.find('.useIf-first').isVisible()).toBeTruthy();
    })

    it('when show is true, useShow-first should exist.', () => {
        expect(testWrapper.vm.show).toBeTruthy();

        expect(testWrapper.find('.useShow-first').exists()).toBeTruthy();
        expect(testWrapper.find('.useShow-first').isVisible()).toBeTruthy();
    })

    it('when show is true, useIf-second should not exist.', () => {
        expect(testWrapper.vm.show).toBeTruthy();

        expect(testWrapper.find('.useIf-second').exists()).toBeFalsy();

        // [vue-test-utils]: find did not return .useIf-second, cannot call isVisible() on empty Wrapper
        expect(testWrapper.find('.useIf-second').isVisible()).toBeFalsy();
    })    
})


2. 要怎麼讓SUT在產品跑真的依賴,測試時則跑假的依賴?

使用依賴注入

what:依賴注入(Dependency Injection),一種IoC(控制反轉Inversion of Control)的設計模式

The basic idea of the Dependency Injection is to have a separate object, an assembler, that populates a field in the lister class with an appropriate implementation for the finder interface

when:用於服務層抽換撰寫測試時的接縫(轉換真假DOC)

how:在class之外的地方建立依賴(depedent object/DOC),然後在注入給需要該DOC的class。通常不直接注入一個instance,因為這會失去了DI保持彈性的效果。不過如果是通用的class還是可以注入instance

why:可解決兩個類別間耦合性過高的問題

  • 注入通用instance的例子
    先來看DI的部分

      // App.ts,從DI取出DOC然後export的檔案
    
      import * as DI from './DI';
    
      class App {
          get auth() {
              return DI.get('Authorization');
          }
          get order() {
              return DI.get('Order');
          }
      }
    
      export default new App();
    
      // DI.ts,進行依賴注入的檔案
      // Authorization.ts、Order.ts內容不重要,總之就是一個class
    
      import Authorization from "./Authorization";
      import Order from './Order';
    
      const instancesPool: { [key: string]: any } = {}
    
      const lazyInstancesFactory: { [key: string]: any } = {
          Authorization: () => Authorization,
          Order: () => Order,
      }
    
      export const get = (name: string): any => {
          if (name in instancesPool) {
              return instancesPool[name];
          }
    
          if (!(name in lazyInstancesFactory)) {
              throw new Error('instance can not be set');
          }
          const classConstructor = lazyInstancesFactory[name]();
          const instance = new classConstructor();
    
          instancesPool[name] = instance;
          return instance;
      }
    
      export const set = <T>(name: string, instance: any): T => {
          instancesPool[name] = instance;
          return instance;
      }
    

    再來看要使用DOC的組件

      <template>
          <div class="container">
              帳號:<input type="text" placeholder="請輸入電話號碼" v-model="account">
              密碼:<input type="password" v-model="password">
              <span v-if="errorMsg">{{errorMsg}}</span>
    
              <button @click="login">login</button>
          </div>
      </template>
    
      <script lang="ts">
          import { Component, Vue } from 'vue-property-decorator';
    
          import App from '@/sdk/App';
    
          @Component
          export default class Login extends Vue {
              // data
              account = '';
              password = '';  
              errorMsg = '';  
    
              // methods
              async login():Promise<void>{
                  if(this.account.trim() && this.password.trim()){
                      try{
                          await App.auth.loginWithPhoneAndPassword(this.account, this.password);
    
                      this.$router.push('/home');
                      }catch(e:any){
                          this.errorMsg = `登入失敗 ${e.message}`;
                      }
                  }else{
                      this.errorMsg = '請輸入帳號和密碼';
                  }
              }
          }
      </script>
    

    再來看測試的部分

      import { shallowMount } from '@vue/test-utils'
    
      import {auth, order} from '../mocks'; // 這是mocks.ts裡面裝了假的DOC
      import * as DI from '../../src/sdk/DI';
    
      // 用set()將假的DOC新增到pool裡,跑測試時App會透過get()去挖DOC出來,
      // 因為有先新增到pool,所以挖到的會是假的DOC,這樣就可以達到轉換真假DOC的效果
    
      DI.set('Authorization', auth); 
      DI.set('Order', order);
    
      import Login from '@/views/Login.vue';
    
      const $router = {
          currentRoute:{
              fullPath:'/'
          },
          params: {},
          query: {},
          push:(url:string) => {
              wrapper.vm.$router.currentRoute.fullPath = url;
          }
       }
    
      const wrapper = shallowMount(Login, {
          mocks:{
              $router,
          },
          data(){
              return{
                  account:'0912345678',
                  password:'abcdef'
              }
          }
      });
    
      describe('test Login', () => {
          it('enter account and password, and click login. then page will be push to Home', () => {
              try{
                  wrapper.find('button').trigger('click');
              }finally{
                  expect(wrapper.vm.$router.currentRoute.fullPath).toBe('/home');
              }
          })
      })
    
  • 不直接注入instance的例子(此例為建構式注入)

          async getUserById(id:string):Promise<User>{
              try {
                  const res = await fetch(`https://api.github.com/users/${id}}`, {
                      method:'GET'
                  })
    
                 const data = await res.json();
    
                  return new User({
                      id:data.id,
                      name:data.name,
                      location:data.location,
                      url:data.url
                  } as any);
              }
              catch (e:any) {
                  throw new Error(`get user fail: ${e.message}`);
              }        
         }
    
      class User {
          _id: any;
          data: userData;
    
          constructor(data:userData & {id:string}) {
              this._id = data.id;
              this.data = data;
          }
    
          async update(data:userData):Promise<void> {
              // omit
          }
      }
    



📖
Dependency Injection
使用依賴注入來解除強耦合吧
Inversion of Control and Dependency Injection


#depency injection #DI #依賴注入 #component test







Related Posts

[學學ReactNative] DAY1 - 前言與動機:為何選擇 React Native?

[學學ReactNative] DAY1 - 前言與動機:為何選擇 React Native?

每日心得筆記 2020-07-10(五)

每日心得筆記 2020-07-10(五)

滲透測試基本技術 第三章 (002)

滲透測試基本技術 第三章 (002)


Comments