We're sorry but this app doesn't work properly without JavaScript enabled. Please enable it to continue.

This lesson's interactive features are locked, please to keep using them

Testing With Testify

In most of our Go courses, we encourage the use of the standard library's testing package, and tend to use table-driven tests... but ThePrimeagen (ex-Netflix btw) has a different preferred approach, so we'll roll with that for this course!

No complex logic in my tests, please!!! I'll explain my approach below.

  1. I prefer bigger tests (higher level) to smaller tests (tiny units) because it means I can refactor my codebase more aggressively without having to rewrite a bunch of tests.
  2. I don't write tests for every function. I write tests for functions that I'm unlikely to get right the first time. So complicated logic, like our parser, is a perfect candidate for lots of tests.
  3. I keep my tests simple and declarative.

Point #3 is why I tend to use testify and avoid the loops required for table-driven tests. This is what the test suite looked like in Lane's way of doing things:

tests := []struct {
		name            string
		request         string
		wantMethod      string
		wantTarget      string
		wantVersion     string
		wantErr         bool
	}{
		// test cases here...
}
for _, tt := range tests {
	t.Run(tt.name, func(t *testing.T) {
		r, err := RequestFromReader(strings.NewReader(tt.request))
		if tt.wantErr && err == nil {
			t.Fatalf("expected an error, but got none")
			return
		}
		if !tt.wantErr && err != nil {
			t.Fatalf("expected no error, but got one")
			return
		}
		if err != nil {
			return
		}
		if r == nil {
			t.Fatalf("expected a request, but got none")
		}
		if r.RequestLine.Method != tt.wantMethod {
			t.Fatalf("got method = %v, want %v", r.RequestLine.Method, tt.wantMethod)
		}
		if r.RequestLine.RequestTarget != tt.wantTarget {
			t.Fatalf("got target = %v, want %v", r.RequestLine.RequestTarget, tt.wantTarget)
		}
		if r.RequestLine.HttpVersion != tt.wantVersion {
			t.Fatalf("got version = %v, want %v", r.RequestLine.HttpVersion, tt.wantVersion)
		}
	})

I prefer a simple procedural approach:

// Test: Good GET Request line
r, err := RequestFromReader(strings.NewReader("GET / HTTP/1.1\r\nHost: localhost:42069\r\nUser-Agent: curl/7.81.0\r\nAccept: */*\r\n\r\n"))
require.NoError(t, err)
require.NotNil(t, r)
assert.Equal(t, "GET", r.RequestLine.Method)
assert.Equal(t, "/", r.RequestLine.RequestTarget)
assert.Equal(t, "1.1", r.RequestLine.HttpVersion)

// Test: Good GET Request line with path
r, err = RequestFromReader(strings.NewReader("GET /coffee HTTP/1.1\r\nHost: localhost:42069\r\nUser-Agent: curl/7.81.0\r\nAccept: */*\r\n\r\n"))
require.NoError(t, err)
require.NotNil(t, r)
assert.Equal(t, "GET", r.RequestLine.Method)
assert.Equal(t, "/coffee", r.RequestLine.RequestTarget)
assert.Equal(t, "1.1", r.RequestLine.HttpVersion)

Fewer conditionals, fewer things for me to get wrong in my tests.

Assignment

We'll be working on the request line parser, so let's get our directory structure and test dependency set up.

mkdir -p ./internal/request
go get -u github.com/stretchr/testify/assert
package request

import (
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestRequestLineParse(t *testing.T) {
	assert.Equal(t, "TheTestagen", "TheTestagen")
}

Run and submit the CLI tests from the root of your project.