💾 Archived View for mediocregopher.com › assets › component-oriented-design.tgz › v1 › main_test.go captured on 2024-07-09 at 00:01:50.

View Raw

More Information

⬅️ Previous capture (2023-09-08)

➡️ Next capture (2024-08-18)

-=-=-=-=-=-=-

package main

import (
	"bytes"
	"net/http"
	"net/http/httptest"
	"reflect"
	"testing"
	"time"
)

type nullLogger struct{}

func (nullLogger) Printf(string, ...interface{}) {}

////////////////////////////////////////////////////////////////////////////////
// Test scoreboard component

type fileStub struct {
	*bytes.Buffer
}

func newFileStub(init string) *fileStub {
	return &fileStub{Buffer: bytes.NewBufferString(init)}
}

func (fs *fileStub) Truncate(i int64) error {
	fs.Buffer.Truncate(int(i))
	return nil
}

func (fs *fileStub) Seek(i int64, whence int) (int64, error) {
	return i, nil
}

func TestScoreboard(t *testing.T) {
	newScoreboard := func(t *testing.T, fileStub *fileStub, saveTicker <-chan time.Time) *scoreboard {
		t.Helper()
		scoreboard, err := newScoreboard(fileStub, saveTicker, nullLogger{})
		if err != nil {
			t.Errorf("unexpected error checking saved scored: %v", err)
		}
		return scoreboard
	}

	assertScores := func(t *testing.T, expScores, gotScores map[string]int) {
		t.Helper()
		if !reflect.DeepEqual(expScores, gotScores) {
			t.Errorf("expected scores of %+v, but instead got %+v", expScores, gotScores)
		}
	}

	assertSavedScores := func(t *testing.T, expScores map[string]int, fileStub *fileStub) {
		t.Helper()
		fileStubCp := newFileStub(fileStub.String())
		tmpScoreboard := newScoreboard(t, fileStubCp, nil)
		assertScores(t, expScores, tmpScoreboard.scores())
	}

	t.Run("loading", func(t *testing.T) {
		// make sure loading scoreboards with various file contents works
		assertSavedScores(t, map[string]int{}, newFileStub(""))
		assertSavedScores(t, map[string]int{"foo": 1}, newFileStub(`{"foo":1}`))
		assertSavedScores(t, map[string]int{"foo": 1, "bar": -2}, newFileStub(`{"foo":1,"bar":-2}`))
	})

	t.Run("tracking", func(t *testing.T) {
		scoreboard := newScoreboard(t, newFileStub(""), nil)
		assertScores(t, map[string]int{}, scoreboard.scores()) // sanity check

		scoreboard.guessedCorrect("foo")
		assertScores(t, map[string]int{"foo": 1000}, scoreboard.scores())

		scoreboard.guessedIncorrect("bar")
		assertScores(t, map[string]int{"foo": 1000, "bar": -1}, scoreboard.scores())

		scoreboard.guessedIncorrect("foo")
		assertScores(t, map[string]int{"foo": 999, "bar": -1}, scoreboard.scores())
	})

	t.Run("saving", func(t *testing.T) {
		// this test tests scoreboard's periodic save feature using a ticker
		// channel which will be written to manually. The saveLoopWaitCh is used
		// here to ensure that each ticker has been fully processed.
		ticker := make(chan time.Time)
		fileStub := newFileStub("")
		scoreboard := newScoreboard(t, fileStub, ticker)

		tick := func() {
			ticker <- time.Time{}
			scoreboard.saveLoopWaitCh <- struct{}{}
		}

		// this should not effect the save file at first
		scoreboard.guessedCorrect("foo")
		assertSavedScores(t, map[string]int{}, fileStub)

		// after the ticker the new score should get saved
		tick()
		assertSavedScores(t, map[string]int{"foo": 1000}, fileStub)

		// ticker again after no changes should save the same thing as before
		tick()
		assertSavedScores(t, map[string]int{"foo": 1000}, fileStub)

		// buffer a bunch of changes, shouldn't get saved till after tick
		scoreboard.guessedCorrect("foo")
		scoreboard.guessedCorrect("bar")
		scoreboard.guessedCorrect("bar")
		assertSavedScores(t, map[string]int{"foo": 1000}, fileStub)
		tick()
		assertSavedScores(t, map[string]int{"foo": 2000, "bar": 2000}, fileStub)
	})
}

////////////////////////////////////////////////////////////////////////////////
// Test httpHandlers component

type mockScoreboard map[string]int

func (mockScoreboard) guessedCorrect(name string) int { return 1 }

func (mockScoreboard) guessedIncorrect(name string) int { return -1 }

func (m mockScoreboard) scores() map[string]int { return m }

type mockRandSrc struct{}

func (m mockRandSrc) Int() int { return 666 }

func TestHTTPHandlers(t *testing.T) {
	mockScoreboard := mockScoreboard{"foo": 1, "bar": 2}
	httpHandlers := newHTTPHandlers(mockScoreboard, mockRandSrc{}, nullLogger{})

	assertRequest := func(t *testing.T, expCode int, expBody string, r *http.Request) {
		t.Helper()
		rw := httptest.NewRecorder()
		httpHandlers.ServeHTTP(rw, r)
		if rw.Code != expCode {
			t.Errorf("expected HTTP response code %d, got %d", expCode, rw.Code)
		} else if rw.Body.String() != expBody {
			t.Errorf("expected HTTP response body %q, got %q", expBody, rw.Body.String())
		}
	}

	r := httptest.NewRequest("GET", "/guess?name=foo&n=665", nil)
	assertRequest(t, 400, "Try higher. Your score is now -1\n", r)

	r = httptest.NewRequest("GET", "/guess?name=foo&n=667", nil)
	assertRequest(t, 400, "Try lower. Your score is now -1\n", r)

	r = httptest.NewRequest("GET", "/guess?name=foo&n=666", nil)
	assertRequest(t, 200, "Correct! Your score is now 1\n", r)

	r = httptest.NewRequest("GET", "/scores", nil)
	assertRequest(t, 200, "bar: 2\nfoo: 1\n", r)
}

////////////////////////////////////////////////////////////////////////////////
//
// httpServer is NOT tested, for the following reasons:
// * It depends on a `net.Listener`, which is not trivial to mock.
// * It does very little besides passing an httpHandlers along to an http.Server
//   and managing cleanup.
// * It isn't likely to be changed often.
// * If it were to break it would be very apparent in subsequent testing stages.
//