Jak mogę przeprowadzić konfigurację testową przy użyciu pakietu testowego w Go

111

Jak mogę przeprowadzić ogólne przetwarzanie konfiguracji testów, które ustawia etap dla wszystkich testów podczas korzystania z pakietu testowego ?

Jako przykład w Nunit istnieje [SetUp]atrybut.

[TestFixture]
public class SuccessTests
{
  [SetUp] public void Init()
  { /* Load test data */ }
}
miltonb
źródło
2
Począwszy od wersji 1.4 możesz mieć globalną konfigurację i porzucić
Salvador Dali

Odpowiedzi:

159

Począwszy od Go 1.4 możesz zaimplementować konfigurację / dezaktywację (nie ma potrzeby kopiowania funkcji przed / po każdym teście). Dokumentacja jest opisana tutaj w sekcji głównej :

TestMain działa w głównym goroutine i może wykonać dowolną konfigurację i rozłączenie, jakie jest konieczne po wezwaniu m.Run. Powinien wtedy wywołać os.Exit z wynikiem m.Run

Zajęło mi trochę czasu, zanim zrozumiałem, że oznacza to, że jeśli test zawiera funkcję, func TestMain(m *testing.M)to ta funkcja zostanie wywołana zamiast uruchamiania testu. W tej funkcji mogę zdefiniować, jak będą przebiegać testy. Na przykład mogę zaimplementować globalną konfigurację i porzucić:

func TestMain(m *testing.M) {
    setup()
    code := m.Run() 
    shutdown()
    os.Exit(code)
}

Kilka innych przykładów można znaleźć tutaj .

Funkcja TestMain dodana do frameworka testowego Go w najnowszej wersji jest prostym rozwiązaniem dla kilku testowych przypadków użycia. TestMain zapewnia globalny punkt zaczepienia do wykonywania konfiguracji i zamykania, kontrolowania środowiska testowego, uruchamiania innego kodu w procesie potomnym lub sprawdzania zasobów wyciekających przez kod testowy. Większość pakietów nie będzie potrzebować TestMain, ale jest to mile widziany dodatek na te czasy, kiedy jest potrzebny.

Salvador Dali
źródło
17
TestMainznajduje się w pakiecie, więc nie jest to przydatne. Uważam, że podtesty są lepsze do bardziej złożonych celów.
Inanc Gumus
3
Jak masz przekazać kontekst z funkcji setup do testów bez używania zmiennych globalnych? Na przykład, jeśli mySetupFunction () tworzy katalog tymczasowy do wykonywania testów (z unikalną, losową nazwą), w jaki sposób testy znają nazwę katalogu? Musi być miejsce na ustawienie tego kontekstu?
Lqueryvg
1
Wygląda na to, że jest to oficjalny sposób obsługi haków przed i po testach, patrz golang.org/pkg/testing/#hdr-Main, aby uzyskać oficjalną dokumentację
de-jcup
4
@InancGumuslstat $GOROOT/subtests: no such file or directory
030
1
pamiętaj, że 'code: = m.Run ()' to ten, który uruchamia inne TestFunctions!
Alex Punnen
49

Można to osiągnąć, umieszczając init()w _test.gopliku funkcję . To zostanie uruchomione przed init()funkcją.

// package_test.go
package main

func init() {
     /* load test data */
}

_Test.init () zostanie wywołana przed funkcją pakietu init ().

miltonb
źródło
2
Wiem, że odpowiadasz na własne pytanie, więc prawdopodobnie jest to zgodne z Twoim własnym przypadkiem użycia, ale nie jest to odpowiednik przykładu NUnit, który zawarłeś w swoim pytaniu.
James Henstridge
Cóż, @james, pokazałem jedną myśl na temat odpowiedzi na problem, a inni dostarczyli już dobre spostrzeżenia, w tym twoje. Przydatne jest uzyskanie wpływów z zewnątrz, aby dostroić podejście. Dzięki.
miltonb
2
Słusznie. To, co pokazałeś w tej odpowiedzi, jest nieco bliższe użyciu [TestFixtureSetUp]atrybutu NUnit .
James Henstridge
2
nie obejmuje części do
zburzenia
7
Nie jest to dobre rozwiązanie, jeśli plik testowy znajduje się w tym samym pakiecie z funkcją main.
MouseWanted
28

Biorąc pod uwagę prostą funkcję do testu jednostkowego:

package math

func Sum(a, b int) int {
    return a + b
}

Możesz to przetestować za pomocą funkcji konfiguracji, która zwraca funkcję porzucenia. Po wywołaniu setup () możesz wykonać odroczone wywołanie teardown ().

package math

import "testing"

func setupTestCase(t *testing.T) func(t *testing.T) {
    t.Log("setup test case")
    return func(t *testing.T) {
        t.Log("teardown test case")
    }
}

func setupSubTest(t *testing.T) func(t *testing.T) {
    t.Log("setup sub test")
    return func(t *testing.T) {
        t.Log("teardown sub test")
    }
}

func TestAddition(t *testing.T) {
    cases := []struct {
        name     string
        a        int
        b        int
        expected int
    }{
        {"add", 2, 2, 4},
        {"minus", 0, -2, -2},
        {"zero", 0, 0, 0},
    }

    teardownTestCase := setupTestCase(t)
    defer teardownTestCase(t)

    for _, tc := range cases {
        t.Run(tc.name, func(t *testing.T) {
            teardownSubTest := setupSubTest(t)
            defer teardownSubTest(t)

            result := Sum(tc.a, tc.b)
            if result != tc.expected {
                t.Fatalf("expected sum %v, but got %v", tc.expected, result)
            }
        })
    }
}

Narzędzie do testowania Go zgłosi instrukcje logowania w konsoli powłoki:

% go test -v
=== RUN   TestAddition
=== RUN   TestAddition/add
=== RUN   TestAddition/minus
=== RUN   TestAddition/zero
--- PASS: TestAddition (0.00s)
    math_test.go:6: setup test case
    --- PASS: TestAddition/add (0.00s)
        math_test.go:13: setup sub test
        math_test.go:15: teardown sub test
    --- PASS: TestAddition/minus (0.00s)
        math_test.go:13: setup sub test
        math_test.go:15: teardown sub test
    --- PASS: TestAddition/zero (0.00s)
        math_test.go:13: setup sub test
        math_test.go:15: teardown sub test
    math_test.go:8: teardown test case
PASS
ok      github.com/kare/go-unit-test-setup-teardown 0.010s
% 

Dzięki takiemu podejściu możesz przekazać dodatkowe parametry do konfiguracji / usunięcia.

Kare Nuorteva
źródło
2
To naprawdę prosta, ale skuteczna sztuczka. Świetne wykorzystanie składni Go.
miltonb
1
Tak, ale zwiększa zagnieżdżenie ( rodzaj piramidy zagłady w javascript ). Ponadto testy nie są uruchamiane automatycznie przez pakiet, tak jak w przypadku testów zewnętrznych.
Inanc Gumus
12

Zazwyczaj testy w go nie są napisane w tym samym stylu, co inne języki. Często jest stosunkowo mniej funkcji testowych, ale każda zawiera zestaw przypadków testowych oparty na tabeli. Zobacz ten artykuł napisany przez jednego z członków zespołu Go.

W przypadku testu opartego na tabeli po prostu umieszczasz kod konfiguracyjny przed pętlą, która wykonuje poszczególne przypadki testowe określone w tabeli, a następnie umieszczasz dowolny kod czyszczący.

Jeśli nadal masz współdzielony kod konfiguracji między funkcjami testowymi, możesz wyodrębnić udostępniony kod konfiguracji do funkcji i użyć, sync.Oncejeśli ważne jest, aby został wykonany dokładnie raz (lub jak sugeruje inna odpowiedź, użyj init(), ale ma to tę wadę, że konfiguracja zostanie wykonane, nawet jeśli przypadki testowe nie są uruchomione (być może dlatego, że ograniczyłeś przypadki testowe za pomocą go test -run <regexp>.)

Powiedziałbym, że jeśli uważasz, że potrzebujesz wspólnej konfiguracji między różnymi testami, które są wykonywane dokładnie wtedy, gdy powinieneś się zastanowić, czy naprawdę tego potrzebujesz, i jeśli test oparty na tabeli nie byłby lepszy.

Paul Hankin
źródło
7
To świetnie, gdy testujesz trywialne rzeczy, takie jak parser flag lub algorytm, który ubija liczby. Ale tak naprawdę nie pomaga, gdy próbujesz przetestować różne elementy funkcjonalności, które wymagają podobnego standardowego kodu. Przypuszczam, że mógłbym zdefiniować moje funkcje testowe w tablicy i je iterować, ale wtedy nie jest to tak naprawdę oparte na tabelach, a raczej prosta pętla, która powinna być wbudowana w samą strukturę testową (w postaci odpowiedniego zestawu testów z funkcjami konfiguracji /
rozłączania
9

Struktura testowa Go nie ma nic równoważnego z atrybutem SetUp NUnit (oznaczającym funkcję, która ma zostać wywołana przed każdym testem w zestawie). Jest jednak kilka opcji:

  1. Po prostu wywołaj swoją SetUpfunkcję z każdego testu, gdy jest potrzebna.

  2. Użyj rozszerzenia do platformy testowej Go, która implementuje paradygmaty i koncepcje xUnit. Przychodzą na myśl trzy mocne opcje:

Każda z tych bibliotek zachęca Cię do organizowania testów w pakiety / urządzenia podobne do innych frameworków xUnit i wywoła metody konfiguracji dla typu pakietu / urządzenia przed każdą z Test*metod.

James Henstridge
źródło
0

Bezwstydna wtyczka, stworzyłem https://github.com/houqp/gtest, aby pomóc rozwiązać dokładnie ten problem.

Oto krótki przykład:

import (
  "strings"
  "testing"
  "github.com/houqp/gtest"
)

type SampleTests struct{}

// Setup and Teardown are invoked per test group run
func (s *SampleTests) Setup(t *testing.T)      {}
func (s *SampleTests) Teardown(t *testing.T)   {}
// BeforeEach and AfterEach are invoked per test run
func (s *SampleTests) BeforeEach(t *testing.T) {}
func (s *SampleTests) AfterEach(t *testing.T)  {}

func (s *SampleTests) SubTestCompare(t *testing.T) {
  if 1 != 1 {
    t.FailNow()
  }
}

func (s *SampleTests) SubTestCheckPrefix(t *testing.T) {
  if !strings.HasPrefix("abc", "ab") {
    t.FailNow()
  }
}

func TestSampleTests(t *testing.T) {
  gtest.RunSubTests(t, &SampleTests{})
}

Możesz utworzyć dowolną grupę testową w pakiecie z każdą z nich przy użyciu innego zestawu procedur konfiguracji / dezaktywacji.

houqp
źródło