3 min read

Dependency Injection in Go with Tests

Table of Contents

Hey there! ๐Ÿ‘‹ Letโ€™s talk about dependency injection (DI) in Go.

Sounds fancy, right?

But donโ€™t worryโ€”itโ€™s actually pretty simple (and super useful).

Why Should You Care? ๐Ÿค”

  • No frameworks needed (Go keeps it lightweight).
  • Makes testing easier (no more fighting with stdout).
  • Keeps your code flexible (write once, use anywhere).

Letโ€™s Start with a Simple Example

Imagine we have a Greet function that prints a friendly message:

func Greet(name string) {
    fmt.Printf("Hello, %s", name)
}

But how do we test this? fmt.Printf writes directly to the terminal, which is hard to check in a test.

The Problem? Tight Coupling ๐Ÿ”—

Our function is stuck printing to stdout. What if we want to:

  • Test the output?
  • Send the greeting to a file?
  • Output it in an HTTP response?

The Solution? Dependency Injection! ๐ŸŽ‰

Instead of hardcoding fmt.Printf, we can pass in where the output should go.

How? Use io.Writer!

Goโ€™s io.Writer is an interface that says:

โ€œHey, I can write bytes somewhere!โ€

Many things implement io.Writer:

  • Files (os.File)
  • HTTP responses (http.ResponseWriter)
  • Even a simple buffer (bytes.Buffer) for testing!

Letโ€™s Refactor ๐Ÿ› 

Step 1: Rewrite Greet to Accept a Writer

func Greet(writer io.Writer, name string) {
    fmt.Fprintf(writer, "Hello, %s", name) // Fprintf takes a Writer!
}

Step 2: Test It with a Buffer

func TestGreet(t *testing.T) {
    buffer := bytes.Buffer{}
    Greet(&buffer, "Chris")

    got := buffer.String()
    want := "Hello, Chris"

    if got != want {
        t.Errorf("got %q want %q", got, want)
    }
}

func TestGreetEmptyName(t *testing.T) {
    buffer := bytes.Buffer{}
    Greet(&buffer, "")

    got := buffer.String()
    want := "Hello, "

    if got != want {
        t.Errorf("got %q want %q", got, want)
    }
}

func TestGreetUnicodeName(t *testing.T) {
    buffer := bytes.Buffer{}
    Greet(&buffer, "ไธ–็•Œ")

    got := buffer.String()
    want := "Hello, ไธ–็•Œ"

    if got != want {
        t.Errorf("got %q want %q", got, want)
    }
}

โœ… Now we can test various cases without printing to the terminal!

Step 3: Use It in Real Life

func main() {
    Greet(os.Stdout, "Elodie") // Prints to terminal
}

Or even in an HTTP handler!

func MyGreeterHandler(w http.ResponseWriter, r *http.Request) {
    Greet(w, "world") // Writes to HTTP response
}

Key Takeaways ๐Ÿ—

  1. DI makes testing easy โ€“ Swap real dependencies (like stdout) with test-friendly ones (like bytes.Buffer).
  2. Decouple your code โ€“ Separate what your function does from where the output goes.
  3. Reuse everywhere โ€“ The same Greet function works in CLI apps, web servers, and tests!

But Waitโ€ฆ What About Mocking? ๐Ÿ˜ฑ

Mocking is useful (and not evil, I promise!), but sometimes the standard library already gives you what you need.

Pro Tip: Study io.Writer ๐Ÿ“š

The more you know about Goโ€™s interfaces (io.Reader, io.Writer, etc.), the more flexible your code becomes!

Next Up: Mocking โ€“ Because sometimes you do need to fake it till you make it.

Support my open source work ๐Ÿš€

Let's Connect

I'm open to discussing new opportunities. Feel free to connect with me or send me an email at [email protected]