Software and Tests
Binary Systems
Largest non-billions USD founded cloud
Founded in 2011 in NYC
Worldwide operations (13 datacenters in 7 countries)
Remote first company headquartered in NYC
500K active customers
80M HTTP requests/day
1.7M active droplets + 90PiB storage
500 deploys/day
1200 services
USD 4.5 Billion valuation
200 Engineers in a 600 people company
Not this Binary system
This Binary system
By User:Zhatt - Own work, Public Domain, https://commons.wikimedia.org/w/index.php?curid=280053
The context:
Micro-services
Small request/response services with (or without) dependencies
DigitalOcean's overall architecture
Few (3) legacy Ruby on Rails monolithic applications
Migrating to Golang microservices since 2015 using gRPC/protobufs
Different datastores depending on the needs highlighting MySQL and etcd
VERY MANY unit tests
Fair # of integration tests
Some end to end tests
Why does the pyramid exist?
Unit tests are fast
but don't catch integration problems
Integration tests are slower and harder to debug
but they catch more problems
End to end tests verify what the user sees
but are slower and less reliable
The problems that come with the pyramid:
Unit tests that are very simple and don't really catch real problems
Integration tests that repeat a lot of setup/verifications from unit tests
End to end tests only for happy paths that are most commonly exercised
THE END!
The implementation of this idea is left as an exercise to the audience
Test is code
that depends on the code under test
to output 👍 or 👎
A go http webserver 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
This doesn't test the integration with the mux library (or the NewServer code)
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
This is better, but doesn't handle HTTP (de)serialization
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
Now everything is tested: bringing the server up, executing the code and closing it all
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
A LOT of duplication! We can refactor this
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
That's a lot closer
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
Now it's obvious, the dependencies are different, but the test is the same
Refactor to inject dependencies
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
Now we can erase TestHomeReturnsHelloWorldLocal and use 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
A little profit
$ $ 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
We can profit more
Add the ability to test an external service
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
More profit
$ 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
Same code, in memory tests ("unit"), local ("integration") or remote ("smoke")!
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
Pause for questions
Next step:
add a dependency to the service itself
Adding a dependency to the system (and the test)
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
Fixing the server
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
Fixing the unit tests
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
Fixing the integration tests
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
Fixing the integration tests (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
What is this magic MemoryStore?
// 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
Adding a feature
// 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
Adding a feature
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
Adding a feature
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
Testing
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
Testing
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
Testing
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
Uh oh, that's a problem! How to fix it?
The solution:
Dependency Injection
// 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
Got it right?
In the end, your test is also software
Use the techniques you use
in your production software
for your test software
But, Hugo, that's going to be super slow!
$ 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
But, Hugo, what about flaky tests?
Other improvements we've had
Use that test suite to verify generated DB query performance
Run this suite synthetically outside of the system to find external problems
Our architecture is testable by our users (since we test it like they would)
Next steps
Use the mock/fake setups in tests to validate compability with the actual results in real
Improve the setup/preparation of data for test runs
Try in other contexts