I would like to know what are the recommended approaches to unit testing the generated client codes. There is a generated example test but for integration. I am pretty new to go and ent, but I thought of the following strategies:
Move interactions on ent.Client to a separate layer e.g. using patterns like repository/DAO, then mock out that layer. However, I think that this is redundant.
Move individual interactions on ent.Client to separate functions that can be overridden during tests. This is simple and doable for simple usages.
Mock the generated client code by entc.
Use a mock sql driver like (https://github.com/DATA-DOG/go-sqlmock), or sqlite. I think this is also redundant. I would like to use objects outside of ent, and perform integration tests on an actual database at ent layer.
We use the same approach as the httptest package. Which means your code continue to work with ent.Client, but the underlying storage (sql.DB) is mocked.
You can use either go-sqlmock or sqlite. I'll recommend you to use sqlite, because with go-sqlmock you define exceptions on the executed queries and these are "low level" details that I don't expect our end users will have to deal with (in most cases).
Also, in some cases you have complicated queries that are hard to mock (with go-sqlmock), and it's much easier to generate a set of entities in the database and expect that the public API (you are testing) returns the right result.
I'll try to give an example how does a test in our services look like and I hope it will help.
Suppose you have a REST or GraphQL handler that uses ent.Client as a storage dependency, and you want to write some exceptions for the different scenarios. The code looks as follows:
func TestCreateUser(t *testing.T) {
tests := []struct {
name string // name of the test
before func(*testing.T, *ent.Client) // work before test
expect func(*testing.T, *Server) // actual exceptions
}{
{
name: "create/already_exist",
before: func(t *testing.T, client *ent.Client) {
_, err := client.User.Create()...Save(ctx)
require.NoError(t, err)
},
expect: func(t *testing.T, server *Server) {
// Try to create a new User and expect it
// fails due to "UserAlreadyExistsError"
},
},
// more tests ...
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create an SQLite memory database and generate the schema.
client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
require.NoError(t, err)
defer client.Close()
require.NoError(t, client.Schema.Create(context.Background()))
// Run work before the test.
tt.before(t, client)
// Run the actual exceptions.
tt.expect(t, &Server{
Client: client, // inject ent.Client as a dependency.
})
})
}
}
The table driven test is not really necessary here.
All clear! Thanks for the great answer. I hope this will help other people as well 馃帀
Most helpful comment
We use the same approach as the
httptestpackage. Which means your code continue to work withent.Client, but the underlying storage (sql.DB) is mocked.You can use either
go-sqlmockorsqlite. I'll recommend you to usesqlite, because withgo-sqlmockyou define exceptions on the executed queries and these are "low level" details that I don't expect our end users will have to deal with (in most cases).Also, in some cases you have complicated queries that are hard to mock (with
go-sqlmock), and it's much easier to generate a set of entities in the database and expect that the public API (you are testing) returns the right result.I'll try to give an example how does a test in our services look like and I hope it will help.
Suppose you have a REST or GraphQL handler that uses
ent.Clientas a storage dependency, and you want to write some exceptions for the different scenarios. The code looks as follows:The table driven test is not really necessary here.