InfoGrab DocsInfoGrab Docs

Vue 3 테스팅

요약

Vue 3으로 전환하는 과정에서 테스트가 Vue 3 모드에서 통과되는 것이 중요합니다. 현재 다음 경우에 파이프라인이 실패합니다: Vue 3 모드에서 실패하는 새 테스트 파일이 추가된 경우. 이전에는 통과하던 기존 테스트 파일이 Vue 3에서 실패하는 경우.

Vue 3으로 전환하는 과정에서 테스트가 Vue 3 모드에서 통과되는 것이 중요합니다. 올바른 Vue 3 테스팅을 강제하기 위해 파이프라인에 점진적으로 더 엄격한 검사를 추가하고 있습니다.

현재 다음 경우에 파이프라인이 실패합니다:

  • Vue 3 모드에서 실패하는 새 테스트 파일이 추가된 경우.

  • 이전에는 통과하던 기존 테스트 파일이 Vue 3에서 실패하는 경우.

  • 격리 목록에 있는 알려진 실패 테스트 중 하나가 이제 통과하는데 격리 목록에서 제거되지 않은 경우.

Vue 3로 로컬에서 단위 테스트 실행#

Vue 3을 사용하여 단위 테스트를 실행하려면, jest를 실행할 때 VUE_VERSION 환경 변수를 3으로 설정하세요.

VUE_VERSION=3 yarn jest #[file-path]

파이프라인도 단위 테스트 스위트를 실행합니다.

Vue 3로 로컬에서 시스템 테스트(spec/features) 실행#

Vue 3을 사용하여 시스템 테스트를 실행하려면 GDK가 Vue 3가 활성화된 상태로 실행 중이어야 합니다. 다음 명령어로 활성화할 수 있습니다:

gdk config set vite.vue_version 3 # 또는 vue 2로 돌아가려면 2
gdk reconfigure
gdk restart

평소처럼 rspec으로 테스트를 실행하세요.

bundle exec rspec spec/features/abuse_report_spec.rb

브라우저에서 로컬 인스턴스를 방문하면 Vue 3(@vue/compat)로도 실행됩니다. 이를 통해 Vue 3에서 기능을 수동으로 테스트할 수 있습니다.

머지 리퀘스트에서 Vue 3로 시스템 테스트 스위트 실행#

머지 리퀘스트 변경 사항이 Vue 3와 호환되는지 확인하려면, ~pipeline:run-rspec-vue3 라벨을 추가하여 파이프라인에서 시스템 테스트 스위트를 실행하세요.

rspec system pg17 vue3rspec-ee system pg17 vue3라는 이름의 RSpec job이 추가되어 일반적인 rspec system job과 병렬로 실행되지만, Vue 3 빌드를 사용합니다!

단위 테스팅 주의사항#

컴포저블 모킹 시 Ref 관리#

Vue 3 컴포저블을 테스트할 때 흔한 패턴은 해당 파일이 반환하는 ref 또는 computed 값을 모킹하는 것입니다.

다음 데모 컴포저블을 살펴보세요:

export const useCounter = () => {
  const counter = ref(1)
  const increase = () => { counter.value += 1 }

  return { counter, increase }
}

이 컴포저블을 현재 사용하고 카운터를 노출하는 컴포넌트가 있다면, 기능을 커버하는 테스트를 작성하고 싶을 것입니다. 이 간단한 예시처럼 일부 경우에는 컴포저블을 전혀 모킹하지 않아도 되지만, Tanstack Query 래퍼나 Apollo 래퍼와 같이 더 복잡한 기능의 경우에는 jest.mock을 활용하는 것이 필요할 수 있습니다.

이런 경우 테스트 파일은 컴포저블 모킹이 필요합니다:

<script setup>
const { counter, increase } = useCounter()
</script>

<template>
  <p>Super useful counter: {{ counter }}</p>
  <button @click="increase">+</button>
</template>
import { ref } from 'vue'
import { useCounter } from '~/composables/useCounter'

jest.mock('~/composables/useCounter')

describe('MyComponent', () => {
  const increaseMock = jest.fn()
  const counter = ref(1)

  beforeEach(() => {
    useCounter.mockReturnValue({
      increase: increaseMock,
      counter
    })
  })

  describe('When the counter is 2', () => {
    beforeEach(() => {
      counter.value = 2
      createComponent()
    })

    it('...', () => {})
  })

  it('should default to 1', () => {
    createComponent()

    expect(findSuperUsefulCounter().text()).toBe(1)
    // failure
  })
})

위 예시에서 컴포저블이 반환하는 함수의 목과 counter ref 모두 생성하고 있지만, 예시에서 매우 중요한 단계가 빠져 있습니다.

counter 상수는 ref이므로, 매 테스트마다 수정하면 할당한 값이 유지됩니다. 예시에서 두 번째 it 블록은 counter가 이전 테스트에서 할당된 값을 유지하기 때문에 실패할 것입니다.

해결책이자 모범 사례는 최상위 beforeEach 블록에서 항상 ref를 초기화하는 것입니다.

import { ref } from 'vue'
import { useCounter } from '~/composables/useCounter'

jest.mock('~/composables/useCounter')

describe('MyComponent', () => {
  const increaseMock = jest.fn()

  // 더 안전하게 하려면 `undefined`로 초기화할 수 있습니다
  const counter = ref(undefined)

  beforeEach(() => {
    counter.value = 1
    useCounter.mockReturnValue({
      increase: increaseMock,
      counter
    })
  })

  describe('When the counter is 2', () => {
    beforeEach(() => {
      counter.value = 2
      createComponent()
    })

    it('...', () => {})
  })

  it('should default to 1', () => {
    createComponent()

    expect(findSuperUsefulCounter().text()).toBe(1)
    // pass
  })
})

Vue 라우터#

실제(모킹되지 않은) VueRouter 객체를 사용하는 Vue Router 구성을 테스트하는 경우, 다음 가이드라인을 참고하세요. 실패의 원인 중 하나는 Vue Router 4가 라우팅을 비동기적으로 처리한다는 것이므로, 라우팅 작업이 완료될 때까지 await해야 합니다. waitForPromises 유틸리티를 사용하여 모든 프로미스가 처리될 때까지 기다릴 수 있습니다.

다음 예시에서, 테스트는 버튼 클릭 후 VueRouter가 페이지로 이동했는지 확인합니다. 버튼 클릭 후 waitForPromises를 호출하지 않으면, 라우터의 상태가 타깃 페이지로 전환되지 않아 어서션이 실패할 것입니다.

it('navigates to /create when clicking New workspace button', async () => {
  expect(findWorkspacesListPage().exists()).toBe(true);

  await findNewWorkspaceButton().trigger('click');
  await waitForPromises();

  expect(findCreateWorkspacePage().exists()).toBe(true);
});

Vue Apollo 트러블슈팅#

Apollo 뮤테이션을 실행하고 인메모리 쿼리 캐시를 업데이트하는 컴포넌트에서 단위 테스트 실패가 발생할 수 있습니다. 예를 들면:

ApolloError: 'get' on proxy: property '[property]' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '#' but got '#')

이 오류는 writeQuery 또는 updateQuery 메서드를 호출할 때 Apollo가 Vue 반응성 객체를 수정하려고 하기 때문에 발생합니다. Apollo의 캐시를 업데이트하는 작업에서 컴포넌트의 프로퍼티를 통해 전달된 객체 사용을 피하세요. 새 객체를 생성하거나 이미 Apollo의 캐시에 존재하는 데이터를 활용해야 합니다. 최후의 수단으로, cloneDeep 유틸리티를 사용하여 타깃 객체에서 Vue의 반응성 프록시를 제거하세요.

다음 예시에서, 컴포넌트는 뮤테이션이 성공한 후 두 배열 사이에서 agent 객체를 교환하여 Apollo의 인메모리 캐시를 업데이트합니다. agent 객체는 agent 프로퍼티에서도 사용할 수 있지만, 반응성 객체입니다. 잘못된 접근 방식은 컴포넌트에 프로퍼티로 전달된 agent 객체를 참조하여 프록시 오류를 유발합니다. 올바른 접근 방식은 Apollo의 캐시에 이미 저장된 agent 객체를 찾는 것입니다.

<script>
import { toRaw } from 'vue';

export default {
  props: {
    namespace: {
      type: String,
      required: true,
    },
    agent: {
      type: Object,
      required: true,
    },
  },

  methods: {
    async execute() {
      try {
        await this.$apollo.mutate({
          mutation: createClusterAgentMappingMutation,
          update(store) {
            store.updateQuery(
              {
                query: getAgentsWithAuthorizationStatusQuery,
                variables: { namespace },
              },
              (sourceData) =>
                produce(sourceData, (draftData) => {
                  const { mappedAgents, unmappedAgents } = draftData.namespace;

                  /*
                  * BAD: The error described in this section is caused by adding a Vue reactive
                  * object the nodes array. `this.agent` is a component property hence it is wrapped
                  * with a reactivity proxy.
                  */
                  mappedAgents.nodes.push(this.agent);
                  unmappedAgents.nodes = removeFrom.nodes.filter((node) => node.id !== agent.id);

                  /*
                  * PREFERRED FIX: Only use data that already exists in the in-memory cache.
                  */
                  const targetAgentIndex = removeFrom.nodes.findIndex((node) => node.id === agent.id);

                  mappedAgents.nodes.push(removeFrom.nodes[targetAgentIndex]);
                  unmappedAgents.nodes.splice(targetAgentIndex, 1);

                  /*
                  * ALTERNATIVE (LAST RESORT) FIX: Use lodash `cloneDeep` to create a clone
                  * of the object without Vue reactivity:
                  */
                  mappedAgents.nodes.push(cloneDeep(this.agent));
                  unmappedAgents.nodes = removeFrom.nodes.filter((node) => node.id !== agent.id);

                }),
            );
          },
        });
      } catch (e) {
        Sentry.captureException(e);
        this.$emit('error', e);
      }
    },
  },
};
</script>

Vue 라우터 테스팅#

전체 비모킹 vue-router@4를 테스트할 때 Vue 2와의 호환성을 위해 고려해야 할 몇 가지 주의사항이 있습니다.

Window location#

vue-router@4는 window location의 변경을 감지하지 않으므로, setWindowLocation과 같은 헬퍼로 현재 URL을 설정해도 효과가 없습니다.

대신, 초기 라우트를 설정하거나 다른 라우트로 수동으로 이동하세요.

초기 라우트#

테스트의 초기 라우트를 설정할 때, vue-router@4는 기본적으로 / 라우트로 설정됩니다. 라우터 구성에 / 경로에 대한 라우트가 정의되어 있지 않으면 테스트는 기본적으로 오류가 발생합니다. 이 경우, 컴포넌트가 생성되기 전에 정의된 라우트 중 하나로 이동하는 것이 중요합니다.

router = createRouter();

await router.push({ name: 'tab', params: { tabId }})

모든 내비게이션은 항상 비동기적이므로 await가 필요합니다.

다른 라우트로 이동#

이미 마운트된 컴포넌트에서 다른 라우트로 이동하려면, 라우터의 push 또는 replace 호출을 await해야 합니다.

createComponent()

await router.push('/different-route')

push 메서드에 접근할 수 없는 경우, 예를 들어 이벤트를 통해 컴포넌트 코드 내부에서 push를 트리거하는 경우에는 await waitForPromises로 충분합니다.

다음 컴포넌트를 살펴보세요:

<script>
export default {
  methods: {
    nextPage() {
      this.$router.push({
        path: 'some path'
      })
    }
  }
}
</script>
<template>
  <gl-keyset-pagination @push="nextPage" />
</template>

$router.push 호출이 이루어지는지 테스트하려면, gl-keyset-pagination 컴포넌트의 next 이벤트를 통해 내비게이션을 트리거해야 합니다.

wrapper.findComponent(GlKeysetNavigation).vm.$emit('push');
// $router.push is triggered in the component
await waitForPromises()

내비게이션 가드#

내비게이션 가드는 내비게이션 가드를 통과하는 어떤 경로에서도 세 번째 인수인 next를 정확히 한 번 호출해야 합니다. 이는 vue-router@3vue-router@4 모두에서 필요하지만, 모든 내비게이션이 비동기적이고 await되어야 하는 vue-router@4에서 더 중요합니다.

next를 호출하지 않으면 디버깅하기 어려운 오류가 발생할 수 있습니다. 예를 들면:

Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout

디버깅#

대부분의 경우 아래와 같이 이해하기 어려운 오류를 만나게 될 것입니다.

Unexpected calls to console (1) with:

        [1] warn: [Vue Router warn]: uncaught error during route navigation:

      23 |     .join('\n');
      24 |
    > 25 |   throw new Error(
         |         ^
      26 |     `Unexpected calls to console (${consoleCalls.length}) with:\n${consoleCallsList}\n`,
      27 |   );
      28 | };

Vue 라우터가 필요로 하는 것을 더 잘 이해하려면 jest.fn()을 사용하여 console.warn을 재정의하여 오류 출력을 볼 수 있습니다.

console.warn = jest.fn()

afterEach(() => {
  console.log(console.warn.mock.calls)
})

이렇게 하면 위의 오류를 이해하기 쉬운 형태로 변환할 수 있습니다. 머지 리퀘스트를 제출하기 전에 이 코드를 제거하는 것을 잊지 마세요.

'[Vue Router warn]: Record with path "/" is either missing a "component(s)" or "children" property.'

component 및 children 프로퍼티#

Vue 라우터 3(Vue 2)과 달리, Vue 라우터 4는 component 또는 children 프로퍼티(각각의 component와 함께)가 정의되어야 합니다. 일부 시나리오에서는 router-view 없이 라우터 쿼리 변수를 관리하기 위해 Vue 라우터를 사용한 경우가 있었습니다. 예를 들어 app/assets/javascripts/projects/your_work/components/app.vue에서처럼 말이죠.

이는 안티 패턴으로, Vue 라우터는 과도한 선택이며, 예를 들어 URL searchParams를 사용하여 바닐라 JS로 쿼리 라우트를 관리하는 것이 더 나은 접근 방식입니다.

컴포넌트 재작성이 불가능한 경우, router-view 없이 애플리케이션이 렌더링하는 App 컴포넌트를 전달하면 테스트를 통과할 수 있습니다. 그러나 이는 나중에 컴포넌트에 <router-view />가 추가될 경우 의도치 않은 동작을 도입할 가능성이 있으므로 주의해서 사용해야 합니다.

격리 목록#

scripts/frontend/quarantined_vue3_specs.txt 파일은 알려진 모든 Vue 3 실패 테스트 파일로 구성되어 있습니다. 실패하는 파이프라인으로 압도되지 않도록, 이 파일들은 Vue 3 테스트 job에서 건너뜁니다.

이 내용을 읽고 있다면, 격리 job 실패로 인해 여기로 안내받았을 것입니다. 이 job은 테스트가 통과하면 실패하고, 모두 실패하면 통과하기 때문에 혼란스럽습니다. 이는 새로 통과하는 모든 테스트를 격리 목록에서 제거해야 하기 때문입니다. 이전에 실패하던 테스트를 수정한 것을 스스로 축하하고, 이 파이프라인이 다시 통과하도록 격리 목록에서 제거하세요.

격리 목록에서 제거#

vue3 check quarantined job으로 인해 파이프라인이 실패하고 있다면, 좋은 소식입니다! 이전에 실패하던 테스트를 수정했습니다! 이제 해야 할 일은 격리 목록에서 새로 통과하는 테스트를 제거하는 것입니다. 이렇게 하면 테스트가 계속 통과하고 추가적인 회귀를 방지할 수 있습니다.

격리 목록에 추가#

하지 마세요. 이 목록은 줄어들기만 해야 하며, 커져서는 안 됩니다. 머지 리퀘스트가 새 테스트 파일을 도입하거나 현재 통과 중인 테스트를 깨뜨린다면, 수정해야 합니다.

테스트 파일을 한 위치에서 다른 위치로 이동하는 경우에는 격리 목록의 위치를 수정해도 됩니다. 단, 그전에 테스트를 먼저 수정하는 것을 고려해 보세요.

Vue 3 테스팅

GitLab v19.1
원문 보기
요약

Vue 3으로 전환하는 과정에서 테스트가 Vue 3 모드에서 통과되는 것이 중요합니다. 현재 다음 경우에 파이프라인이 실패합니다: Vue 3 모드에서 실패하는 새 테스트 파일이 추가된 경우. 이전에는 통과하던 기존 테스트 파일이 Vue 3에서 실패하는 경우.

Vue 3으로 전환하는 과정에서 테스트가 Vue 3 모드에서 통과되는 것이 중요합니다. 올바른 Vue 3 테스팅을 강제하기 위해 파이프라인에 점진적으로 더 엄격한 검사를 추가하고 있습니다.

현재 다음 경우에 파이프라인이 실패합니다:

  • Vue 3 모드에서 실패하는 새 테스트 파일이 추가된 경우.

  • 이전에는 통과하던 기존 테스트 파일이 Vue 3에서 실패하는 경우.

  • 격리 목록에 있는 알려진 실패 테스트 중 하나가 이제 통과하는데 격리 목록에서 제거되지 않은 경우.

Vue 3로 로컬에서 단위 테스트 실행#

Vue 3을 사용하여 단위 테스트를 실행하려면, jest를 실행할 때 VUE_VERSION 환경 변수를 3으로 설정하세요.

VUE_VERSION=3 yarn jest #[file-path]

파이프라인도 단위 테스트 스위트를 실행합니다.

Vue 3로 로컬에서 시스템 테스트(spec/features) 실행#

Vue 3을 사용하여 시스템 테스트를 실행하려면 GDK가 Vue 3가 활성화된 상태로 실행 중이어야 합니다. 다음 명령어로 활성화할 수 있습니다:

gdk config set vite.vue_version 3 # 또는 vue 2로 돌아가려면 2
gdk reconfigure
gdk restart

평소처럼 rspec으로 테스트를 실행하세요.

bundle exec rspec spec/features/abuse_report_spec.rb

브라우저에서 로컬 인스턴스를 방문하면 Vue 3(@vue/compat)로도 실행됩니다. 이를 통해 Vue 3에서 기능을 수동으로 테스트할 수 있습니다.

머지 리퀘스트에서 Vue 3로 시스템 테스트 스위트 실행#

머지 리퀘스트 변경 사항이 Vue 3와 호환되는지 확인하려면, ~pipeline:run-rspec-vue3 라벨을 추가하여 파이프라인에서 시스템 테스트 스위트를 실행하세요.

rspec system pg17 vue3rspec-ee system pg17 vue3라는 이름의 RSpec job이 추가되어 일반적인 rspec system job과 병렬로 실행되지만, Vue 3 빌드를 사용합니다!

단위 테스팅 주의사항#

컴포저블 모킹 시 Ref 관리#

Vue 3 컴포저블을 테스트할 때 흔한 패턴은 해당 파일이 반환하는 ref 또는 computed 값을 모킹하는 것입니다.

다음 데모 컴포저블을 살펴보세요:

export const useCounter = () => {
  const counter = ref(1)
  const increase = () => { counter.value += 1 }

  return { counter, increase }
}

이 컴포저블을 현재 사용하고 카운터를 노출하는 컴포넌트가 있다면, 기능을 커버하는 테스트를 작성하고 싶을 것입니다. 이 간단한 예시처럼 일부 경우에는 컴포저블을 전혀 모킹하지 않아도 되지만, Tanstack Query 래퍼나 Apollo 래퍼와 같이 더 복잡한 기능의 경우에는 jest.mock을 활용하는 것이 필요할 수 있습니다.

이런 경우 테스트 파일은 컴포저블 모킹이 필요합니다:

<script setup>
const { counter, increase } = useCounter()
</script>

<template>
  <p>Super useful counter: {{ counter }}</p>
  <button @click="increase">+</button>
</template>
import { ref } from 'vue'
import { useCounter } from '~/composables/useCounter'

jest.mock('~/composables/useCounter')

describe('MyComponent', () => {
  const increaseMock = jest.fn()
  const counter = ref(1)

  beforeEach(() => {
    useCounter.mockReturnValue({
      increase: increaseMock,
      counter
    })
  })

  describe('When the counter is 2', () => {
    beforeEach(() => {
      counter.value = 2
      createComponent()
    })

    it('...', () => {})
  })

  it('should default to 1', () => {
    createComponent()

    expect(findSuperUsefulCounter().text()).toBe(1)
    // failure
  })
})

위 예시에서 컴포저블이 반환하는 함수의 목과 counter ref 모두 생성하고 있지만, 예시에서 매우 중요한 단계가 빠져 있습니다.

counter 상수는 ref이므로, 매 테스트마다 수정하면 할당한 값이 유지됩니다. 예시에서 두 번째 it 블록은 counter가 이전 테스트에서 할당된 값을 유지하기 때문에 실패할 것입니다.

해결책이자 모범 사례는 최상위 beforeEach 블록에서 항상 ref를 초기화하는 것입니다.

import { ref } from 'vue'
import { useCounter } from '~/composables/useCounter'

jest.mock('~/composables/useCounter')

describe('MyComponent', () => {
  const increaseMock = jest.fn()

  // 더 안전하게 하려면 `undefined`로 초기화할 수 있습니다
  const counter = ref(undefined)

  beforeEach(() => {
    counter.value = 1
    useCounter.mockReturnValue({
      increase: increaseMock,
      counter
    })
  })

  describe('When the counter is 2', () => {
    beforeEach(() => {
      counter.value = 2
      createComponent()
    })

    it('...', () => {})
  })

  it('should default to 1', () => {
    createComponent()

    expect(findSuperUsefulCounter().text()).toBe(1)
    // pass
  })
})

Vue 라우터#

실제(모킹되지 않은) VueRouter 객체를 사용하는 Vue Router 구성을 테스트하는 경우, 다음 가이드라인을 참고하세요. 실패의 원인 중 하나는 Vue Router 4가 라우팅을 비동기적으로 처리한다는 것이므로, 라우팅 작업이 완료될 때까지 await해야 합니다. waitForPromises 유틸리티를 사용하여 모든 프로미스가 처리될 때까지 기다릴 수 있습니다.

다음 예시에서, 테스트는 버튼 클릭 후 VueRouter가 페이지로 이동했는지 확인합니다. 버튼 클릭 후 waitForPromises를 호출하지 않으면, 라우터의 상태가 타깃 페이지로 전환되지 않아 어서션이 실패할 것입니다.

it('navigates to /create when clicking New workspace button', async () => {
  expect(findWorkspacesListPage().exists()).toBe(true);

  await findNewWorkspaceButton().trigger('click');
  await waitForPromises();

  expect(findCreateWorkspacePage().exists()).toBe(true);
});

Vue Apollo 트러블슈팅#

Apollo 뮤테이션을 실행하고 인메모리 쿼리 캐시를 업데이트하는 컴포넌트에서 단위 테스트 실패가 발생할 수 있습니다. 예를 들면:

ApolloError: 'get' on proxy: property '[property]' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '#' but got '#')

이 오류는 writeQuery 또는 updateQuery 메서드를 호출할 때 Apollo가 Vue 반응성 객체를 수정하려고 하기 때문에 발생합니다. Apollo의 캐시를 업데이트하는 작업에서 컴포넌트의 프로퍼티를 통해 전달된 객체 사용을 피하세요. 새 객체를 생성하거나 이미 Apollo의 캐시에 존재하는 데이터를 활용해야 합니다. 최후의 수단으로, cloneDeep 유틸리티를 사용하여 타깃 객체에서 Vue의 반응성 프록시를 제거하세요.

다음 예시에서, 컴포넌트는 뮤테이션이 성공한 후 두 배열 사이에서 agent 객체를 교환하여 Apollo의 인메모리 캐시를 업데이트합니다. agent 객체는 agent 프로퍼티에서도 사용할 수 있지만, 반응성 객체입니다. 잘못된 접근 방식은 컴포넌트에 프로퍼티로 전달된 agent 객체를 참조하여 프록시 오류를 유발합니다. 올바른 접근 방식은 Apollo의 캐시에 이미 저장된 agent 객체를 찾는 것입니다.

<script>
import { toRaw } from 'vue';

export default {
  props: {
    namespace: {
      type: String,
      required: true,
    },
    agent: {
      type: Object,
      required: true,
    },
  },

  methods: {
    async execute() {
      try {
        await this.$apollo.mutate({
          mutation: createClusterAgentMappingMutation,
          update(store) {
            store.updateQuery(
              {
                query: getAgentsWithAuthorizationStatusQuery,
                variables: { namespace },
              },
              (sourceData) =>
                produce(sourceData, (draftData) => {
                  const { mappedAgents, unmappedAgents } = draftData.namespace;

                  /*
                  * BAD: The error described in this section is caused by adding a Vue reactive
                  * object the nodes array. `this.agent` is a component property hence it is wrapped
                  * with a reactivity proxy.
                  */
                  mappedAgents.nodes.push(this.agent);
                  unmappedAgents.nodes = removeFrom.nodes.filter((node) => node.id !== agent.id);

                  /*
                  * PREFERRED FIX: Only use data that already exists in the in-memory cache.
                  */
                  const targetAgentIndex = removeFrom.nodes.findIndex((node) => node.id === agent.id);

                  mappedAgents.nodes.push(removeFrom.nodes[targetAgentIndex]);
                  unmappedAgents.nodes.splice(targetAgentIndex, 1);

                  /*
                  * ALTERNATIVE (LAST RESORT) FIX: Use lodash `cloneDeep` to create a clone
                  * of the object without Vue reactivity:
                  */
                  mappedAgents.nodes.push(cloneDeep(this.agent));
                  unmappedAgents.nodes = removeFrom.nodes.filter((node) => node.id !== agent.id);

                }),
            );
          },
        });
      } catch (e) {
        Sentry.captureException(e);
        this.$emit('error', e);
      }
    },
  },
};
</script>

Vue 라우터 테스팅#

전체 비모킹 vue-router@4를 테스트할 때 Vue 2와의 호환성을 위해 고려해야 할 몇 가지 주의사항이 있습니다.

Window location#

vue-router@4는 window location의 변경을 감지하지 않으므로, setWindowLocation과 같은 헬퍼로 현재 URL을 설정해도 효과가 없습니다.

대신, 초기 라우트를 설정하거나 다른 라우트로 수동으로 이동하세요.

초기 라우트#

테스트의 초기 라우트를 설정할 때, vue-router@4는 기본적으로 / 라우트로 설정됩니다. 라우터 구성에 / 경로에 대한 라우트가 정의되어 있지 않으면 테스트는 기본적으로 오류가 발생합니다. 이 경우, 컴포넌트가 생성되기 전에 정의된 라우트 중 하나로 이동하는 것이 중요합니다.

router = createRouter();

await router.push({ name: 'tab', params: { tabId }})

모든 내비게이션은 항상 비동기적이므로 await가 필요합니다.

다른 라우트로 이동#

이미 마운트된 컴포넌트에서 다른 라우트로 이동하려면, 라우터의 push 또는 replace 호출을 await해야 합니다.

createComponent()

await router.push('/different-route')

push 메서드에 접근할 수 없는 경우, 예를 들어 이벤트를 통해 컴포넌트 코드 내부에서 push를 트리거하는 경우에는 await waitForPromises로 충분합니다.

다음 컴포넌트를 살펴보세요:

<script>
export default {
  methods: {
    nextPage() {
      this.$router.push({
        path: 'some path'
      })
    }
  }
}
</script>
<template>
  <gl-keyset-pagination @push="nextPage" />
</template>

$router.push 호출이 이루어지는지 테스트하려면, gl-keyset-pagination 컴포넌트의 next 이벤트를 통해 내비게이션을 트리거해야 합니다.

wrapper.findComponent(GlKeysetNavigation).vm.$emit('push');
// $router.push is triggered in the component
await waitForPromises()

내비게이션 가드#

내비게이션 가드는 내비게이션 가드를 통과하는 어떤 경로에서도 세 번째 인수인 next를 정확히 한 번 호출해야 합니다. 이는 vue-router@3vue-router@4 모두에서 필요하지만, 모든 내비게이션이 비동기적이고 await되어야 하는 vue-router@4에서 더 중요합니다.

next를 호출하지 않으면 디버깅하기 어려운 오류가 발생할 수 있습니다. 예를 들면:

Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout

디버깅#

대부분의 경우 아래와 같이 이해하기 어려운 오류를 만나게 될 것입니다.

Unexpected calls to console (1) with:

        [1] warn: [Vue Router warn]: uncaught error during route navigation:

      23 |     .join('\n');
      24 |
    > 25 |   throw new Error(
         |         ^
      26 |     `Unexpected calls to console (${consoleCalls.length}) with:\n${consoleCallsList}\n`,
      27 |   );
      28 | };

Vue 라우터가 필요로 하는 것을 더 잘 이해하려면 jest.fn()을 사용하여 console.warn을 재정의하여 오류 출력을 볼 수 있습니다.

console.warn = jest.fn()

afterEach(() => {
  console.log(console.warn.mock.calls)
})

이렇게 하면 위의 오류를 이해하기 쉬운 형태로 변환할 수 있습니다. 머지 리퀘스트를 제출하기 전에 이 코드를 제거하는 것을 잊지 마세요.

'[Vue Router warn]: Record with path "/" is either missing a "component(s)" or "children" property.'

component 및 children 프로퍼티#

Vue 라우터 3(Vue 2)과 달리, Vue 라우터 4는 component 또는 children 프로퍼티(각각의 component와 함께)가 정의되어야 합니다. 일부 시나리오에서는 router-view 없이 라우터 쿼리 변수를 관리하기 위해 Vue 라우터를 사용한 경우가 있었습니다. 예를 들어 app/assets/javascripts/projects/your_work/components/app.vue에서처럼 말이죠.

이는 안티 패턴으로, Vue 라우터는 과도한 선택이며, 예를 들어 URL searchParams를 사용하여 바닐라 JS로 쿼리 라우트를 관리하는 것이 더 나은 접근 방식입니다.

컴포넌트 재작성이 불가능한 경우, router-view 없이 애플리케이션이 렌더링하는 App 컴포넌트를 전달하면 테스트를 통과할 수 있습니다. 그러나 이는 나중에 컴포넌트에 <router-view />가 추가될 경우 의도치 않은 동작을 도입할 가능성이 있으므로 주의해서 사용해야 합니다.

격리 목록#

scripts/frontend/quarantined_vue3_specs.txt 파일은 알려진 모든 Vue 3 실패 테스트 파일로 구성되어 있습니다. 실패하는 파이프라인으로 압도되지 않도록, 이 파일들은 Vue 3 테스트 job에서 건너뜁니다.

이 내용을 읽고 있다면, 격리 job 실패로 인해 여기로 안내받았을 것입니다. 이 job은 테스트가 통과하면 실패하고, 모두 실패하면 통과하기 때문에 혼란스럽습니다. 이는 새로 통과하는 모든 테스트를 격리 목록에서 제거해야 하기 때문입니다. 이전에 실패하던 테스트를 수정한 것을 스스로 축하하고, 이 파이프라인이 다시 통과하도록 격리 목록에서 제거하세요.

격리 목록에서 제거#

vue3 check quarantined job으로 인해 파이프라인이 실패하고 있다면, 좋은 소식입니다! 이전에 실패하던 테스트를 수정했습니다! 이제 해야 할 일은 격리 목록에서 새로 통과하는 테스트를 제거하는 것입니다. 이렇게 하면 테스트가 계속 통과하고 추가적인 회귀를 방지할 수 있습니다.

격리 목록에 추가#

하지 마세요. 이 목록은 줄어들기만 해야 하며, 커져서는 안 됩니다. 머지 리퀘스트가 새 테스트 파일을 도입하거나 현재 통과 중인 테스트를 깨뜨린다면, 수정해야 합니다.

테스트 파일을 한 위치에서 다른 위치로 이동하는 경우에는 격리 목록의 위치를 수정해도 됩니다. 단, 그전에 테스트를 먼저 수정하는 것을 고려해 보세요.