O Paralelepípedo de testes
Quebrando a pirâmide de testes propositalmente
O contexto:
Micro-serviços
Serviços pequenos request/response com dependências
A pirâmide:
MUITOS testes de unidade
Vários de integração
alguns de usuário
Por que a pirâmide existe?
Testes de unidade são rápidos mas não pegam problemas de integração.
Testes de integração são mais lentos e difíceis de debugar mas pegam mais problemas
Testes de usuário exercitam o que o usuário vê mas são lentos e pouco confiáveis
Os problemas que vêm com a pirâmide:
Testes de unidade muito simples que não pegam problemas reais
Testes de integração com muita repetição de lógica das unidades
Testes de usuário apenas para casos de sucesso que rodam várias vezes
A solução:
Injeção de dependência
FIM!
O desenvolvimento dessa ideia fica a cargo da audiência
Teste é código
que dependende do código sob teste
para dar 👍 ou 👎
Exemplo de código go http
func main() {
s := server.NewHTTPServer()
ll := log.New(os.Stdout, "HTTP", 0)
addr := net.JoinHostPort("", "8080")
if err := http.ListenAndServe(addr, s); err != nil {
ll.Fatal("HTTP(s) server failed")
}
}
cmd/server/main.go
type Server struct {
*mux.Router
}
func NewHTTPServer() *Server {
r := mux.NewRouter()
r.HandleFunc("/", helloWorld).Methods(http.MethodGet)
return &Server{r}
}
func helloWorld(w http.ResponseWriter, req *http.Request) {
msg := "Hello, world"
fmt.Println("Responding request")
w.Write([]byte(msg))
}
internal/server/http.go
https://github.com/gorilla/mux
func TestHelloWord(t *testing.T) {
w := httptest.NewRecorder()
helloWorld(w, nil)
require.Equal(t, http.StatusOK, w.Code, "expected status code to match")
body := string(w.Body.Bytes())
assert.Equal(t, "Hello, world", body, "expected body to match")
}
internal/server/http_internal_test.go
https://github.com/stretchr/testify
Não testa a integração com a biblioteca mux (nem o código de NewServer)
func TestHomeReturnsHelloWorldLocal(t *testing.T) {
s := server.NewHTTPServer()
w := httptest.NewRecorder()
httpReq := httptest.NewRequest(http.MethodGet, "/", nil)
s.ServeHTTP(w, httpReq)
require.Equal(t, http.StatusOK, w.Code, "expected status code to match for req %+v", httpReq)
body := string(w.Body.Bytes())
assert.Equal(t, "Hello, world", body, "expected body to match")
}
internal/server/http_test.go
Melhor, mas não lida com (de)serialização do http
func TestHomeReturnsHelloWorld(t *testing.T) {
s := server.NewHTTPServer()
listener, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatalf("could not listen for HTTP requests: %+v", err)
}
baseURL := "http://" + listener.Addr().String()
srvr := http.Server{Addr: baseURL, Handler: s}
go srvr.Serve(listener)
defer func() {
if err := srvr.Shutdown(context.Background()); err != nil {
t.Logf("could not shutdown http server: %+v", err)
}
}()
httpReq, err := http.NewRequest(http.MethodGet, baseURL+"/", nil)
require.NoError(t, err, "could not create GET / request")
resp, err := http.DefaultClient.Do(httpReq)
require.NoError(t, err, "error making request %+v", httpReq)
require.Equal(t, http.StatusOK, resp.StatusCode, "expected status code to match for req %+v", httpReq)
body, err := readBodyFrom(resp)
require.NoError(t, err, "unexpected error reading response body")
assert.Equal(t, "Hello, world", body, "expected body to match")
}
internal/server/http_test.go
Agora testa tudo: subir o servidor, executar o código e fechar tudo
func TestHomeReturnsHelloWorldLocal(t *testing.T) {
s := server.NewHTTPServer()
w := httptest.NewRecorder()
httpReq := httptest.NewRequest(http.MethodGet, "/", nil)
s.ServeHTTP(w, httpReq)
require.Equal(t, http.StatusOK, w.Code, "expected status code to match for req %+v", httpReq)
body := string(w.Body.Bytes())
assert.Equal(t, "Hello, world", body, "expected body to match")
}
func TestHomeReturnsHelloWorld(t *testing.T) {
baseURL, stop := startTestingHTTPServer(t)
defer stop()
httpReq, err := http.NewRequest(http.MethodGet, baseURL+"/", nil)
require.NoError(t, err, "could not create GET / request")
resp, err := http.DefaultClient.Do(httpReq)
require.NoError(t, err, "error making request %+v", httpReq)
require.Equal(t, http.StatusOK, resp.StatusCode, "expected status code to match for req %+v", httpReq)
body, err := readBodyFrom(resp)
require.NoError(t, err, "unexpected error reading response body")
assert.Equal(t, "Hello, world", body, "expected body to match")
}
internal/server/http_test.go
MUITA duplicação! Dá pra refatorar
func TestHomeReturnsHelloWorldLocal(t *testing.T) {
s := server.NewHTTPServer()
baseURL := ""
makeRequest := func(r *http.Request) (*http.Response, error) {
w := httptest.NewRecorder()
s.ServeHTTP(w, r)
return &http.Response{
StatusCode: w.Code,
Body: ioutil.NopCloser(bytes.NewReader(w.Body.Bytes())),
}, nil
}
httpReq, err := http.NewRequest(http.MethodGet, baseURL+"/", nil)
require.NoError(t, err, "could not create GET / request")
resp, err := makeRequest(httpReq)
require.NoError(t, err, "error making request %+v", httpReq)
require.Equal(t, http.StatusOK, resp.StatusCode, "expected status code to match for req %+v", httpReq)
body, err := readBodyFrom(resp)
require.NoError(t, err, "unexpected error reading response body")
assert.Equal(t, "Hello, world", body, "expected body to match")
}
func TestHomeReturnsHelloWorld(t *testing.T) {
baseURL, stop := startTestingHTTPServer(t)
defer stop()
makeRequest := func(r *http.Request) (*http.Response, error) {
return http.DefaultClient.Do(r)
}
httpReq, err := http.NewRequest(http.MethodGet, baseURL+"/", nil)
require.NoError(t, err, "could not create GET / request")
resp, err := makeRequest(httpReq)
require.NoError(t, err, "error making request %+v", httpReq)
require.Equal(t, http.StatusOK, resp.StatusCode, "expected status code to match for req %+v", httpReq)
body, err := readBodyFrom(resp)
require.NoError(t, err, "unexpected error reading response body")
assert.Equal(t, "Hello, world", body, "expected body to match")
}
internal/server/http_test.go
Bem mais semelhante
type HTTPClient interface {
Do(r *http.Request) (*http.Response, error)
}
type InMemoryHTTPClient struct {
server *server.Server
}
func (c *InMemoryHTTPClient) Do(r *http.Request) (*http.Response, error) {
w := httptest.NewRecorder()
c.server.ServeHTTP(w, r)
return &http.Response{
StatusCode: w.Code,
Body: ioutil.NopCloser(bytes.NewReader(w.Body.Bytes())),
}, nil
}
func TestHomeReturnsHelloWorldLocal(t *testing.T) {
s := server.NewHTTPServer()
httpClient := &InMemoryHTTPClient{server: s}
baseURL := ""
httpReq, err := http.NewRequest(http.MethodGet, baseURL+"/", nil)
require.NoError(t, err, "could not create GET / request")
resp, err := httpClient.Do(httpReq)
require.NoError(t, err, "error making request %+v", httpReq)
require.Equal(t, http.StatusOK, resp.StatusCode, "expected status code to match for req %+v", httpReq)
body, err := readBodyFrom(resp)
require.NoError(t, err, "unexpected error reading response body")
assert.Equal(t, "Hello, world", body, "expected body to match")
}
func TestHomeReturnsHelloWorld(t *testing.T) {
baseURL, stop := startTestingHTTPServer(t)
defer stop()
httpClient := http.DefaultClient
httpReq, err := http.NewRequest(http.MethodGet, baseURL+"/", nil)
require.NoError(t, err, "could not create GET / request")
resp, err := httpClient.Do(httpReq)
require.NoError(t, err, "error making request %+v", httpReq)
require.Equal(t, http.StatusOK, resp.StatusCode, "expected status code to match for req %+v", httpReq)
body, err := readBodyFrom(resp)
require.NoError(t, err, "unexpected error reading response body")
assert.Equal(t, "Hello, world", body, "expected body to match")
}
internal/server/http_test.go
Agora está bem claro, as dependências são diferentes, mas o teste é o mesmo
Refatora para injetar as dependências
func withDependencies(baseT *testing.T, test func(*testing.T, string, HTTPClient)) {
testStates := map[string]func(*testing.T) (string, func(), HTTPClient){
"unitServerTest": func(*testing.T) (string, func(), HTTPClient) {
s := server.NewHTTPServer()
httpClient := &InMemoryHTTPClient{server: s}
return "", func() {}, httpClient
},
"integrationServerTest": func(t *testing.T) (string, func(), HTTPClient) {
baseURL, stop := startTestingHTTPServer(t)
return baseURL, stop, http.DefaultClient
},
}
for name, dep := range testStates {
baseT.Run(name, func(t *testing.T) {
baseURL, stop, client := dep(t)
defer stop()
test(t, baseURL, client)
})
}
}
internal/server/http_test.go
Agora podemos apagar o TestHomeReturnsHelloWorldLocal e usar no TestHomeReturnsHelloWorld
func TestHomeReturnsHelloWorld(t *testing.T) {
withDependencies(t, func(t *testing.T, baseURL string, httpClient HTTPClient) {
httpReq, err := http.NewRequest(http.MethodGet, baseURL+"/", nil)
require.NoError(t, err, "could not create GET / request")
resp, err := httpClient.Do(httpReq)
require.NoError(t, err, "error making request %+v", httpReq)
require.Equal(t, http.StatusOK, resp.StatusCode, "expected status code to match for req %+v", httpReq)
body, err := readBodyFrom(resp)
require.NoError(t, err, "unexpected error reading response body")
assert.Equal(t, "Hello, world", body, "expected body to match")
})
}
internal/server/http_test.go
Lucrinho
$ $ go test -v ./...
? github.com/hugocorbucci/multitest-go-example/cmd/server [no test files]
=== RUN TestHelloWord
Responding request
--- PASS: TestHelloWord (0.00s)
=== RUN TestHomeReturnsHelloWorld
=== RUN TestHomeReturnsHelloWorld/unitServerTest
Responding request
=== RUN TestHomeReturnsHelloWorld/integrationServerTest
Responding request
--- PASS: TestHomeReturnsHelloWorld (0.00s)
--- PASS: TestHomeReturnsHelloWorld/unitServerTest (0.00s)
--- PASS: TestHomeReturnsHelloWorld/integrationServerTest (0.00s)
PASS
ok github.com/hugocorbucci/multitest-go-example/internal/server 0.148s
Aproveita pra lucrar mais
Introduz a habilidade de testar serviço externo
func withDependencies(baseT *testing.T, test func(*testing.T, string, HTTPClient)) {
if len(os.Getenv("TARGET_URL")) == 0 {
testStates := map[string]func(*testing.T) (string, func(), HTTPClient){
"unitServerTest": func(*testing.T) (string, func(), HTTPClient) {
s := server.NewHTTPServer()
httpClient := &InMemoryHTTPClient{server: s}
return "", func() {}, httpClient
},
"integrationServerTest": func(t *testing.T) (string, func(), HTTPClient) {
baseURL, stop := startTestingHTTPServer(t)
return baseURL, stop, http.DefaultClient
},
}
for name, dep := range testStates {
baseT.Run(name, func(t *testing.T) {
baseURL, stop, client := dep(t)
defer stop()
test(t, baseURL, client)
})
}
} else {
test(baseT, os.Getenv("TARGET_URL"), http.DefaultClient)
}
}
internal/server/http_test.go
Mais lucro
$ TARGET_URL=http://localhost:8080 go test -v ./...
? github.com/hugocorbucci/multitest-go-example/cmd/server [no test files]
=== RUN TestHelloWord
Responding request
--- PASS: TestHelloWord (0.00s)
=== RUN TestHomeReturnsHelloWorld
--- PASS: TestHomeReturnsHelloWorld (0.00s)
PASS
ok github.com/hugocorbucci/multitest-go-example/internal/server 0.034s
$ TARGET_URL=http://138.197.59.82 go test -v ./...
? github.com/hugocorbucci/multitest-go-example/cmd/server [no test files]
=== RUN TestHelloWord
Responding request
--- PASS: TestHelloWord (0.00s)
=== RUN TestHomeReturnsHelloWorld
--- PASS: TestHomeReturnsHelloWorld (0.02s)
PASS
ok github.com/hugocorbucci/multitest-go-example/internal/server 0.065s
Mesmo código, testes em memória, local ou remoto!
func TestHomeReturnsHelloWorld(t *testing.T) {
withDependencies(t, func(baseURL string, httpClient HTTPClient) {
httpReq, err := http.NewRequest(http.MethodGet, baseURL+"/", nil)
require.NoError(t, err, "could not create GET / request")
resp, err := httpClient.Do(httpReq)
require.NoError(t, err, "error making request %+v", httpReq)
require.Equal(t, http.StatusOK, resp.StatusCode, "expected status code to match for req %+v", httpReq)
body, err := readBodyFrom(resp)
require.NoError(t, err, "unexpected error reading response body")
assert.Equal(t, "Hello, world", body, "expected body to match")
})
}
internal/server/http_test.go
Pausa para perguntas
Próximo passo:
adicionar dependências no serviço em si
Introduzindo dependência no sistema (e no teste)
func main() {
ll := log.New(os.Stdout, "HTTP - ", 0)
addr := net.JoinHostPort("", port)
dbConn := os.Getenv("DB_CONN")
if len(dbConn) == 0 {
dbConn = "root:sekret@tcp(localhost:3306)/multitest"
}
db := initializeStore(context.Background(), dbConn, ll)
repo := storage.NewSQLStore(db)
s := server.NewHTTPServer(repo)
if err := http.ListenAndServe(addr, s); err != nil {
ll.Fatal("HTTP(s) server failed")
}
}
cmd/server/main.go
Corrigindo o servidor
type httpHandler struct {
repo storage.Repository
}
// NewHTTPServer creates a new server
func NewHTTPServer(repo storage.Repository) *Server {
handler := &httpHandler{repo}
r := mux.NewRouter()
r.HandleFunc("/", handler.helloWorld).Methods(http.MethodGet)
return &Server{
r,
}
}
func (h *httpHandler) helloWorld(w http.ResponseWriter, req *http.Request) {
msg := "Hello, world"
fmt.Println("Responding request")
w.Write([]byte(msg))
}
internal/server/http.go
Corrigindo os testes de unidade
func TestHelloWord(t *testing.T) {
handler := &httpHandler{}
w := httptest.NewRecorder()
handler.helloWorld(w, nil)
require.Equal(t, http.StatusOK, w.Code, "expected status code to match")
body := string(w.Body.Bytes())
assert.Equal(t, "Hello, world", body, "expected body to match")
}
internal/server/http_internal_test.go
Corrigindo os testes de integração
func withDependencies(baseT *testing.T, test func(*testing.T, string, HTTPClient)) {
if len(os.Getenv("TARGET_URL")) == 0 {
testStates := map[string]func(*testing.T) (string, func(), HTTPClient){
"unitServerTest": func(t *testing.T) (string, func(), HTTPClient) {
repo := stubs.NewStubRepository(nil)
s := server.NewHTTPServer(repo)
httpClient := &InMemoryHTTPClient{server: s}
return "", func() {}, httpClient
},
"integrationServerTest": func(t *testing.T) (string, func(), HTTPClient) {
ctx := context.Background()
inMemoryStore, err := sqltesting.NewMemoryStore(ctx, sqltesting.NewTestingLog(t))
require.NoError(t, err, "error creating in memory store")
baseURL, stop := startTestingHTTPServer(t, inMemoryStore)
return baseURL, stop, http.DefaultClient
},
}
for name, dep := range testStates {
baseT.Run(name, func(t *testing.T) {
baseURL, stop, client := dep(t)
defer stop()
test(t, baseURL, client)
})
}
} else {
test(baseT, os.Getenv("TARGET_URL"), http.DefaultClient)
}
}
internal/server/http_test.go
Corrigindo os testes de integração (2)
func startTestingHTTPServer(t *testing.T, inMemoryStore *sqltesting.MemoryStore) (string, func()) {
ctx := context.Background()
s := server.NewHTTPServer(inMemoryStore)
listener, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatalf("could not listen for HTTP requests: %+v", err)
}
addr := "http://" + listener.Addr().String()
srvr := http.Server{Addr: addr, Handler: s}
go srvr.Serve(listener)
return addr, func() {
if err := srvr.Shutdown(ctx); err != nil {
t.Logf("could not shutdown http server: %+v", err)
}
}
}
internal/server/http_test.go
E esse MemoryStore mágico?
// NewMemoryStore returns a sqlite "in-memory" repository for local tests.
func NewMemoryStore(ctx context.Context, l *log.Logger) (*MemoryStore, error) {
db, err := NewSQLiteDB(ctx, l)
if err != nil {
return nil, err
}
return &MemoryStore{
DB: db,
Store: storage.NewSQLStore(db),
}, nil
}
// NewSQLiteDB creates a SQLite DB with the default structure
func NewSQLiteDB(ctx context.Context, l *log.Logger) (*sql.DB, error) {
return sql.Open("sqlite3", "file::memory:?mode=memory&cache=shared")
}
internal/storage/sqltesting/memory.go
Adicionando uma funcionalidade
// NewHTTPServer creates a new server
func NewHTTPServer(repo storage.Repository) *Server {
handler := &httpHandler{repo}
r := mux.NewRouter()
r.HandleFunc("/", handler.helloWorld).Methods(http.MethodGet)
r.HandleFunc("/s/{short:[a-f0-9]+}", handler.shortURL).Methods(http.MethodGet)
return &Server{
r,
}
}
internal/server/http.go
Adicionando uma funcionalidade
func (h *httpHandler) shortURL(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
short := mux.Vars(req)["short"]
if len(short) != 12 {
w.WriteHeader(http.StatusNotFound)
return
}
longURL, err := h.repo.ExpandShortURL(ctx, short)
if err != nil {
if err == sql.ErrNoRows {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("404 page not found\n"))
} else {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf("%+v", err)))
}
return
}
w.WriteHeader(http.StatusFound)
w.Header().Set("Location", longURL)
}
internal/server/http.go
Adicionando uma funcionalidade
func (h *httpHandler) shortURL(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
short := mux.Vars(req)["short"]
if len(short) != 12 {
w.WriteHeader(http.StatusNotFound)
return
}
longURL, err := h.repo.ExpandShortURL(ctx, short)
if err != nil {
if err == sql.ErrNoRows {
w.WriteHeader(http.StatusNotFound)
} else {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(fmt.Sprintf("%+v", err)))
}
return
}
w.WriteHeader(http.StatusFound)
w.Header().Set("Location", longURL)
}
internal/server/http.go
Testando
func TestShortURLReturnsNotFoundForInvalidURL(baseT *testing.T) {
withDependencies(baseT, func(t *testing.T, baseURL string, httpClient HTTPClient) {
httpReq, err := http.NewRequest(http.MethodGet, baseURL+"/s/invalid", nil)
require.NoError(t, err, "could not create GET / request")
resp, err := httpClient.Do(httpReq)
require.NoError(t, err, "error making request %+v", httpReq)
require.Equal(t, http.StatusNotFound, resp.StatusCode, "expected status code to match for req %+v", httpReq)
body, err := readBodyFrom(resp)
require.NoError(t, err, "unexpected error reading response body")
assert.Equal(t, "404 page not found\n", body, "expected body to match")
})
}
internal/server/http_test.go
Testando
func TestShortURLReturnsNotFoundForUnknown(baseT *testing.T) {
withDependencies(baseT, func(t *testing.T, baseURL string, httpClient HTTPClient) {
httpReq, err := http.NewRequest(http.MethodGet, baseURL+"/s/123456789012", nil)
require.NoError(t, err, "could not create GET / request")
resp, err := httpClient.Do(httpReq)
require.NoError(t, err, "error making request %+v", httpReq)
require.Equal(t, http.StatusNotFound, resp.StatusCode, "expected status code to match for req %+v", httpReq)
body, err := readBodyFrom(resp)
require.NoError(t, err, "unexpected error reading response body")
assert.Equal(t, "404 page not found\n", body, "expected body to match")
})
}
internal/server/http_test.go
Testando
func TestShortURLReturnsFoundForValidURL(baseT *testing.T) {
withDependencies(baseT, func(t *testing.T, baseURL string, httpClient HTTPClient) {
mocking := false
input := "123456789012"
output := "https://www.digitalocean.com"
if mocking {
// TODO: Mockar
} else {
// TODO: cadastrar a URL
}
httpReq, err := http.NewRequest(http.MethodGet, baseURL+"/s/"+input, nil)
require.NoError(t, err, "could not create GET / request")
resp, err := httpClient.Do(httpReq)
require.NoError(t, err, "error making request %+v", httpReq)
require.Equal(t, http.StatusFound, resp.StatusCode, "expected status code to match for req %+v", httpReq)
body, err := readBodyFrom(resp)
require.NoError(t, err, "unexpected error reading response body")
assert.Equal(t, "", body, "expected body to match")
assert.Equal(t, output, resp.Header.Get("Location"), "expected location to match")
})
}
internal/server/http_test.go
Xi, ferrou! Como resolver?
A solução:
Injeção de dependência
// TestDependencies encapsulates the dependencies needed to run a test
type TestDependencies struct {
BaseURL string
HTTPClient HTTPClient
}
func withDependencies(baseT *testing.T, test func(*testing.T, *TestDependencies)) {
if len(os.Getenv("TARGET_URL")) == 0 {
testStates := map[string]func(*testing.T) (*TestDependencies, func()){
"unitServerTest": func(t *testing.T) (*TestDependencies, func()) {
repo := stubs.NewStubRepository(nil)
s := server.NewHTTPServer(repo)
httpClient := &InMemoryHTTPClient{server: s}
return &TestDependencies{BaseURL: "", HTTPClient: httpClient}, func() {}
},
"integrationServerTest": func(t *testing.T) (*TestDependencies, func()) {
ctx := context.Background()
inMemoryStore, err := sqltesting.NewMemoryStore(ctx, sqltesting.NewTestingLog(t))
require.NoError(t, err, "error creating in memory store")
baseURL, stop := startTestingHTTPServer(t, inMemoryStore)
return &TestDependencies{BaseURL: baseURL, HTTPClient: http.DefaultClient}, stop
},
}
for name, dep := range testStates {
baseT.Run(name, func(t *testing.T) {
deps, stop := dep(t)
defer stop()
test(t, deps)
})
}
} else {
test(baseT, &TestDependencies{BaseURL: os.Getenv("TARGET_URL"), HTTPClient: http.DefaultClient})
}
}
func withDependencies(baseT *testing.T, test func(*testing.T, *TestDependencies)) {
if len(os.Getenv("TARGET_URL")) == 0 {
testStates := map[string]func(*testing.T) (*TestDependencies, func()){
"unitServerTest": unitDependencies,
"integrationServerTest": integrationDependencies,
}
for name, dep := range testStates {
baseT.Run(name, func(t *testing.T) {
deps, stop := dep(t)
defer stop()
test(t, deps)
})
}
} else {
test(baseT, smokeDependencies(baseT))
}
}
func unitDependencies(t *testing.T) (*TestDependencies, func()) {
repo := stubs.NewStubRepository(nil)
s := server.NewHTTPServer(repo)
httpClient := &InMemoryHTTPClient{server: s}
return &TestDependencies{HTTPClient: httpClient}, func() {}
}
func integrationDependencies(t *testing.T) (*TestDependencies, func()) {
ctx := context.Background()
inMemoryStore, err := sqltesting.NewMemoryStore(ctx, sqltesting.NewTestingLog(t))
require.NoError(t, err, "error creating in memory store")
baseURL, stop := startTestingHTTPServer(t, inMemoryStore)
return &TestDependencies{BaseURL: baseURL, HTTPClient: http.DefaultClient}, stop
}
func smokeDependencies(_ *testing.T) *TestDependencies {
return &TestDependencies{BaseURL: os.Getenv("TARGET_URL"), HTTPClient: http.DefaultClient}
}
internal/server/http_test.go
type TestDependencies struct {
BaseURL string
HTTPClient HTTPClient
DB storage.Repository
}
func unitDependencies(t *testing.T) (*TestDependencies, func()) {
repo := stubs.NewStubRepository(nil)
s := server.NewHTTPServer(repo)
httpClient := &InMemoryHTTPClient{server: s}
return &TestDependencies{
HTTPClient: httpClient,
DB: repo,
}, func() {}
}
func integrationDependencies(t *testing.T) (*TestDependencies, func()) {
ctx := context.Background()
inMemoryStore, err := sqltesting.NewMemoryStore(ctx, sqltesting.NewTestingLog(t))
require.NoError(t, err, "error creating in memory store")
baseURL, stop := startTestingHTTPServer(t, inMemoryStore)
return &TestDependencies{
BaseURL: baseURL,
HTTPClient: http.DefaultClient,
DB: inMemoryStore.Store,
}, stop
}
func smokeDependencies(t *testing.T) *TestDependencies {
ctx := context.Background()
l := sqltesting.NewTestingLog(t)
db := storage.InitializeStore(ctx, "mysql", os.Getenv("DB_CONN"), l, 1, 10*time.Second)
return &TestDependencies{
BaseURL: os.Getenv("TARGET_URL"),
HTTPClient: http.DefaultClient,
DB: storage.NewSQLStore(db),
}
}
internal/server/http_test.go
func TestShortURLReturnsFoundForValidURL(baseT *testing.T) {
withDependencies(baseT, func(t *testing.T, deps *TestDependencies) {
repo, mocking := deps.DB.(*stubs.Repository)
input := "123456789012"
output := "https://www.digitalocean.com"
if mocking {
repo.Add(input, output)
} else {
// TODO: cadastrar a URL
}
httpReq, err := http.NewRequest(http.MethodGet, deps.BaseURL+"/s/"+input, nil)
require.NoError(t, err, "could not create GET / request")
resp, err := deps.HTTPClient.Do(httpReq)
require.NoError(t, err, "error making request %+v", httpReq)
require.Equal(t, http.StatusFound, resp.StatusCode, "expected status code to match for req %+v", httpReq)
body, err := readBodyFrom(resp)
require.NoError(t, err, "unexpected error reading response body")
assert.Equal(t, "", body, "expected body to match")
assert.Equal(t, output, resp.Header.Get("Location"), "expected location to match")
})
}
internal/server/http_test.go
Já deu pra entender né?
No fim, seu teste é um sistema
Use as técnicas que você usa
para o seu sistema de produção
no seu sistema de teste
Pô Hugo, mas isso aí na real vai demorar muito!
$ for f in $(find . -name '*_test.go'); do grep 'func Test' $f; done | wc -l
179
$ for f in $(find . -name '*_test.go'); do cat $f; done | wc -l
4893
$ for f in $(find . -name '*.go' | grep -v '_test.go'); do cat $f; done | wc -l
4811 64 1528 190 7986
$ time make test
go test -tags=integration ./...
...
74.55 real 145.33 user 27.76 sys
$ PUBLIC_SMOKE_TESTS=true ENV=production make smoke_tests
../smoke_tests/bin/run
...
[go] Task status: passed, took: 1m 46.536s
Outras vantagens que tivemos
Usar essa suite para evitar problema de desempenho em banco de dados
Rodar essa suite periodicamente de fora do sistema para achar problemas externos
Nossa arquitetura é testável pelos nossos usuários
Coisas que ainda queremos fazer
Usar o setup do mock no modo de integração para validar resultados reais
Melhorar o sistema de preparação do banco/dados
Tentar em outros contextos
É isso aí
Obrigado!
hcorbucci@digitalocean.com
twitter.com/hugocorbucci