Skip to main content

Migration Vue 2 zu Vue 3 mit Composition API, Typescript und script setup: Herausforderung wrapper.setData() in Unit Tests

Bei der Migration von Vue 2 zu Vue 3 traf ich in einem Projekt auf eine unerwartete Verhaltensänderung in Vue, die zu fehlschlagenden Unittests führte.

In einem unserer Projekte migrieren wir nach der Scout-Regel Schritt für Schritt die Vue 2 Komponenten zu Vue 3 mit script setup und Typescript. Dabei müssen gelegentlich auch die Unit Tests angepasst werden.

Die Migration ist meist unkompliziert. In einem Unit Test wurde allerdings wrapper.setData() aufgerufen. Das funktioniert so mit der Composition API nicht mehr.

Die zu migrierende Vue 2 Komponente enthielt diesen Code:

data() {
  return {
    isDirt: false
  }
}

Der Unit Test dazu enthielt diesen Test:

test (`saving changes allowed`, async () => {
  await wrapper.setData ({isDirty: true})
  expect (…)...
}

Die Herausforderungen bei der Migration hier sind:

  • wrapper.setData() funktioniert auf script setup Komponenten nicht mehr.
  • console.log(wrapper.vm) zeigt keine Refs und Funktionen auf. Die IDE erkennt auch nichts in der Code Completion und stellt den Aufruf sogar als Warning dar.

Die Lösung:

Der migrierte Code der Komponente mit Vue 3 und script setup

const isDirty = ref(false)
function foo() {}

Im migrierten Unit Test kann man über vm auf die Ref zugreifen:

test.each(allowedRoles)(`saving changes allowed for %s`, async (role) => {
  wrapper.vm.isDirty = true
  await nextTick()
})

Aha-Momente hierbei:

  • Obwohl isDirty nun eine Ref ist, darf man im Unit Test nicht wrapper.vm.isDirty.value schreiben.
  • Eine Ref wird von der IDE erkannt, wenn sie da steht aber nicht in der Code Completion. (Es gibt keinen Warning marker, und nachdem sie geschrieben ist, wird der Typ im Popup korrekt angezeigt.)
  • Eine Funktion, bspw. wrapper.vm.foo() wird nie von der IDE erkannt und erhält einen Warning Marker “unknown function”. Aber: Führt man den Test aus, wird die Funktion korrekt aufgerufen. Der nächste Punkt trifft dann trotzdem zu.
  • Ohne await nextTick() funktioniert der Vue 3 Test Code trotzdem nicht. Das ist das Ungewöhnlichste an dieser Migration. Testet man eine Vue 2 Komponente braucht man das nextTick() nicht. Die Änderung des internen States der data Properties wirkt sich sofort im Template Code aus. Beim Verwenden von Refs verhält sich Vue anders.
  • defineExpose() hat keinen Einfluss auf console.log(wrapper.vm) oder die IDE Code Completion in Unit Tests.
  • Ruft man eine Funktion auf, funktioniert zwar der Unit Test, aber der Build schlägt fehl, da der vue-tsc Befehl die Funktion nicht findet und den Build abbricht. Da der problematische Code im Unit Test liegt, kann man dem vue-tsc über eine Konfigurationsdatei mitteilen, Unit Tests beim Build zu ignorieren:
tsconfig.build-dts.json neben tsconfig.json
{
  „extends“: „,./tsconfig.json“,
  „exclude“: [
    „**/*.spec.ts“,
    „**/*.test.ts“
  ]
}

Update 2025

Diesen Unittest würde ich heute nicht mehr durchgehen lassen, da er auf internen State zugreift. Einer meiner Kollegen fragte mich letztens, was für mich denn der Unterschied zwischen Unittest und Component Test ist, da in meinem Projekt keine “Component-Tests” enthalten sind, sondern nur Unittests.

Im Vue-Kontext ist eine Component eine SFC (Single-File-Component). Diese betrachte ich als eine Unit. Gute Component- bzw. Unit-Tests betrachten die Unit als Blackbox und testen erwartete Ausgaben (Output) für die gegebenen Eingaben (Input).

Bei einer SFC gibt es diese Eingabemöglichkeiten (Input):

  • Props
  • Benutzer Interaktion über den Template Code. Das schließt auch Events gestubbten (!!!) aus Child-Components ein. Wie man Events aus gestubbten Child-Components im Unittest generieren kann zeige ich in diesem Snippet: Vue Composition API: Unit testing custom events of stubbed child components
  • Änderungen des Store-States, die via computed Properties die Ausgabe der SFC dynamisch ändern
  • API-Zugriffe (wobei ich das für Bad-Practice halte, wenn diese in SFCs direkt erfolgen)

Ein SFC kann diese Ausgaben (Output) haben:

  • Events
  • DOM-Struktur-Änderungen via v-if, v-else-if oder v-else (bspw. Validierungsmeldungen)
  • CSS Änderungen (bspw. enabled/disabled State)

Die genannten Inputs können über Vitest sehr leicht bereitgestellt oder gemockt werden, sodass ein Zugriff auf den internen State, wie er anfangs in diesem Artikel beschrieben ist, gar nicht braucht. Ein Unittest, der eine interne Ref testet beweist letztendlich gar nicht, dass die SFC auf die gegebene Eingabe die korrekte Ausgabe liefert. Tatsächlich sollte der Unittest umgeschrieben werden, um auf die erwartete DOM-Struktur oder CSS Klassen zu prüfen.

Robert Wloch

I am a software architect (SA) with more than 15 years of professional experience in multiple programming languages, development processes and software sizes from small prototypes to green field projects to 20 years old cloud shops. Having worked in various segments with many teams I have become very strong at creating technical documentation of software, onboarding and mentoring my teams to refactor and write code that has less cognitive complexity, is better testable and pushing my teams towards the practices of continuous delivery and increased software quality. Apart from my technical expertise I have developed excellent people skills and are an exceptionally empathic team player. To me, software development is not an individual achievement but a team effort.