Hey, is it possible to mock ent without using sqlite driver?
Probably something like mock with callbacks that user can override.
We are starting to use ent in production and sqlite is not always suitable or enough.
(Also thank you again, project is really great!)
Hey @ernado and thanks for the kind words.
Hey, is it possible to mock ent without using sqlite driver?
Currently no. I can share that a few months ago I started to work on an in-memory driver to replace SQLite and it wasn't so easy. Mainly because some of the logic is configured in the database schema, like foreign-key actions (cascade-delete and set-null) and other nullability and JSON checks.
Also, I wasn't sure what is the right API to mock the client mutations and queries. For example, how to mock a traversal query like user.QueryE().Where(..).QueryE().All(), or some aggregation queries.
I can understand that using SQLite does not feel like a "pure unit testing", but it does the job and it does it very well.
Re-posting here my comment from issue #217:
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 := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
defer client.Close()
// 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.
but it does the job and it does it very well.
Agree, but there are some points:
Can pluggable storage drivers help with this?
I wasn't sure what is the right API to mock the client mutations and queries.
Same for me, I don't expect easy solution here, but I'm willing to help. Currently this issue is not critical for us, but eventually we will reach limits of this approach. We can't just mock ent (at least by default), so at least some kind of basic support from framework is still required.
I'm not asking for full solution (e.g. full featured testing framework), but a way to build it :)
I accidentally posted my previous comment before finishing it, so I would like to understand more the use-case, why SQLite doesn't work for you, and what API do you expect (or propose) for ent in testing.
(Ignore this, just saw your comment)
Regarding your comment, I get the motivation, and just want to mention that it works great for us with huge number of entities/edges and unit-tests (and it's pretty fast as well).
Portability (we can't use CGO_ENABLED=0)
Yeah, this is why I tried to tackle this a few months ago (I suffered from the same problem). The Go playground doesn't support CGO as well.
Can pluggable storage drivers help with this?
Yes
We can't just mock ent (at least by default), so at least some kind of basic support from framework is still required.
I'm not asking for full solution (e.g. full featured testing framework), but a way to build it :)
Let's start a discussion about it here. What API people expect from ent for testing, etc.
One of the things I like in the current approach (with SQLite), is that I'm fully able to test my business logic in UT, and it depends on the state of the data I generate at the beginning of the test. IMO, if we want to provide an alternative solution, it should provide the same experience.
Thanks for your feedback.
What API people expect from ent for testing, etc.
What about fully in-memory generated storage driver for ent?
We have a similar need and we use the following workaround:
import (
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/facebook/ent/dialect/sql"
)
...
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error [%v]", err)
}
defer db.Close()
driver := sql.OpenDB("mysql", db)
driverOption := ent.Driver(driver)
entClient := ent.NewClient(driverOption)
mock.
ExpectBegin()
...
basically we created a variable of type Driver and a ent.Client using that driver