Skip to content

proposal: testing: per-subtest setup and cleanup #59291

Open
@unkeep

Description

@unkeep

When writing tests in BDD style using subtests it's often necessary to invoke some common code before or after a subtest. In the example below we call beforeRun in the first place of subtests 1 and 2, otherwise the subtests would affect the outcome of each other:

type CoffeeMachine struct {
	on bool
}

func (a *CoffeeMachine) TurnOn() error {
	if a.on {
		return fmt.Errorf("aready on")
	}
	a.on = true
	return nil
}

func (a *CoffeeMachine) MakeCoffee() error {
	if !a.on {
		return fmt.Errorf("power is off")
	}

	return nil
}

func TestCoffeeMachine(t *testing.T) {
	// 0
	t.Run("given a new coffee machine", func(t *testing.T) {
		var a CoffeeMachine

		beforeRun := func(t *testing.T) {
			a = CoffeeMachine{}
		}

		// 1
		t.Run("it could be turned on", func(t *testing.T) {
			beforeRun(t)

			err := a.TurnOn()
			assert.NoError(t, err)

			// 1.1
			t.Run("it could not be turned on again", func(t *testing.T) {
				err := a.TurnOn()
				assert.Error(t, err)
			})

			// 1.2
			t.Run("it makes a coffee", func(t *testing.T) {
				err := a.MakeCoffee()
				assert.NoError(t, err)
			})
		})

		// 2
		t.Run("MakeCoffee fails because it's not turned on", func(t *testing.T) {
			beforeRun(t)

			err := a.MakeCoffee()
			assert.Error(t, err)
		})
	})
}

It would be nice instead, if the testing package could invoke a specific function by itself before/after direct subtests of the parent where it's configured:

func TestCoffeeMachine(t *testing.T) {
	// 0
	t.Run("given a new coffee machine", func(t *testing.T) {
		var a CoffeeMachine

		// a new proposed method for testing.T
		t.BeforeRun(func(t *testing.T) {
			a = CoffeeMachine{}
		})

		// 1
		t.Run("it could be turned on", func(t *testing.T) {
			// the functor passed to BeforeRun is called before we reach this point

			err := a.TurnOn()
			assert.NoError(t, err)

			// 1.1
			t.Run("it could not be turned on again", func(t *testing.T) {
				err := a.TurnOn()
				assert.Error(t, err)
			})

			// 1.2
			t.Run("it makes a coffee", func(t *testing.T) {
				err := a.MakeCoffee()
				assert.NoError(t, err)
			})
		})

		// 2
		t.Run("MakeCoffee fails because it's not turned on", func(t *testing.T) {
			// the functor passed to BeforeRun is called before we reach this point

			err := a.MakeCoffee()
			assert.Error(t, err)
		})
	})
}

The points against of the explicit beforeRun(t) calls from subtests are the following:

  1. it can simply be skipped by mistake
  2. it can be called from a wrong level by mistake

So I propose to add func (t *T) BeforeRun(func(t *testing.T) and func (t *T) AfterRun(func(t *testing.T) methods which should be invoked by the testing package before/after direct subtests of the parent where it's configured.

Relates to #39222 #40984

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status

    Incoming

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions