Isolating Dependencies with Test Doubles: Fakes

Introduction to Fakes in TDD

Welcome to our lesson on using Fakes as test doubles in Test Driven Development (TDD) with Go and Testify. In this lesson, you'll explore how fakes can streamline your testing by simulating real-world components. Our journey so far has exposed you to various test doubles like dummies, stubs, and mocks. Now, we'll dive into fakes, which enable you to create realistic implementations that mirror complex dependencies, making your tests more robust and reliable. As always, we'll practice the TDD cycle: Red, Green, Refactor, as we see how fakes fit into our testing strategy.

Code Example and Walkthrough: Implementing an In-memory Fake Repository

Let's see how to implement a simple fake: an InMemoryUserRepository. This serves as a stand-in for a real database repository, providing controlled behavior for our tests.

Create a file in_memory_user_repository.go:

package users

import (
    "fmt"
    "time"
)

type InMemoryUserRepository struct {
    users     map[string]*User
    currentId int
}

func NewInMemoryUserRepository() *InMemoryUserRepository {
    return &InMemoryUserRepository{
        users:     make(map[string]*User),
        currentId: 1,
    }
}

func (r *InMemoryUserRepository) generateId() string {
    id := fmt.Sprintf("%d", r.currentId)
    r.currentId++
    return id
}

func (r *InMemoryUserRepository) Create(userData User) (*User, error) {
    user := &User{
        Id:        r.generateId(),
        Email:     userData.Email,
        Name:      userData.Name,
        CreatedAt: time.Now(),
    }

    r.users[user.Id] = user
    return user, nil
}

func (r *InMemoryUserRepository) FindById(id string) (*User, error) {
    user, exists := r.users[id]
    if !exists {
        return nil, nil
    }
    return user, nil
}

func (r *InMemoryUserRepository) FindByEmail(email string) (*User, error) {
    for _, user := range r.users {
        if user.Email == email {
            return user, nil
        }
    }
    return nil, nil
}

func (r *InMemoryUserRepository) Update(id string, data User) (*User, error) {
    existing, exists := r.users[id]
    if !exists {
        return nil, nil
    }

    updated := &User{
        Id:        existing.Id,
        Email:     existing.Email,
        Name:      existing.Name,
        CreatedAt: existing.CreatedAt,
    }
    
    if data.Email != "" {
        updated.Email = data.Email
    }
    
    if data.Name != "" {
        updated.Name = data.Name
    }
    
    r.users[id] = updated
    return updated, nil
}

func (r *InMemoryUserRepository) Delete(id string) (bool, error) {
    if _, exists := r.users[id]; !exists {
        return false, nil
    }
    delete(r.users, id)
    return true, nil
}

func (r *InMemoryUserRepository) FindAll() ([]*User, error) {
    users := make([]*User, 0, len(r.users))
    for _, user := range r.users {
        users = append(users, user)
    }
    return users, nil
}

func (r *InMemoryUserRepository) Clear() {
    r.users = make(map[string]*User)
    r.currentId = 1
}

Explanation:

  • We use a Go map to simulate an in-memory store for users.
  • Each function mimics typical database operations such as creating and finding users.
  • The Clear method ensures data isolation between tests—a crucial feature for repeatable outcomes.

By having a controlled data store, we make sure our tests are focused on business logic and not dependent on an external database. Fakes are often quite complicated to build because they mimic the behavior of the real thing. They can be used to verify the state after your code acts on the fake, which can be very useful when you are trying to mimic the environment as best as possible without introducing the uncertainty or delay that the real implementation would introduce.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal