Gunnar Bittersmann: Wenn zwei (Validator und Unit-Test) sich streiten, ärgert sich der dritte (ich)

In einer Vue-Komponente SchnickSchnackSchnuck steht u.a.

<script type="text/babel">

  function isValidSymbol(value) {
    return ['Schere', 'Stein', 'Papier'].includes(value);
  }

  export default {
    name: 'SchnickSchnackSchnuck',
    props: {
      symbol: {
        type: String,
        required: true,
        validator: isValidSymbol,
      },
    },
  };

</script>

In der Komponente ist implementiert, was sie bei 'Schere', 'Stein' und 'Papier' jeweils machen soll; aber auch, wie sie mit ungültigen Werten umgehen soll (wenn sie bspw. mit <SchnickSchnackSchnuck symbol="Brunnen"/> aufgerufen wird).

Im Unit-Test wird das Verhalten bei 'Schere', 'Stein' und 'Papier' und auch die Fehlerbehandlung bei 'Brunnen' geprüft; die Tests werden bestanden.

Allerdings wird auch die Warnung des Validators ausgegebn, da ich ja mit 'Brunnen' einen ungültigen Wert verwende. Wie kann man für den Unit-Test die Validator-Meldung unterdrücken?

LLAP 🖖

--
„Man kann sich halt nicht sicher sein“, sagt der Mann auf der Straße, „dass in einer Gruppe Flüchtlinge nicht auch Arschlöcher sind.“
„Stimmt wohl“, sagt das Känguru, „aber immerhin kann man sich sicher sein, dass in einer Gruppe Rassisten nur Arschlöcher sind.“

—Marc-Uwe Kling
  1. Hallo Gunnar,

    ich ziehe deine Frage mal vor:

    Allerdings wird auch die Warnung des Validators ausgegebn, da ich ja mit 'Brunnen' einen ungültigen Wert verwende. Wie kann man für den Unit-Test die Validator-Meldung unterdrücken?

    ich würde in dem test console.error einfach überschreiben:

    console.error = jest.fn()
    expect(console.error).toHaveBeenCalled();
    

    That said: ich vermute, du testest falsch. Du testest ja, ob Vue funktioniert; das solltest du nicht tun. Du solltest in einem Unit Test deinen Validator testen, ob der richtig funktioniert und ggfls. noch die Konfiguration deiner Komponente testen. Da deine Komponente ja ein simples Objekt ist, kannst du ja sehr einfach prüfen, ob die entsprechenden Eigenschaften den richtigen Wert haben. Und dein Validator ist nur eine Funktion, die kannst du auch sehr einfach testen.

    Freundliche Grüße,
    Christian Kruse

    1. @@Christian Kruse

      Allerdings wird auch die Warnung des Validators ausgegebn, da ich ja mit 'Brunnen' einen ungültigen Wert verwende. Wie kann man für den Unit-Test die Validator-Meldung unterdrücken?

      ich würde in dem test console.error einfach überschreiben:

      console.error = jest.fn()
      expect(console.error).toHaveBeenCalled();
      

      Die erste Zeile tut’s.

      That said: ich vermute, du testest falsch. Du testest ja, ob Vue funktioniert; das solltest du nicht tun.

      Tu ich das?

      Im Prinzip will ich sowas wie bei HTML: es werden Dinge für ungültig erklärt (bspw. <button><div>Pick me!</div></button>), aber dennoch eine Fehlerbehandlung dafür definiert.

      Die Alternative wäre, dass der Validator nicht nur eine Warnung, sondern einen Fehler wirft und die ganze Anwendung ihren Dienst quittiert. Wie wäre das zu machen?

      LLAP 🖖

      --
      „Man kann sich halt nicht sicher sein“, sagt der Mann auf der Straße, „dass in einer Gruppe Flüchtlinge nicht auch Arschlöcher sind.“
      „Stimmt wohl“, sagt das Känguru, „aber immerhin kann man sich sicher sein, dass in einer Gruppe Rassisten nur Arschlöcher sind.“

      —Marc-Uwe Kling
      1. Hallo Gunnar,

        Allerdings wird auch die Warnung des Validators ausgegebn, da ich ja mit 'Brunnen' einen ungültigen Wert verwende. Wie kann man für den Unit-Test die Validator-Meldung unterdrücken?

        ich würde in dem test console.error einfach überschreiben:

        console.error = jest.fn()
        expect(console.error).toHaveBeenCalled();
        

        Die erste Zeile tut’s.

        Die erste Zeile überschreibt, die zweite Zeile prüft ob console.error auch tatsächlich aufgerufen wurde 😀

        That said: ich vermute, du testest falsch. Du testest ja, ob Vue funktioniert; das solltest du nicht tun.

        Tu ich das?

        Ich kann das nur vermuten, da mit der Einblick in dein genaues Szenario fehlt. Meine Vermutung basierte auf deinem Beispiel.

        Im Prinzip will ich sowas wie bei HTML: es werden Dinge für ungültig erklärt (bspw. <button><div>Pick me!</div></button>), aber dennoch eine Fehlerbehandlung dafür definiert.

        🤷‍♂️ hört sich für mich jetzt schon eher danach an, als würdest du das richtige testen. Aber siehe oben…

        Die Alternative wäre, dass der Validator nicht nur eine Warnung, sondern einen Fehler wirft und die ganze Anwendung ihren Dienst quittiert. Wie wäre das zu machen?

        Um einen fatalen Fehler zu generieren würde ich eine Exception mit einem aussagekräftigen Fehlertext werfen.

        Dass die Anwendung ihren Dienst einstellt kannst du aber natürlich nicht erzwingen. Ein Entwickler könnte immer noch Pokemon spielen: gotta catch 'em all…

        Freundliche Grüße,
        Christian Kruse

        1. @@Christian Kruse

          Ich kann das nur vermuten, da mit der Einblick in dein genaues Szenario fehlt.

          Szenario ist eine Heading-Komponente.
          Für <Heading level="3" text="Und sonst so?"/> soll dann
          <h3>Und sonst so?</h3> rauskommen.

          <template>
            <component :is="headingType">{{ text }}</component>
          </template>
          
          <script type="text/babel">
            function isValidHeadingLevel(value) {
              return [1, 2, 3, 4, 5, 6].includes(value);
            }
          
            export default {
              name: 'Heading',
              props: {
                /**
                 * heading level (integer from 1 to 6)
                 */
                level: {
                  type: Number,
                  required: true,
                  validator: isValidHeadingLevel,
                },
                /**
                 * heading text
                 */
                text: {
                  type: String,
                  required: true,
                },
              },
              computed: {
                headingType() {
                  return `h${this.level}`;
                },
              },
            };
          </script>
          

          Wenn man da nun aber <Heading level="42" text="Antwort"/> aufruft, meckert zwar der Validator; aber wer liest schon Warnungen? 🧐 Es wird <h42>Antwort</h42> generiert.

          Fehlerbehandlung eingebaut:

              computed: {
                headingType() {
                  return isValidHeadingLevel(this.level) ? `h${this.level}` : 'div';
                },
              },
          

          Lass den Validator meckern, die Komponente ist robust und gibt <div>Antwort</div> aus.

          Und genau das will ich im Unit-Test auch abfragen. Und dabei soll der Validator schön still sein.

          LLAP 🖖

          --
          „Man kann sich halt nicht sicher sein“, sagt der Mann auf der Straße, „dass in einer Gruppe Flüchtlinge nicht auch Arschlöcher sind.“
          „Stimmt wohl“, sagt das Känguru, „aber immerhin kann man sich sicher sein, dass in einer Gruppe Rassisten nur Arschlöcher sind.“

          —Marc-Uwe Kling
          1. Hallo Gunnar,

            ich persönlich würde so einen Validator gar nicht erst einbauen. Man sollte Entwicklern zumuten, dass sie wissen, dass es h1 bis h6 gibt. Genau genommen würde ich nichtmal die Heading-Komponente bauen, wenn sie wirklich nur ein h1 mit entsprechendem Level generiert. Warum sollte man dann nicht direkt h1 bis h6 verwenden?

            Wenn du es aber doch tun willst (etwa weil du Infos weg gelassen hast und es in deinem Fall tatsächlich sinnvoll ist):

            Für <Heading level="3" text="Und sonst so?"/> soll dann
            <h3>Und sonst so?</h3> rauskommen.

            <template>
              <component :is="headingType">{{ text }}</component>
            </template>
            
            <script type="text/babel">
              function isValidHeadingLevel(value) {
                return [1, 2, 3, 4, 5, 6].includes(value);
              }
            
              export default {
                name: 'Heading',
                props: {
                  /**
                   * heading level (integer from 1 to 6)
                   */
                  level: {
                    type: Number,
                    required: true,
                    validator: isValidHeadingLevel,
                  },
                  /**
                   * heading text
                   */
                  text: {
                    type: String,
                    required: true,
                  },
                },
                computed: {
                  headingType() {
                    return `h${this.level}`;
                  },
                },
              };
            </script>
            

            […]

                computed: {
                  headingType() {
                    return isValidHeadingLevel(this.level) ? `h${this.level}` : 'div';
                  },
                },
            

            Ich würde hier headingType und isValidHeadingLevel einzeln testen und nicht die ganze Komponente. Du schreibst ja einen Unit Test und keinen Integration Test.

            Freundliche Grüße,
            Christian Kruse

            1. @@Christian Kruse

              Genau genommen würde ich nichtmal die Heading-Komponente bauen, wenn sie wirklich nur ein h1 mit entsprechendem Level generiert.

              Das Beispiel war schon etwas abgespeckt. Statt {{ text }} ist in Wirklichkeit da etwas Struktur drin. Außerdem …

              Warum sollte man dann nicht direkt h1 bis h6 verwenden?

              … weil die Heading-Komponente nichts vom Kontext weiß, in dem sie verwendet wird. Sie steht beispielsweise in einer Section-Komponente, welche ineinander verschachtelt werden können. Um eine saubere Überschriftenhierarchie hinzubekommen, übergibt man das jeweilige Level von außen – womöglich gar aus einer Berechnung (Outline-Algorithmus).

              Für <Heading :level="Math.PI" text="Quadratur des Kreises"/> bedarf es schon Boswilligkeit. (Ohne Fehlerbehandlung würde <h3.1415926…>Quadratur des Kreises</h3.1415926…> rauskommen.) Aber gegen Level 7, 8, 9, … möchte man sich schon absichern.

              LLAP 🖖

              --
              „Man kann sich halt nicht sicher sein“, sagt der Mann auf der Straße, „dass in einer Gruppe Flüchtlinge nicht auch Arschlöcher sind.“
              „Stimmt wohl“, sagt das Känguru, „aber immerhin kann man sich sicher sein, dass in einer Gruppe Rassisten nur Arschlöcher sind.“

              —Marc-Uwe Kling
      2. Tach!

        That said: ich vermute, du testest falsch. Du testest ja, ob Vue funktioniert; das solltest du nicht tun.

        Tu ich das?

        Die Frage ist, was du eigentlich testen möchtest. Möchtest du testen, ob dein Validator funktioniert, dann teste diesen. Du musst nicht das Szenario testen, dass Vue irgendwelche Werte vom Komponentenaufruf an deinen Validator durchreicht. Das ist Aufgabe von Vue und dessen Tests. Du solltest davon ausgehen, dass, egal welchen Wert du an die Komponente gibst, es Vue hinbekommt, damit deinen Validator aufzurufen. Diesen Teil in deinem Beispiel kannst du übergehen. Deswegen die Empfehlung, nur die Validator-Funktion selbst zu testen.

        Ein Test mit Einbeziehung der Vue-Komponente ist beispielsweise dann sinnvoll, wenn du darin einen Fehler entdeckst und - solange der nicht beseitigt ist - einen Workaround schreiben musst, und diesen testen möchtest.

        Weiterhin könnte es sein, dass du die Reaktion auf den Fehler testen möchtest, also ob mit dem von dir geschriebenen HTML/JavaScript-Teil die beabsichtigten Dinge passieren. In dem Fall ist es aber fraglich, ob du die Vue-Komponente dazu brauchst, oder ob ein Test-Dummy die bessere Wahl ist. Der Test-Dummy verhält sich gemäß der Testszenarien, aber das eigentliche Tun wird ausklammert, was so eine Vue-Komponente im Inneren macht, zuzüglich Nebenwirkungen. Wie gesagt, du möchtest ja nicht Vue testen, sondern wie dein Code mit bestimmten Werten umgeht. Inwieweit man mit Vue Tests mit Dummys erstellen kann, kann ich aber nicht beantworten.

        dedlfix.