Oddzielenie testów jednostkowych i testów integracyjnych w Go

98

Czy istnieje ustalona najlepsza praktyka oddzielania testów jednostkowych i testów integracyjnych w GoLang (zeznawaj)? Mam mieszankę testów jednostkowych (które nie opierają się na żadnych zewnętrznych zasobach i dlatego działają bardzo szybko) i testów integracyjnych (które polegają na jakichkolwiek zasobach zewnętrznych i dlatego działają wolniej). Więc chcę mieć możliwość kontrolowania, czy włączać testy integracji, kiedy mówię go test.

Wydaje się, że najbardziej prostą techniką byłoby zdefiniowanie flagi zintegrowanej w main:

var runIntegrationTests = flag.Bool("integration", false
    , "Run the integration tests (in addition to the unit tests)")

Następnie, aby dodać instrukcję if na początku każdego testu integracji:

if !*runIntegrationTests {
    this.T().Skip("To run this test, use: go test -integration")
}

Czy to najlepsze, co mogę zrobić? Przeszukałem dokumentację zeznań, aby sprawdzić, czy jest może jakaś konwencja nazewnictwa lub coś, co zapewnia mi to, ale nic nie znalazłem. Czy coś mi brakuje?

Craig Jones
źródło
2
Myślę, że stdlib używa -short, aby wyłączyć testy, które trafiły do ​​sieci (i inne długo działające rzeczy). Poza tym twoje rozwiązanie wygląda dobrze.
Volker
-short jest dobrą opcją, podobnie jak własne flagi kompilacji, ale flagi nie muszą znajdować się w main. jeśli zdefiniujesz var integration = flag.Bool("integration", true, "Enable integration testing.")zmienną jako poza funkcją, zmienna pojawi się w zakresie pakietu, a flaga będzie działać poprawnie
Atifm

Odpowiedzi:

156

@ Ainar-G sugeruje kilka świetnych wzorców do oddzielnych testów.

Ten zestaw praktyk Go z SoundCloud zaleca używanie tagów kompilacji ( opisanych w sekcji „Ograniczenia kompilacji” pakietu kompilacji ) w celu wybrania testów do uruchomienia:

Napisz plik Integracja_test.go i nadaj mu tag kompilacji integracji. Zdefiniuj (globalne) flagi dla rzeczy takich jak adresy usług i łączenie łańcuchów i używaj ich w swoich testach.

// +build integration

var fooAddr = flag.String(...)

func TestToo(t *testing.T) {
    f, err := foo.Connect(*fooAddr)
    // ...
}

go test pobiera tagi kompilacji, tak jak go build, więc możesz wywołać go test -tags=integration . Syntetyzuje również pakiet main, który wywołuje flag.Parse, więc wszelkie zadeklarowane i widoczne flagi zostaną przetworzone i udostępnione do testów.

Podobną opcję można by również przeprowadzić domyślnie uruchamiając testy integracji przy użyciu warunku kompilacji // +build !unit , a następnie wyłączyć je na żądanie, uruchamiając go test -tags=unit.

@adamc komentarze:

Dla każdego, kto próbuje użyć tagów kompilacji, ważne jest, aby // +build testkomentarz był pierwszym wierszem w pliku i aby po komentarzu umieścić pusty wiersz, w przeciwnym razie-tags polecenie zignoruje dyrektywę.

Ponadto tag używany w komentarzu do kompilacji nie może zawierać myślnika, chociaż znaki podkreślenia są dozwolone. Na przykład // +build unit-testsnie zadziała, podczas gdy // +build unit_testsbędzie.

Alex
źródło
1
Używam tego od jakiegoś czasu i jest to zdecydowanie najbardziej logiczne i najprostsze podejście.
Ory Band
1
jeśli masz testy jednostkowe w tym samym pakiecie, musisz ustawić // + build unittesty jednostkowe i użyć jednostki -tag do uruchamiania testów
LeoCBS
2
@ Tyler.z.yang Czy możesz podać link lub więcej szczegółów na temat wycofywania tagów? Nie znalazłem takich informacji. Używam tagów z go1.8 do sposobu opisanego w odpowiedzi, a także do mockowania typów i funkcji w testach. Myślę, że to dobra alternatywa dla interfejsów.
Alexander I.Grafov
2
Dla każdego, kto próbuje użyć tagów kompilacji, ważne jest, aby // +buildkomentarz testowy był pierwszą linią w pliku i aby dołączyć pustą linię po komentarzu, w przeciwnym razie -tagspolecenie zignoruje dyrektywę. Ponadto tag używany w komentarzu do kompilacji nie może zawierać myślnika, chociaż znaki podkreślenia są dozwolone. Na przykład, // +build unit-testsnie zadziała, podczas gdy // +build unit_testswill
adamc
6
Jak radzić sobie z dzikimi kartami? go test -tags=integration ./...nie działa, ignoruje tag
Erika Dsouza
54

Wypracowanie na mój komentarz do @ doskonałą odpowiedź Ainar-G, w ciągu ostatniego roku byłem za pomocą kombinacji -shortzIntegration konwencją nazewnictwa, aby osiągnąć to, co najlepsze z obu światów.

Testy zgodności jednostek i integracji w tym samym pliku

Budowanie flag wcześniej zmusiło mnie do posiadania wielu plików ( services_test.go,services_integration_test.go itp).

Zamiast tego weźmy poniższy przykład, gdzie pierwsze dwa to testy jednostkowe, a na końcu mam test integracji:

package services

import "testing"

func TestServiceFunc(t *testing.T) {
    t.Parallel()
    ...
}

func TestInvalidServiceFunc3(t *testing.T) {
    t.Parallel()
    ...
}

func TestPostgresVersionIntegration(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping integration test")
    }
    ...
}

Zauważ, że ostatni test ma konwencję:

  1. za pomocą Integration w nazwie testu.
  2. sprawdzenie, czy działa pod -shortdyrektywą flag.

Zasadniczo specyfikacja brzmi: „pisz wszystkie testy normalnie. Jeśli są to testy długotrwałe lub test integracji, postępuj zgodnie z konwencją nazewnictwa i sprawdź, czy -short są one miłe dla rówieśników”.

Uruchom tylko testy jednostkowe:

go test -v -short

zapewnia to ładny zestaw komunikatów, takich jak:

=== RUN   TestPostgresVersionIntegration
--- SKIP: TestPostgresVersionIntegration (0.00s)
        service_test.go:138: skipping integration test

Uruchom tylko testy integracji:

go test -run Integration

To uruchamia tylko testy integracji. Przydatne do testowania dymu kanarków w produkcji.

Oczywiście wadą tego podejścia jest to, że jeśli ktokolwiek uruchomi się go testbez -shortflagi, domyślnie uruchomi wszystkie testy - testy jednostkowe i integracyjne.

W rzeczywistości, jeśli Twój projekt jest wystarczająco duży, aby mieć testy jednostkowe i integracyjne, najprawdopodobniej używasz w nim Makefileprostego dyrektywy go test -short. Lub po prostu umieść to w swoim README.mdpliku i nazwij to dniem.

eduncan911
źródło
3
uwielbiam prostotę
Jacob Stanley,
Czy tworzysz oddzielny pakiet dla takiego testu, aby uzyskać dostęp tylko do publicznych części pakietu? Czy wszystko mieszane?
Dr eel
@ Dr.eel Cóż, to jest OT z odpowiedzi. Ale osobiście wolę jedno i drugie: inną nazwę pakietu dla testów, więc mogę importmój pakiet i przetestować go, co kończy się pokazaniem mi, jak wygląda moje API innym. Następnie podążam za wszelką pozostałą logiką, która musi zostać uwzględniona jako wewnętrzne nazwy pakietów testowych.
eduncan911
@ eduncan911 Dzięki za odpowiedź! Tak więc, jak rozumiem, package serviceszawiera test integracji, więc aby przetestować APIfo pakiet jako czarną skrzynkę, nazwijmy go w inny sposób package services_integration_test, nie da nam to szansy na pracę ze strukturami wewnętrznymi. Dlatego pakiet do testów jednostkowych (dostęp do elementów wewnętrznych) powinien mieć nazwę package services. Czy tak jest?
Dr eel
Zgadza się, tak. Oto czysty przykład tego, jak to robię: github.com/eduncan911/podcast (zwróć uwagę na 100% pokrycie kodu, używając przykładów)
eduncan911
50

Widzę trzy możliwe rozwiązania. Pierwszym jest użycie trybu skróconego do testów jednostkowych. Więc używałbyś go test -shortz testami jednostkowymi i tym samym, ale bez-short flagi, aby uruchomić również testy integracji. Biblioteka standardowa korzysta z trybu skróconego, aby pomijać długotrwałe testy lub przyspieszyć ich działanie, udostępniając prostsze dane.

Drugim jest użycie konwencji i wywołanie testów albo TestUnitFoolub, TestIntegrationFooa następnie użycie -runflagi testing, aby wskazać, które testy mają zostać wykonane. Więc używałbyś go test -run 'Unit'do testów jednostkowych igo test -run 'Integration' testów integracyjnych.

Trzecią opcją jest użycie zmiennej środowiskowej i umieszczenie jej w konfiguracji testów za pomocą os.Getenv. Następnie możesz użyć prostego go testdo testów jednostkowych iFOO_TEST_INTEGRATION=true go test testów integracyjnych.

Osobiście wolałbym to -shortrozwiązanie, ponieważ jest prostsze i jest używane w standardowej bibliotece, więc wydaje się, że jest to de facto sposób na oddzielenie / uproszczenie długotrwałych testów. Ale rozwiązania -runi os.Getenvoferują większą elastyczność (wymagana jest również większa ostrożność, ponieważ używane są wyrażenia regularne -run).

Ainar-G
źródło
1
Zwróć uwagę, że społecznościowe Tester-Goprogramy uruchamiające testy (np. ) wspólne dla IDE (Atom, Sublime itp.) mają wbudowaną opcję uruchamiania z -shortflagą, wraz z -coverageinnymi. w związku z tym w nazwie testu używam kombinacji opcji Integracja, a także if testing.Short()sprawdzeń w ramach tych testów. pozwala mi mieć to, co najlepsze z obu światów: działać w -shortramach IDE i jawnie uruchamiać tylko testy integracji zgo test -run "Integration"
eduncan911
5

Ostatnio próbowałem znaleźć rozwiązanie tego samego. To były moje kryteria:

  • Rozwiązanie musi być uniwersalne
  • Brak oddzielnego pakietu do testów integracyjnych
  • Separacja powinna być kompletna (powinienem być w stanie uruchomić tylko testy integracyjne )
  • Brak specjalnej konwencji nazewnictwa testów integracji
  • Powinien dobrze działać bez dodatkowego oprzyrządowania

Powyższe rozwiązania (niestandardowa flaga, niestandardowy tag kompilacji, zmienne środowiskowe) tak naprawdę nie spełniały wszystkich powyższych kryteriów, więc po krótkim kopaniu i zabawie wpadłem na takie rozwiązanie:

package main

import (
    "flag"
    "regexp"
    "testing"
)

func TestIntegration(t *testing.T) {
    if m := flag.Lookup("test.run").Value.String(); m == "" || !regexp.MustCompile(m).MatchString(t.Name()) {
        t.Skip("skipping as execution was not requested explicitly using go test -run")
    }

    t.Parallel()

    t.Run("HelloWorld", testHelloWorld)
    t.Run("SayHello", testSayHello)
}

Wdrożenie jest proste i minimalne. Chociaż wymaga prostej konwencji do testów, ale jest mniej podatny na błędy. Dalszym ulepszeniem może być eksportowanie kodu do funkcji pomocniczej.

Stosowanie

Uruchom testy integracji tylko dla wszystkich pakietów w projekcie:

go test -v ./... -run ^TestIntegration$

Uruchom wszystkie testy ( zwykłe i integracyjne):

go test -v ./... -run .\*

Wykonuj tylko regularne testy:

go test -v ./...

To rozwiązanie działa dobrze bez narzędzi, ale plik Makefile lub niektóre aliasy mogą ułatwić użytkownikowi. Można go również łatwo zintegrować z dowolnym IDE, które obsługuje uruchamianie testów go.

Pełny przykład można znaleźć tutaj: https://github.com/sagikazarmark/modern-go-application

mark.sagikazar
źródło