Snapshot Testing - Prostota i dokumentacja w jednym

Snapshot Testing - Prostota i dokumentacja w jednym

Dostarczając funkcjonalność opartą na Azure Cognitive Services, w której chciałem wykorzystać funkcję czytania tekstu przez AI, zaskoczyło mnie SDK. Okazało się, że biblioteka NuGet Microsoft.CognitiveServices.Speech nie posiada własnych modeli, które by odzwierciedlały strukturę języka znaczników syntezy mowy: Speech Synthesis Markup Language (SSML). Rozwinięcie tego tematu to na pewno pomysł na kolejny artykuł. :) W wielkim skrócie, wypuściłem w świat paczkę SSMLBuilder.Azure, która adresuje te niedogodności.

Jak przetestować generator struktury XML?

Podczas pracy nad kodem źródłowym do tworzenia tekstu dla efektywnej syntezy mowy, zauważyłem, że większość interfejsów, czyli całe „mięso”, to w istocie tworzenie generatorów odpowiedniej struktury XML, którą jest SSML. Zacząłem zastanawiać się, jak przetestować poszczególne bloki struktury, które sprowadzały się praktycznie do implementacji poniższego interfejsu:

 public interface ISsmlElement
    {
        /// <summary>
        /// Converts the implementing element to an SSML-formatted element.
        /// </summary>
        /// <param name="ns">The XML namespace to be used for the SSML element, if needed.</param>
        /// <returns>An <see cref="SsmlElement"/> that represents the current object in SSML format.</returns>
        SsmlElement ToSsml(XNamespace? ns = null);
    }

czyli w praktyce zwrócenia obiektu XML:

public class SsmlElement
    {
        [...]
        /// <summary>
        /// Gets the underlying XElement representation of this SSML element.
        /// </summary>
        public XElement XElement { get; }
        [...]
    }

Asserty XElement

Pierwszym pomysłem, a tak naprawdę brakiem zrozumienia problemu, byłoby każdorazowe przygotowywanie oczekiwanego obiektu XML do porównania w assercie. W czasach GPT możemy łatwo „wyobrazić” sobie, jakby wyglądałyby Asserty w tej sytuacji i jakie byłoby ich setupowanie:

// give me c# code that will produce such content hardcoded, using XElements: [...]
XNamespace ns = "http://www.w3.org/2001/10/synthesis";
        XElement expectedElement = new XElement(ns + "speak",
            new XAttribute("version", "1.0"),
            new XAttribute(XNamespace.Xml + "lang", "en-US"),
            new XElement(ns + "voice",
                new XAttribute("name", "en-US-AvaMultilingualNeural"),
                "Why are snails slow?"
            )
        );

Utrzymanie tego rodzaju testów z pewnością nie jest przyjemne, a błędy w literkach mogą prowadzić do kolejnych straconych minut. Co więcej, jak sprawdzić, czy Azure Speech Service zgodnie z oczekiwaniem interpretuje ten skomplikowany markup? Wydaje się, że nie obyłoby się bez testów integracyjnych, ale takie rozwiązanie rodzi tylko kolejne pytania bez odpowiedzi. Jak przetestować nagranie dźwiękowe? Jak uruchamiać testy integracyjne przeciwko chmurze Azure w projekcie, który stworzyłem jako open source? To totalna przesada w kontekście utrzymania.

Może testować plain text?

Jeśli Pomysł na Asserty XElement nie wchodzi w grę, mam pomysł by porównywać rezultat generowania jako plain text. Zgodnie z dokumentacją metody XNode.ToString(), która zwraca XML dla reprezentowanego przez obiekt węzła, można użyć tej metody do generowania i porównywania stringów XML. Oto przykład takiego rozwiązania:

// Arrange
            var sut = Speak.InLanguage("en-US")
                .WithElement(Voice
                .Named("en-US-AvaMultilingualNeural")
                .WithContent(Content.Of("Why are snails slow?")));

            // Act
            var result = sut.ToString();

Do assercji można przygotować zahardkodowany string z oczekiwaną strukturą XML. To rozwiązanie jest lepsze, ponieważ programista widząc oczekiwaną strukturę:

var expectedResult = "<speak version="1.0" xml:lang="en-US" xmlns="http://www.w3.org/2001/10/synthesis">
  <voice name="en-US-AvaMultilingualNeural">Why are snails slow?</voice>
</speak>"

ma możliwość wizualnej oceny struktury XML, co daje lepsze zrozumienie tego, czego dotyczy test, a także umożliwia łatwe skopiowanie wyniku lub oczekiwanego rezultatu do Azure Speech Studio i przesłuchania wygenerowanego nagrania. Skopiowanie struktury z debuggera Visual Studio zamienia znaki „<“ na „lt” ze względu na zabezpieczenie przed atakami typu injection, ale to problem można rozwiązać w dalszej pracy nad kodem. Kolejne dopracowanie tego pomysłu polega na zastąpieniu oczekiwanych rezultatów, hardkodowanych w postaci stringów elementów XML, przechowywaniem ich w osobnych plikach. Dla każdego testu można by mieć oddzielny plik XML, co ułatwi zarządzanie i aktualizację oczekiwanych wyników bez potrzeby ingerowania w kod źródłowy testów. To podejście znacząco usprawnia zarządzanie testami i może przyczynić się do lepszej organizacji oraz czytelności testów. Należałoby przygotować po jednym pliku dla każdego przypadku testowego. Czy istnieje lepszy sposób?

Testowanie przeciwko Snapshotom

Co to jest Snapshot testing?

Snapshot testing, czyli testowanie migawek, to technika oprogramowania, która polega na porównywaniu aktualnego stanu obiektu z wcześniej zapisanym „zrzutem”. Automatyzacja procesu zapisywania i porównywania snapshotów jest często zapewniana przez różne SDK. Ta metoda jest szczególnie popularna w testowaniu interfejsów użytkownika, gdzie zmiany są zazwyczaj subtelne, aby nie wprowadzać zamieszania i utrzymać komfort użytkowania. Interfejsy użytkownika, takie jak HTML czy XAML, ze względu na ich względną stabilność, są idealnymi kandydatami do tego rodzaju testowania.

Rozwój AI otwiera nowe możliwości dla technik testowania migawek, szczególnie w kontekście analizy i interpretacji obrazów. Narzędzia takie jak Analiza obrazu w usługach Azure mogą przyczynić się do rozwijania nowych metodologii testowania, które będą w stanie lepiej rozumieć i interpretować zmiany wizualne. To może obejmować nie tylko tradycyjne snapshoty, ale również bardziej zaawansowane formy oceny zmian w dynamicznie generowanych interfejsach czy wizualnych aspektach aplikacji. Możliwość automatycznego rozpoznawania i oceniania zmian wizualnych dzięki AI może znacząco zwiększyć efektywność i dokładność procesów testowych, zmieniając sposób, w jaki podchodzimy do zapewnienia jakości w rozwoju oprogramowania.

Testowanie generowanych XML ze snapshotów - Verify

Wystarczyło połączyć kilka elementów, aby uzyskać rozwiązanie, które jest szybkie w implementacji i proste w utrzymaniu. Verify to biblioteka dla platformy .Net, umożliwiająca tworzenie testów typu snapshot. Po jej wdrożeniu cały proces testowania sprowadził się do generowania struktur i wywoływania weryfikacji, na przykład w SpeakTests:

[Fact()]
        public async Task WhenElement()
        {
            // Arrange
            var sut = Speak.InLanguage("en-US")
                .WithElement(Voice
                .Named("en-US-AvaMultilingualNeural")
                .WithContent(Content.Of("Why are snails slow?")));

            // Act
            var result = sut.ToString();

            // Assert
            await Verify(result);
        }

W wyniku pierwszego uruchomienia testu został wygenerowany snapshot w postaci pliku .txt, który można było nagrać i sprawdzić w Azure Speech Studio, a następnie zapisać w systemie kontroli wersji jako oczekiwany rezultat dla kolejnych wykonania. To przykład rezultatu dla testu powyżej:

<speak version="1.0" xml:lang="en-US" xmlns="http://www.w3.org/2001/10/synthesis">
  <voice name="en-US-AvaMultilingualNeural">Why are snails slow?</voice>
</speak>

Warto dodać, że biblioteka Verify w ciekawy sposób obsługuje testy wyjątków, traktując je tak samo jak każdy inny wynik, czyli generując snapshot oczekiwanego stanu. To przykład rezultatu dla wyjątku:

{
  Type: SsmlBuilderException,
  Message: No speak elements.,
  StackTrace: at SSMLBuilder.Azure.Elements.Speak.ToSsml()
}

Snapshot testing w pracy nad bibliotekami i open source

Tworzenie SDK, w formie bibliotek, opiera się na podobnych zasadach jak tworzenie interfejsów użytkownika. W tym przypadku naszym klientem jest deweloper korzystający z naszego API, a nie końcowy użytkownik. Dbamy o jego komfort, utrzymując stabilne interfejsy, których drastyczne zmiany mogłyby wprowadzić chaos i zakłócić działanie procesów opartych na naszym rozwiązaniu.

Pracując nad projektami open source, umożliwiamy innym wgląd w nasz kod oraz jego ewentualną modyfikację. Z jednej strony otwiera to ogromne możliwości dzięki połączeniu umiejętności programistów nie tylko z naszej firmy. Z drugiej strony, tworzymy bardziej uniwersalne rozwiązania, które mogą zaspokoić potrzeby nie tylko naszego biznesu, ale potencjalnie całego świata. Wykorzystanie naszej biblioteki w branżach takich jak medycyna czy finanse wiąże się z większą odpowiedzialnością. Wprowadzanie zmian w kodzie przez innych programistów wymaga zaufania oraz weryfikacji, aby upewnić się, że nie wprowadzą one błędów do SDK.

Dzięki zastosowaniu testów migawkowych, umożliwiamy sprawdzanie poprawności działania kodu bez dogłębnej znajomości domeny. Przy proponowaniu zmian, całe community zaangażowane w tworzenie open source może zobaczyć, jak zmienią się rezultaty poszczególnych wywołań oraz zachowanie nowych interfejsów. Przechowywanie snapshotów w ogólnodostępnej bazie kodu pozwala programistom, czyli użytkownikom, na dostęp do migawek, które w połączeniu z testami można traktować jako dokumentację sterowaną zachowaniem. Dodatkowo, w przypadku błędów, mogą przeszukać naszą bazę kodu w poszukiwaniu odpowiedzi na pytanie, jaki scenariusz prowadzi do wywołania konkretnego wyjątku.

Kiedy używać snapshot testing?

Snapshot testing jest potężnym narzędziem, szczególnie wartościowym w kilku kluczowych scenariuszach:

  1. Sprawdzanie regresji w rozwiązaniach opartych na generatywnej sztucznej inteligencji: Testy migawek umożliwiają szybkie wykrycie i dokumentowanie niepożądanych zmian oraz ocenę jakościową, ludzkim okiem, generowanych przez GPT rozwiązań.

  2. Testowanie skomplikowanych struktur danych: Idealne do zapisywania i porównywania złożonych struktur, takich jak dokumenty czy wykresy, z oczekiwanymi wynikami, co zapewnia precyzyjne sprawdzanie zgodności.

  3. Wizualna weryfikacja rezultatów: Gdy zależy nam na szczegółowym sprawdzeniu formatowania lub wyglądu interfejsów, snapshot testing pozwala na dokładne uchwycenie i porównanie stanu wizualnego elementów, z dokładnością do whitespace.

  4. Dokumentacja struktur danych: Testy migawkowe mogą służyć jako narzędzie dokumentacyjne, umożliwiające precyzyjne śledzenie i kontrolę sposobu przekazywania danych między różnymi częściami systemu. W przypadku tworzenia rozwiązań open source, jak SSMLBuilder.Azure dla platformy Azure, snapshoty tworzą dokumentację, która jest zarówno precyzyjna, jak i łatwa do aktualizacji.

Stosowanie testów migawkowych zapewnia solidne zabezpieczenie przed niekontrolowaną regresją i daje możliwość wzrokowego zastanowienia się nad wprowadzanymi zmianami do kontraktów. Z zainteresowaniem obserwuję, jak dynamicznie rozwijająca się branża przetwarzania obrazów oraz rosnące potrzeby weryfikacji modeli AI przeciwko regresji wpłyną na rozwój tego rodzaju narzędzi.