This tutorial series assumes you are familiar with golang, as we will be using it thorough this article series.
Testability implies that the architecture ought to ease the testing of business rules. It means you should be able to unit test your business rules without involving external dependencies.
Testability in Go (Golang) is tightly connected to how well you apply clean architecture principles—because it is fundamentally about separation of concerns and dependency control, which directly enable easy testing.
We shall now apply what we’ve learned in previous articles to unit testing.
Based on previous articles we have a good base for test.
This is how our code should look like for our service
// user/service.go
type UserService struct {
repo UserRepository
}
type UserRepository interface {
FindByID(id string) (*User, error)
}
func (s *UserService) GetUser(id string) (*User, error) {
if id == "" {
return nil, errors.New("invalid id")
}
return s.repo.FindByID(id)
}
Now unit testing is simple.
We can simply mock the UserRepository and test our service/business rule GetUser method.
// user/service_test.go
type MockUserRepository struct {}
func (m *MockUserRepository) FindByID(id string) (*User, error) {
return nil, nil
}
func TestGetUser(t *testing.T) {
repo := &MockUserRepository{}
service := UserService{repo: repo}
err := service.Get("")
if err == nil {
t.Fatal("expected error")
}
}
We often want small interfaces, and there are 2 reasons for it.
This is a good example:
type UserGetter interface {
GetUser(id string) (User, error)
}
This is a bad example… giant interfaces, that would look like this:
type UserRepository interface {
SaveUser(...)
UpdateUser(...)
DeleteUser(...)
GetUser(...)
}
To conclude, smaller interfaces… they lead to easier mocking which equals to better tests.
Integration tests focus on how multiple components work together. So for example we would test how our userRepository works with database(we can use real db or mock it).
Here is an example of integration test, notice how we are only testing repository/db interaction, and not testing our handler nor our service:
// models/user.go
type User struct {
ID string
Name string
}
// repository/user.go
type userRepo struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) *userRepo {
return userRepo{db: db}
}
func (r *userRepo) Create(user *user.User) (*user.User, error) {
stmt, err := db.Prepare("INSERT INTO users (id, name) VALUES (?, ?)")
if err != nil {
return err
}
defer stmt.Close()
_, err = stmt.Exec(user.ID, user.Name)
if err != nil {
return err
}
return user, nil
}
func (r *userRepo) FindByID(id string) (*User, error) {
row := r.db.QueryRow("SELECT id, name FROM users WHERE id = ?", id)
var user User
err := row.Scan(&user.ID, &user.Name)
return &user, err
}
// repository/user_test.go
// a simple integration, testing user creation and retrieval
func TestCreateUser(t *testing.T) {
db := setupTestDB() // real or containerized DB
repo := NewUserRepository(db)
user := user.User{Name: "Alice"}
err := repo.Create(user)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
saved, _ := repo.FindByID("Alice")
if saved.Name != "Alice" {
t.Errorf("expected Alice, got %s", saved.Name)
}
}
E2e tests on the other hand test the complete flow, in our example we setup a server, its dependencies… like db, and test by sending the request.
Here is an example:
// e2e/user_test.go
// a simple e2e, testing user creation
func TestCreateUserE2E(t *testing.T) {
server := setupTestServer()
defer server.Close()
payload := `{"name":"Alice"}`
resp, err := http.Post(server.URL+"/users", "application/json", strings.NewReader(payload))
if err != nil {
t.Fatalf("request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
t.Errorf("expected 201, got %d", resp.StatusCode)
}
}
The general rule is to favour unit tests over integration tests, and favor integration tests over e2e tests.
If you are thinking what’s the amount of each tests i should have… some rule of thumb is to have:
| Test Type | Recommended Percentage |
|---|---|
| Unit | 70–80% |
| Integration | 10–25% |
| E2E | 5–10% |
We’ve learned a bit about testing in golang, how we should use small interfaces, small tests and preference amount of each tests. We’ve also learned how unit, integration and e2e tests might look in practice.