Wann Snapshot Testing dich überschnappen lassen

, Kriese Dominik

Komponentenbasierte Frameworks und Libraries wie React und Angular übernehmen zunehmend die Welt moderner Frontend-Entwicklung. Und mit diesen kommen neue Wege Web-UI-Code zu testen. Eines der bekanntesten Beispiele hierfür ist Snapshot Testing. Es wuchs aus der React Community heraus und bietet viele Vorteile.

Allerdings fokussieren wir uns heute auf das, was bei dieser Vorgehensweise schiefgehen kann. Dafür schauen wir uns zuerst an, was Snapshot Testing ist. Danach gehen wir darauf ein, wie es das Testvorgehen deines Teams unterwandern kann.

Was ist Snapshot Testing

Snapshot testing beschreibt Approval Testing innerhalb der Frontend Community. Oder ist es andersrum? Unabhängig davon: Die grundsätzliche Idee ist, Stücke reinen Textes mit einer gespeicherten Basis zu vergleichen.

Frontendentwickler lieben diese Technik. Und mit gutem Grund. Mit wenig Aufwand entstehen Tests, die viele Aspekte im Hinblick auf unser DOM oder andere Datenstrukturen validieren. Ein Snapshot-Test kann wie folgt aussehen, der daraus entstehende Snapshot ist darunter angegeben:

it('should style a button', () => {
    const callback = jest.fn();
    const button = mount(<Button label={'Button'} clickHandler={callback}/>)
        .find('button');
    expect(button).toMatchSnapshot();
});
exports[`Button should style a button 1`] = `
.c0 {
  background-color: #0f7956;
  border: none;
  border-radius: 4px;
  padding: 6px 24px;
  color: white;
  font-weight: bold;
  float: right;
  margin-top: 16px;
}

.c0:hover {
  background-color: #21ae80;
  cursor: pointer;
}

<button
  className="c0"
  onClick={[MockFunction]}
>
  Button
</button>
`;

Eines fällt direkt auf: Wenige Zeilen Test Code validieren sowohl unsere HTML Struktur als auch die hinterlegten CSS Klassen. Das sieht vielversprechend aus und spart Zeit. Allerdings sollten wir uns der versteckten Fallen bewusst sein.

Snapshot Testing kann der Disziplin deines Teams schaden

In der Praxis hat die anfängliche Begeisterung für Snapshot Testing häufig einen ungewünschten Nebeneffekt: Die Methode wird als Allheilmittel verwendet und dadurch zum Problem. Aber wie kann das passieren? Schauen wir uns mal ein Beispiel mit einem Snapshot Test zur Validierung an:

const Application = styled.div`
  background-color: #f3f3f3;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  color: #333;
  text-align: center;
`

const App = () => {

    const defaultText = "Enter some text";
    let initialState = localStorage.load();
    const [label, setLabel] = useState(initialState ?? defaultText);
    const [input, setInput] = useState('');


    const updateLabel = () => {
        setLabel(!!input ? input : defaultText);
        setInput("");
        localStorage.save(input);
    }

    const inputLabel = 'Enter your text';
    return (
        <Application data-testid={'application'}>
            <Container>
                <Headline text={label}/>
                <TextInput value={input}
                           changeHandler={setInput}
                           label={inputLabel}
                           id={'text'}/>
                <Button label={'UPDATE'} clickHandler={updateLabel}/>
            </Container>
        </Application>
    );
}

export default App;

Jede in diesem Beispiel verwendete Komponente hat selbst wiederum einiges an Code. Der erstellte Text eines Snapshots wächst mit der Anzahl der enthaltenen Komponenten. Das führt zu drei Problemen. Zunächst schlägt der Test bei jeder Änderung in der Applikation fehl. Zweitens schlägt er nie alleine an, da die Komponenten hoffentlich ebenfalls getestet wurden. Und abschließend: Denken wir an eine kritische Deadline, würdest du diesen Snapshot wirklich lesen:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Application should never be tested as a snapshot 1`] = `
.c4 {
  display: block;
  padding: 6px 12px;
  border-bottom: 1px solid #333;
  border-top: none;
  border-right: none;
  border-left: none;
  background-color: #f3f3f3;
  font-size: 1.25rem;
  width: 100%;
  box-sizing: border-box;
}

.c4:focus {
  border-bottom: 2px solid #0f7956;
  padding-bottom: 5px;
}

.c3 {
  display: block;
  margin-top: 16px;
  font-size: 0.75rem;
  background-color: #f3f3f3;
  padding: 4px 12px 0;
  width: 100%;
  text-align: left;
  box-sizing: border-box;
}

.c2 {
  text-align: left;
  font-size: 2.5rem;
}

.c5 {
  background-color: #0f7956;
  border: none;
  border-radius: 4px;
  padding: 6px 24px;
  color: white;
  font-weight: bold;
  float: right;
  margin-top: 16px;
}

.c5:hover {
  background-color: #21ae80;
  cursor: pointer;
}

.c1 {
  background-color: #eaeaea;
  border-radius: 4px;
  padding: 32px;
  -webkit-box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.25);
  -moz-box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.25);
  box-shadow: 2px 2px 5px 0px rgba(0,0,0,0.25);
}

.c0 {
  background-color: #f3f3f3;
  min-height: 100vh;
  display: -webkit-box;
  display: -webkit-flex;
  display: -ms-flexbox;
  display: flex;
  -webkit-flex-direction: column;
  -ms-flex-direction: column;
  flex-direction: column;
  -webkit-align-items: center;
  -webkit-box-align: center;
  -ms-flex-align: center;
  align-items: center;
  -webkit-box-pack: center;
  -webkit-justify-content: center;
  -ms-flex-pack: center;
  justify-content: center;
  color: #333;
  text-align: center;
}

<App>
  <styled.div
    data-testid="application"
  >
    <div
      className="c0"
      data-testid="application"
    >
      <Container>
        <styled.div>
          <div
            className="c1"
          >
            <Headline
              text="Enter some text"
            >
              <styled.h1>
                <h1
                  className="c2"
                >
                  Enter some text
                </h1>
              </styled.h1>
            </Headline>
            <TextInput
              changeHandler={[Function]}
              id="text"
              label="Enter your text"
              value=""
            >
              <styled.label
                htmlFor="text"
              >
                <label
                  className="c3"
                  htmlFor="text"
                >
                  Enter your text
                </label>
              </styled.label>
              <styled.input
                id="text"
                onChange={[Function]}
                type="text"
                value=""
              >
                <input
                  className="c4"
                  id="text"
                  onChange={[Function]}
                  type="text"
                  value=""
                />
              </styled.input>
            </TextInput>
            <Button
              clickHandler={[Function]}
              label="UPDATE"
            >
              <styled.button
                onClick={[Function]}
              >
                <button
                  className="c5"
                  onClick={[Function]}
                >
                  UPDATE
                </button>
              </styled.button>
            </Button>
          </div>
        </styled.div>
      </Container>
    </div>
  </styled.div>
</App>
`;

Ist dir die Farbe des Buttons aufgefallen? Mir nicht. Die meisten Entwickler, die ich kenne, sind hier selbstbewusst und geben jest -u ein, um den fehlschlagenden Test „zu beheben“. Und dabei sind das nicht einmal 200 Zeilen Code in unserer Beispielanwendung. Der Effekt verschlimmert sich bei realen Projekten mit Templates oder Seiten die mehrere Tausend Zeilen rendern zunehmend.

Die ersten beiden Punkte sind hauptsächlich nervig, aber kein großes Problem. Das Letzte ist allerdings groß genug um ernsthafte Risiken zu erzeugen. Hier ist die Nützlichkeit dieser Tests komplett von der Disziplin der beteiligten Entwickler abhängig. Und das beeinflusst auch andere Tests, da der Fehlerlog andere Probleme verstecken kann. In diesen Szenarien ist die Frage nicht ob, sondern wann jemand alle Snapshots im Projekt aktualisiert, ohne ein wirkliches Problem in einer anderen Komponente zu betrachten.

Ist Snapshot Testing deshalb schlecht?

Nein. Snapshot Testing ist genial. Ich liebe es! Seitdem ich sie entdeckt habe spare ich unglaubliche Mengen an Zeit. Aber mir ist folgendes Wichtig: Nutze das richtige Tool für den passenden Job. Vereinen wir eine Vielzahl von kleinen oder mittleren Komponenten zu einem Template oder einer Page ist Snapshot Testing häufig die falsche Wahl. Visual Regression Testing oder eine klassische Prüfung, ob die Komponenten existieren wären hier sinnvolle Alternativen.

Haben wir stattdessen eine kleine Komponente in der Hand? Rendert sie weniger als 100 Zeilen? Hier sind Snapshot Tests häufig das Perfekte Tool. Und eines sollten wir nie vergessen: Snapshot Testing kann mehr als die Integrität des DOMs zu validieren. Hat das Projekt einen Algorithmus der Datenstrukturen erzeugt? Dann könnte Snapshot Testing dir beispielsweise helfen. Aber auch hier gilt: Kannst du deine Snapshots nicht in einer halben Minute lesen, überdenke deine Wahl.

Abschließende Worte

Für heute bin ich mit dem Thema Snapshot Testing durch. Falls du mehr über Teststrategien im Frontend Bereich erfahren willst, dann schau gerne wieder vorbei. Wir werden in naher Zukunft viele weitere spannende Themen behandeln.

Hat dir der Beitrag gefallen? Hast du was gelernt? Dann hinterlasse doch einen Kommentar, ich freue mich nämlich auch von dir zu lernen.

Allgemeine Anfrage

Wir freuen uns darauf, Ihre Herausforderungen zusammen in Angriff zu nehmen und über passende Lösungsansätze zu sprechen. Kontaktieren Sie uns – und erhalten Sie maßgeschneiderte Lösungen für Ihr Unternehmen. Wir freuen uns auf Ihre Kontaktanfrage!

Jetzt Kontakt aufnehmen