O Paralelepípedo de testes

Quebrando a pirâmide de testes propositalmente

Hugo Corbucci - DigitalOcean

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

Injeção de dependências

Teste é código

que dependende do código sob teste

para dar 👍 ou 👎

Exemplo de código go http

https://github.com/hugocorbucci/multitest-go-example

              
                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