Anyway to wrap or tweak a test file before running go test?

95 views
Skip to first unread message

Matt Mueller

unread,
Nov 3, 2021, 4:33:10 PM11/3/21
to golang-nuts
I'm looking for a way to inject test dependencies into a test suite. Something like:

```
package user_test

func UserTest(t *testing.T, db *postgres.DB) {
  fmt.Println("running user test", New("Alice Wonderland"))
}
```

```
go_test ./user_test.go
```

Where go_test would either manipulate the file in memory and pass it to go test, or wrap it somehow in a separate go test file someplace else. I'm trying to make this as transparent to the developer as possible. Just like how go test works.

Any ideas? Thanks!

Matt

Axel Wagner

unread,
Nov 3, 2021, 4:41:13 PM11/3/21
to Matt Mueller, golang-nuts
I tend to do this using a helper, such as

func connectToDB(t *testing.T) *postgres.DB {
    t.Helper()
    // set up the connection, using t.Fatalf if an error occurs
    return conn
}

func UserTest(t *testing.T) {
    db := connectToDB(t)
}

This doesn't seem significantly worse in terms of convenience. But it has the advantage of being easily readable and only using the regular go tooling without any shenanigans.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/7ef4b9e0-3591-4505-9ce8-2bb9a7e76fe6n%40googlegroups.com.

ben...@gmail.com

unread,
Nov 3, 2021, 5:16:04 PM11/3/21
to golang-nuts
func connectToDB(t *testing.T) *postgres.DB {
    t.Helper()
    // set up the connection, using t.Fatalf if an error occurs
    return conn
}

func UserTest(t *testing.T) {
    db := connectToDB(t)
}

Yeah, that's a good way.

And if you want to avoid re-connecting to the db every test, you can use a top-level test function for the "suite", and sub-tests with t.Run() for the individual tests, with the sub-tests closing over the db variable. Like so:

func TestDatabaseThings(t *testing.T) {
    db := connectToDB(t)

    t.Run("Foo", func (t *testing.T) {
        fmt.Println(db, "test foo stuff") // use "db" and test stuff
    })

    t.Run("Bar", func (t *testing.T) {
        fmt.Println(db, "test bar stuff") // use "db" and test stuff
    })
}

-Ben

Matt Mueller

unread,
Nov 3, 2021, 5:43:49 PM11/3/21
to golang-nuts
Hey, thanks. I'm aware of this approach.

I'm hoping for some technique that automatically injects, since it can be cumbersome to inject all your dependencies by hand. 

Similar to google/wire, but without the generated file sitting in the filesystem next to your test.

Matt 

Axel Wagner

unread,
Nov 3, 2021, 5:51:49 PM11/3/21
to Matt Mueller, golang-nuts
On Wed, Nov 3, 2021 at 10:44 PM Matt Mueller <mattm...@gmail.com> wrote:
Hey, thanks. I'm aware of this approach.

I'm hoping for some technique that automatically injects, since it can be cumbersome to inject all your dependencies by hand.

I don't understand what you mean. Obviously, how to connect to a database or other dependencies is highly application specific. So you'd still have to write both the code to create the connections and somehow mention what dependencies you need in the test. I don't really see how you could get any significant savings here - it doesn't seem significantly less cumbersome to add a list of arguments to a function than to add a list of simple initialization statements to a test.

In any case, sorry for the noise then. I'm not aware of any such tool or package.

 

Similar to google/wire, but without the generated file sitting in the filesystem next to your test.

Matt 


On Wednesday, November 3, 2021 at 4:16:04 PM UTC-5 ben...@gmail.com wrote:
func connectToDB(t *testing.T) *postgres.DB {
    t.Helper()
    // set up the connection, using t.Fatalf if an error occurs
    return conn
}

func UserTest(t *testing.T) {
    db := connectToDB(t)
}

Yeah, that's a good way.

And if you want to avoid re-connecting to the db every test, you can use a top-level test function for the "suite", and sub-tests with t.Run() for the individual tests, with the sub-tests closing over the db variable. Like so:

func TestDatabaseThings(t *testing.T) {
    db := connectToDB(t)

    t.Run("Foo", func (t *testing.T) {
        fmt.Println(db, "test foo stuff") // use "db" and test stuff
    })

    t.Run("Bar", func (t *testing.T) {
        fmt.Println(db, "test bar stuff") // use "db" and test stuff
    })
}

-Ben

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

Brian Candler

unread,
Nov 4, 2021, 3:41:18 AM11/4/21
to golang-nuts
On Wednesday, 3 November 2021 at 21:51:49 UTC axel.wa...@googlemail.com wrote:
On Wed, Nov 3, 2021 at 10:44 PM Matt Mueller <mattm...@gmail.com> wrote:
Hey, thanks. I'm aware of this approach.

I'm hoping for some technique that automatically injects, since it can be cumbersome to inject all your dependencies by hand.

I don't understand what you mean. Obviously, how to connect to a database or other dependencies is highly application specific. So you'd still have to write both the code to create the connections and somehow mention what dependencies you need in the test.

Perhaps he's thinking of something like pytest.  Simply by adding a named argument to your test function, a corresponding helper is called to create the value.  There is a 'yield' variation so that the helper can also handle cleanup after the test; and helpers can invoke other helpers in the same way.

@pytest.yield_fixture(scope='function')
def session():
    db_session = make_session()
    yield db_session
    db_session.close()

@pytest.fixture(scope='function')
def customer(session):
    c = Customer(name="Fred")
    session.add(c)
    return c

# The actual tests
def test_foo(session, customer):
    assert session is not None
    assert customer is not None

def test_bar(session, customer):
    ... another test

It reduces the boilerplate somewhat - in go, each test would be something like

func test_foo() {
    session := make_session()
    defer session.close()
    customer := make_customer(session)
    ... rest of test
}

which is actually not unreasonable IMO.

pytest also lets you have objects with longer lifetimes (scopes), so that multiple tests within the same package or session can share the same object.

Axel Wagner

unread,
Nov 4, 2021, 4:26:25 AM11/4/21
to Brian Candler, golang-nuts
On Thu, Nov 4, 2021 at 8:41 AM Brian Candler <b.ca...@pobox.com> wrote:
Perhaps he's thinking of something like pytest.  Simply by adding a named argument to your test function, a corresponding helper is called to create the value.  There is a 'yield' variation so that the helper can also handle cleanup after the test; and helpers can invoke other helpers in the same way.

@pytest.yield_fixture(scope='function')
def session():
    db_session = make_session()
    yield db_session
    db_session.close()

@pytest.fixture(scope='function')
def customer(session):
    c = Customer(name="Fred")
    session.add(c)
    return c

# The actual tests
def test_foo(session, customer):
    assert session is not None
    assert customer is not None

def test_bar(session, customer):
    ... another test

Compare that to Go:

func makeSession(t *testing.T) *Session {
    s = make_session()
    t.Cleanup(s.Close)
    return s
}

func makeCustomer(t *testing.T, s *Session) Customer {
    c = Customer{Name: "Fred"}
    s.Add(c)
    return c
}

// The actual tests
func TestFoo(t *testing.T) {
    s := makeSession(t)
    c := makeCustomer(t, s)
    // test code
}

ISTM the primary difference here is, that instead of adding `s *Session` to `TestFoo`, you add a `s := makeSession(t)` statement. I just don't see that as saving any significant amount of boilerplate. And if the complaint is that you then have to actually pass the created session to `makeCustomer`, you can always have a `makePopulatedSession` helper, if you have to do that too often.

To be clear, I can see *some* benefit. Just not enough to offset the cost of using an obscure tool and thus making it much harder for other Go programmers to work on that project.

YMMV, of course, which is why I left it at "I don't know any such tool".


It reduces the boilerplate somewhat - in go, each test would be something like

func test_foo() {
    session := make_session()
    defer session.close()
    customer := make_customer(session)
    ... rest of test
}

which is actually not unreasonable IMO.

pytest also lets you have objects with longer lifetimes (scopes), so that multiple tests within the same package or session can share the same object.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

Ben Hoyt

unread,
Nov 4, 2021, 4:37:29 AM11/4/21
to Axel Wagner, Brian Candler, golang-nuts
Agreed. I've always found the "just add an argument with the right name" fixture feature too magical and implicit (read: confusing). I much prefer Go's explicitness here, even if slightly more verbose.

IMO even in Python that kind of magic is usually frowned upon.

-Ben

You received this message because you are subscribed to a topic in the Google Groups "golang-nuts" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/golang-nuts/gg7cstoAzbk/unsubscribe.
To unsubscribe from this group and all its topics, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CAEkBMfFQ0okmkZwqB%3D_GaCQH8Q9ECCiE7coM3Fb-8cL3pz32jg%40mail.gmail.com.
Reply all
Reply to author
Forward
0 new messages