Po co testy jak można przeklikać

2

@katakrowa:
Jakich tych testów nie jesteś w stanie zrobić? Jak pracowałem w e-podróżniku nad modułem do komunikacji z czytnikiem kart płatniczych to pisałem go wraz z testami jeszcze nie mając sprzętu. Miałem za to specyfikację z przykładami wiadomości lecących po TCP/IP i to je wrzuciłem do testów i wykryłem część błędów w implementacji. W sumie jakbym się dobrze wczytał w dokumentację to może i wszystkie bym znalazł "na sucho" (jeszcze bez sprzętu w ręce), ale pewne rzeczy były niezrozumiale napisane.

Na pewno wiele zależy od konkretnego przypadku. Napisanie np. testów dla sterowników dla kart graficznych działających bez karty graficznej (ale za to z nagraną wymianą danych między sterownikiem, a kartą) byłoby karkołomne, bo między sterownikiem, a kartą wymieniana jest masa danych. Jednak we wspomnianym przypadku czytnika kart płatniczych tej komunikacji było mało i dało się to opisać w krótkiej specyfikacji.

1

A propos testowania, ktoś tu chyba nie przeklikał: Najnowsza afera programistyczno-szczepionkowa 4 RP

3

ja pisze testy jak mi sie nie chce klikać ;)

2

@katakrowa:

@Wibowit: niby żadne cudo ale to napisz... Tak żeby funkcja leciała w tle a bity zmieniały się "obok niej". To nie jest test, w którym podajesz coś na wejściu i dostajesz wyjście. Jednak widzę, że co do jednego się zgadzamy. Jak się uprzesz to napiszesz nawet testy symulujące małpę skaczącą przed kamerą ale ekonomicznie często nie ma to sensu i zwykłe "przeklikiwanie" okazuje się być najlepszą formą testowania. — katakrowa 26 minut temu

Oto klasa (napisana w Scali, ale to mocno imperatywny styl, raczej łatwo przekładający się na C++) symulująca zmieniający się w zadanych odstępach czasu sygnał zero-jedynkowy:

import BitSignalsIterator.SignalItem

import java.time.LocalDateTime
import scala.concurrent.duration._

class BitSignalsIterator private (startTimeNano: Long,
                                  signalsPrefixSum: Array[SignalItem],
                                  private var currentIndex: Int) {
  def currentBit(): Boolean = {
    val currDuration = System.nanoTime() - startTimeNano
    while (currentIndex < signalsPrefixSum.length && currDuration > signalsPrefixSum(currentIndex)._2) {
      currentIndex += 1
    }
    if (currentIndex >= signalsPrefixSum.length) {
      sys.error("signals buffer ended")
    } else {
      signalsPrefixSum(currentIndex)._1
    }
  }
}

object BitSignalsIterator {
  type InputSignal = (Boolean, FiniteDuration)
  type SignalItem  = (Boolean, Long)

  // factory method for class BitSignalIterator
  def apply(signals: Seq[InputSignal]): BitSignalsIterator = {
    val array              = Array.ofDim[SignalItem](signals.size)
    var cumulativeDuration = 0L
    for (((signal, duration), index) <- signals.zipWithIndex) {
      cumulativeDuration += duration.toNanos
      array(index) = (signal, cumulativeDuration)
    }
    new BitSignalsIterator(System.nanoTime(), array, 0)
  }

  // main method with example usage
  def main(args: Array[String]): Unit = {
    val signalsBuffer =
      Array(true -> 5.seconds, false -> 1.second, true -> 3.seconds, false -> 2.seconds)
    val signalsIterator = BitSignalsIterator(signalsBuffer)
    while (true) {
      println(LocalDateTime.now() + " " + signalsIterator.currentBit())
      Thread.sleep(1 * 650)
    }
  }
}

Wynik:

2021-04-01T12:58:11.712 true
2021-04-01T12:58:12.371 true
2021-04-01T12:58:13.021 true
2021-04-01T12:58:13.672 true
2021-04-01T12:58:14.322 true
2021-04-01T12:58:14.973 true
2021-04-01T12:58:15.623 true
2021-04-01T12:58:16.274 true
2021-04-01T12:58:16.924 false
2021-04-01T12:58:17.574 false
2021-04-01T12:58:18.225 true
2021-04-01T12:58:18.875 true
2021-04-01T12:58:19.525 true
2021-04-01T12:58:20.176 true
2021-04-01T12:58:20.826 false
2021-04-01T12:58:21.477 false
2021-04-01T12:58:22.128 false
Exception in thread "main" java.lang.RuntimeException: signals buffer ended
    at scala.sys.package$.error(package.scala:27)
    at BitSignalsIterator.currentBit(BitSignalsIterator.scala:15)
    at BitSignalsIterator$.main(BitSignalsIterator.scala:42)
    at BitSignalsIterator.main(BitSignalsIterator.scala)
3
heyyou napisał(a):

ja pisze testy jak mi sie nie chce klikać ;)

A ja klikam jak mi się nie chce pisać testów :)

Pobawię się trochę w adwokata diabła, bo wszyscy tu oczywiście konserwatywnie orędują za testami, "by the book", żadne wyzwanie :). A ja konserwatystą nie jestem. Przedstawię kilka argumentów, nie mają one raczej uniwersalnego charakteru, ale pokazują że jak zwykle wszystko zależy.

  1. Żeby napisać test trzeba wiedzieć czego się konkretnie oczekuje na wyjściu danej metody. Są działki gdzie, po pierwsze: nie zawsze wiadomo, po drugie: czasem to w ogóle nie jest ściśle określone. Przykładowo: moja metoda wyrzuca na wyjściu jakiś wektor liczb v. Nikt nie wie czy v[18] powinno być 9.2389 czy może 9.3445. Nawet nie ma możliwości na tym etapie określić jakiegoś progu. Po prostu nie wiem co jest prawidłowym wynikiem, bo to właśnie narzędzie które jest przedmiotem testu służy do określenia tego wyniku, którego ja w inny sposób poznać nie mogę. Gdybym mógł, to to narzędzie nie byłoby mi potrzebne.
  2. Przypadek zbliżony - narzędzie działa niedeterministycznie. Nie tylko nie mogę określić a'priori czy v[18] powinno być 9.2389 czy może 9.3445, ale to co dostanę na wyjściu raz będzie 9.2389, raz 9.3445, raz jeszcze czymś innym.
  3. Być może jestem w stanie ocenić dane wyjściowe, ale mogę to zrobić tylko w złożony sposób. Np. oczekuję by zbiór punktów na wyjściu tworzył oczekiwaną strukturę (skupiał się wokół płaszczyzny o zadanych parametrach). Mój test powinien zatem stworzyć tą płaszczyznę z parametrów i sprawdzić dopasowanie punktów. Ale to zadanie wymaga implementacji pewnej logiki, czasem równie skomplikowanej to ta w testowanej metodzie. To kłóci się trochę z ideą testów jednostkowych, które nie powinny zawierać skomplikowanej logiki. Bywa że i tak się to robi - przyjmując że po prostu oba algorytmy (zaimplementowany w teście i przedmiocie testu) weryfikują się wzajemnie, jeśli na końcu nie ma między nimi sprzeczności.
  4. Innym problemem jest gdy mogę sprawdzić jakość danych na wyjściu, ale potrzebuję je porównać z danymi referencyjnymi, a one zajmują mnóstwo miejsca (np. kilkaset MB).

Skąd zatem wiem, czy narzędzie robi w ogóle coś sensownego?

  • Analiza logiczna. Moje narzędzia są implementacją jakiegoś algorytmu, na który składają się poszczególne kroki. Czasem jedyną możliwością jest po prostu dogłębne przeanalizowanie zaimplementowanej logiki.
  • Szerszy kontekst. Moje dane wyjściowe da się zweryfikować, ale dopiero gdy nakarmi się nimi dalsze komponenty procesu. Istnieje metoda weryfikacji ostatecznego wyniku procesu, czasem jest to ocena dość subiektywna, i dopiero jeśli na tym etapie okaże się, że jakość jest niezadowalająca, to przeprowadza się "backtracing" to miejsca gdzie prawdopodobnie popełniono błąd. Czy można zmockować ten kontekst? Nie zawsze. Całość może być niezwykle złożona i wykraczająca poza jedną platformę narzędziową.
  • Smoke-test: mając doświadczenie jak generalnie wyglądają "właściwe" dane na wyjściu, przetwarzając wcześniej jakieś inne zbiory, można ocenić "z grubsza" czy narzędzie działa czy nie.
  • Przeklikanie :) - generalnie mając pewne doświadczenie, wiem jakie brzegowe lub problematyczne przypadki mogą prowadzić do powstania błędów. Mogę je zasymulować bezpośrednio w kodzie (nie w teście) i sprawdzić czy moje oczekiwania się sprawdziły.
2

Ja nie wierzę deweloperom, którzy mówią, że sprawdzili manualnie. Już nie raz widziałem, że albo kłamali albo źle sprawdzili.
Jeszcze jak to tester przeklikuje, to OK, bo jemu płacą za to, by znaleźć błąd. Ale developer często chce wypchnąć kod i przestać się martwić.

2
nalik napisał(a):

Ja nie wierzę deweloperom, którzy mówią, że sprawdzili manualnie.

Wypraszam sobie :)
Sprawdziłem manualne. Za pierwszym razem.
Za drugim razem sprawdziłem ogólnie czy po zmianach hula-kula.
Za trzecim razem - przecież zmiany nie były duże - sprawdzę przy następnej poprawce

2
GutekSan napisał(a):

Żeby napisać test trzeba wiedzieć czego się konkretnie oczekuje na wyjściu danej metody. Są działki gdzie, po pierwsze: nie zawsze wiadomo, po drugie: czasem to w ogóle nie jest ściśle określone. Przykładowo: moja metoda wyrzuca na wyjściu jakiś wektor liczb v. Nikt nie wie czy v[18] powinno być 9.2389 czy może 9.3445. Nawet nie ma możliwości na tym etapie określić jakiegoś progu. Po prostu nie wiem co jest prawidłowym wynikiem, bo to właśnie narzędzie które jest przedmiotem testu służy do określenia tego wyniku, którego ja w inny sposób poznać nie mogę. Gdybym mógł, to to narzędzie nie byłoby mi potrzebne.

Przecież można napisać test po zaimplementowaniu testowanej metody i to na podstawie jej wyników. W ten sposób i tak zyskujesz, bo masz test automatyczny i nie musisz klikać tego samego ponownie i porównywać. Później, gdy wprowadzisz np optymalizacje wydajnościowe, ten test pozwoli ci wyłapać potencjalne błędy wprowadzane przy takich zmianach.

0

Testy służą dowodzeniu, że dane funkcjonalności działają w zamierzony sposób i jest to równoznaczne matematycznemu dowodzeniu twierdzeń.

Możemy sami przeklikać się przez aplikację, ale inne osoby po za naszymi słowami nie mają żadnych dowodów.
Musieli by je wykonać sami, co może im zabrać kilka godzin przy dużym projekcie.
Pomijam fakt kiepskich testów lub inaczej źle, przeprowadzonych dowodów udowadniających ich tożsamość określoną w dokumentacji.

Nowe osoby dobierające się do projektu, mogą mieć większe problemy jeśli nie ma testów.
Także błędy łatwiej namierzyć jeśli wiemy, który test zawinił i nie musimy debugować całego projektu.

2

@Szalony Programista:

równoznaczne matematycznemu dowodzeniu twierdzeń.

prędzej formalne udowadnianie kodu tym jest

1 użytkowników online, w tym zalogowanych: 0, gości: 1, botów: 0