Ent: Approach to unit tests

Created on 5 Dec 2019  路  2Comments  路  Source: ent/ent

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:

  1. 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.

  2. Move individual interactions on ent.Client to separate functions that can be overridden during tests. This is simple and doable for simple usages.

  3. Mock the generated client code by entc.

  4. 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.

Question

Most helpful comment

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 2 comments

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 馃帀

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rotemtam picture rotemtam  路  4Comments

ernado picture ernado  路  4Comments

a8m picture a8m  路  5Comments

jbub picture jbub  路  5Comments

rubensayshi picture rubensayshi  路  3Comments