Mocking dependencies
Now that we’ve explained some general patterns for testing your web application, in this chapter we’re going to get a bit more serious and write some tests for our GET /snippet/view/{id}
route.
But first, let’s talk about dependencies.
Throughout this project we’ve injected dependencies into our handlers via the application
struct, which currently looks like this:
type application struct { logger *slog.Logger snippets *models.SnippetModel users *models.UserModel templateCache map[string]*template.Template formDecoder *form.Decoder sessionManager *scs.SessionManager }
When testing, it sometimes makes sense to mock these dependencies instead of using exactly the same ones that you do in your production application.
For example, in the previous chapter we mocked the logger
dependency with a logger that write messages to io.Discard
, instead of the os.Stdout
and stream like we do in our production application:
func newTestApplication(t *testing.T) *application { return &application{ logger: slog.New(slog.NewTextHandler(io.Discard, nil)), } }
The reason for mocking this and writing to io.Discard
is to avoid clogging up our test output with unnecessary log messages when we run go test -v
(with verbose mode enabled).
The other two dependencies that it makes sense for us to mock are the models.SnippetModel
and models.UserModel
database models. By creating mocks of these it’s possible for us to test the behavior of our handlers without needing to setup an entire test instance of the MySQL database.
Mocking the database models
If you’re following along, create a new internal/models/mocks
package containing snippets.go
and user.go
files to hold the database model mocks, like so:
$ mkdir internal/models/mocks $ touch internal/models/mocks/snippets.go $ touch internal/models/mocks/users.go
Let’s begin by creating a mock of our models.SnippetModel
. To do this, we’re going to create a simple struct which implements the same methods as our production models.SnippetModel
, but have the methods return some fixed dummy data instead.
package mocks import ( "time" "snippetbox.alexedwards.net/internal/models" ) var mockSnippet = models.Snippet{ ID: 1, Title: "An old silent pond", Content: "An old silent pond...", Created: time.Now(), Expires: time.Now(), } type SnippetModel struct{} func (m *SnippetModel) Insert(title string, content string, expires int) (int, error) { return 2, nil } func (m *SnippetModel) Get(id int) (models.Snippet, error) { switch id { case 1: return mockSnippet, nil default: return models.Snippet{}, models.ErrNoRecord } } func (m *SnippetModel) Latest() ([]models.Snippet, error) { return []models.Snippet{mockSnippet}, nil }
And let’s do the same for our models.UserModel
, like so:
package mocks import ( "snippetbox.alexedwards.net/internal/models" ) type UserModel struct{} func (m *UserModel) Insert(name, email, password string) error { switch email { case "dupe@example.com": return models.ErrDuplicateEmail default: return nil } } func (m *UserModel) Authenticate(email, password string) (int, error) { if email == "alice@example.com" && password == "pa$$word" { return 1, nil } return 0, models.ErrInvalidCredentials } func (m *UserModel) Exists(id int) (bool, error) { switch id { case 1: return true, nil default: return false, nil } }
Initializing the mocks
For the next step in our build, let’s head back to the testutils_test.go
file and update the newTestApplication()
function so that it creates an application
struct with all the necessary dependencies for testing.
package main import ( "bytes" "io" "log/slog" "net/http" "net/http/cookiejar" "net/http/httptest" "testing" "time" // New import "snippetbox.alexedwards.net/internal/models/mocks" // New import "github.com/alexedwards/scs/v2" // New import "github.com/go-playground/form/v4" // New import ) func newTestApplication(t *testing.T) *application { // Create an instance of the template cache. templateCache, err := newTemplateCache() if err != nil { t.Fatal(err) } // And a form decoder. formDecoder := form.NewDecoder() // And a session manager instance. Note that we use the same settings as // production, except that we *don't* set a Store for the session manager. // If no store is set, the SCS package will default to using a transient // in-memory store, which is ideal for testing purposes. sessionManager := scs.New() sessionManager.Lifetime = 12 * time.Hour sessionManager.Cookie.Secure = true return &application{ logger: slog.New(slog.NewTextHandler(io.Discard, nil)), snippets: &mocks.SnippetModel{}, // Use the mock. users: &mocks.UserModel{}, // Use the mock. templateCache: templateCache, formDecoder: formDecoder, sessionManager: sessionManager, } } ...
If you go ahead and try to run the tests now, it will fail to compile with the following errors:
$ go test ./cmd/web # snippetbox.alexedwards.net/cmd/web [snippetbox.alexedwards.net/cmd/web.test] cmd/web/testutils_test.go:40:19: cannot use &mocks.SnippetModel{} (value of type *mocks.SnippetModel) as type *models.SnippetModel in struct literal cmd/web/testutils_test.go:41:19: cannot use &mocks.UserModel{} (value of type *mocks.UserModel) as type *models.UserModel in struct literal FAIL snippetbox.alexedwards.net/cmd/web [build failed] FAIL
This is happening because our application
struct is expecting pointers to models.SnippetModel
and models.UserModel
instances, but we are trying to use pointers to mocks.SnippetModel
and mocks.UserModel
instances instead.
The idiomatic fix for this is to change our application
struct so that it uses interfaces which are satisfied by both our mock and production database models.
To do this, let’s head back to our internal/models/snippets.go
file and create a new SnippetModelInterface
interface type that describes the methods that our actual SnippetModel
struct has.
package models import ( "database/sql" "errors" "time" ) type SnippetModelInterface interface { Insert(title string, content string, expires int) (int, error) Get(id int) (Snippet, error) Latest() ([]Snippet, error) } ...
And let’s also do the same thing for our UserModel
struct too:
package models import ( "database/sql" "errors" "strings" "time" "github.com/go-sql-driver/mysql" "golang.org/x/crypto/bcrypt" ) type UserModelInterface interface { Insert(name, email, password string) error Authenticate(email, password string) (int, error) Exists(id int) (bool, error) } ...
Now that we’ve defined those interface types, let’s update our application
struct to use them instead of the concrete SnippetModel
and UserModel
types. Like so:
package main import ( "crypto/tls" "database/sql" "flag" "html/template" "log/slog" "net/http" "os" "time" "snippetbox.alexedwards.net/internal/models" "github.com/alexedwards/scs/mysqlstore" "github.com/alexedwards/scs/v2" "github.com/go-playground/form/v4" _ "github.com/go-sql-driver/mysql" ) type application struct { logger *slog.Logger snippets models.SnippetModelInterface // Use our new interface type. users models.UserModelInterface // Use our new interface type. templateCache map[string]*template.Template formDecoder *form.Decoder sessionManager *scs.SessionManager } ...
And if you try running the tests again now, everything should work correctly.
$ go test ./cmd/web/ ok snippetbox.alexedwards.net/cmd/web 0.008s
Let’s take a moment to pause and reflect on what we’ve just done.
We’ve updated the application
struct so that instead of the snippets
and users
fields having the concrete types *models.SnippetModel
and *models.UserModel
they are interfaces instead.
So long as an type has the necessary methods to satisfy the interface, we can use them in our application
struct. Both our ‘real’ database models (like models.SnippetModel
) and mock database models (like mocks.SnippetModel
) satisfy the interfaces, so we can now use them interchangeably.
Testing the snippetView handler
With that all now set up, let’s get stuck into writing an end-to-end test for our snippetView
handler which uses these mocked dependencies.
As part of this test, the code in our snippetView
handler will call the mock.SnippetModel.Get()
method. Just to remind you, this mocked model method returns a models.ErrNoRecord
unless the snippet ID is 1
— when it will return the following mock snippet:
var mockSnippet = models.Snippet{ ID: 1, Title: "An old silent pond", Content: "An old silent pond...", Created: time.Now(), Expires: time.Now(), }
So specifically, we want to test that:
- For the request
GET /snippet/view/1
we receive a200 OK
response with the relevant mocked snippet contained in the HTML response body. - For all other requests to
GET /snippet/view/*
we should receive a404 Not Found
response.
For the first part here, we want to check that the request body contains some specific content, rather than being exactly equal to it. Let’s quickly add a new StringContains()
function to our assert
package to help with that:
package assert import ( "strings" // New import "testing" ) ... func StringContains(t *testing.T, actual, expectedSubstring string) { t.Helper() if !strings.Contains(actual, expectedSubstring) { t.Errorf("got: %q; expected to contain: %q", actual, expectedSubstring) } }
And then open up the cmd/web/handlers_test.go
file and create a new TestSnippetView
test like so:
package main ... func TestSnippetView(t *testing.T) { // Create a new instance of our application struct which uses the mocked // dependencies. app := newTestApplication(t) // Establish a new test server for running end-to-end tests. ts := newTestServer(t, app.routes()) defer ts.Close() // Set up some table-driven tests to check the responses sent by our // application for different URLs. tests := []struct { name string urlPath string wantCode int wantBody string }{ { name: "Valid ID", urlPath: "/snippet/view/1", wantCode: http.StatusOK, wantBody: "An old silent pond...", }, { name: "Non-existent ID", urlPath: "/snippet/view/2", wantCode: http.StatusNotFound, }, { name: "Negative ID", urlPath: "/snippet/view/-1", wantCode: http.StatusNotFound, }, { name: "Decimal ID", urlPath: "/snippet/view/1.23", wantCode: http.StatusNotFound, }, { name: "String ID", urlPath: "/snippet/view/foo", wantCode: http.StatusNotFound, }, { name: "Empty ID", urlPath: "/snippet/view/", wantCode: http.StatusNotFound, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { code, _, body := ts.get(t, tt.urlPath) assert.Equal(t, code, tt.wantCode) if tt.wantBody != "" { assert.StringContains(t, body, tt.wantBody) } }) } }
If you run the tests again with the -v
flag enabled, you should now see the new, passing, TestSnippetView
sub-tests in the output:
$ go test -v ./cmd/web/ === RUN TestPing --- PASS: TestPing (0.00s) === RUN TestSnippetView === RUN TestSnippetView/Valid_ID === RUN TestSnippetView/Non-existent_ID === RUN TestSnippetView/Negative_ID === RUN TestSnippetView/Decimal_ID === RUN TestSnippetView/String_ID === RUN TestSnippetView/Empty_ID --- PASS: TestSnippetView (0.01s) --- PASS: TestSnippetView/Valid_ID (0.00s) --- PASS: TestSnippetView/Non-existent_ID (0.00s) --- PASS: TestSnippetView/Negative_ID (0.00s) --- PASS: TestSnippetView/Decimal_ID (0.00s) --- PASS: TestSnippetView/String_ID (0.00s) --- PASS: TestSnippetView/Empty_ID (0.00s) === RUN TestCommonHeaders --- PASS: TestCommonHeaders (0.00s) === RUN TestHumanDate === RUN TestHumanDate/UTC === RUN TestHumanDate/Empty === RUN TestHumanDate/CET --- PASS: TestHumanDate (0.00s) --- PASS: TestHumanDate/UTC (0.00s) --- PASS: TestHumanDate/Empty (0.00s) --- PASS: TestHumanDate/CET (0.00s) PASS ok snippetbox.alexedwards.net/cmd/web 0.015s
As an aside, notice how the names of the sub-tests have been canonicalized? Go automatically replaces any spaces in the sub-test name with an underscore (and any non-printable characters will also be escaped) in the test output.