ginkgo icon indicating copy to clipboard operation
ginkgo copied to clipboard

Request: Higher level `It` statements should be run for each child Context.

Open Bjohnson131 opened this issue 3 years ago • 2 comments

So in order to add a lot of different contexts in which one result should always be consistent, I currently have a lot of statements which say the same thing, often comparing two data structures which are used a lot. To simplify this, I propose that higher level It statements should be inherited by sub contexts within the same context. This will reduce the redundancy of written tests dramatically, as I won't have to copy and paste the same It block 8 times for one unit test file.

As an example, the following Go file:

package pkg

import (
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"testing"
)

func TestPlayground(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Test Playground")
}

var _ = Describe("Test Playground", func() {
	Context("Functionality1", func() {
		var (
			someInt int
		)
		Context("Environment one", function1(someInt))
		Context("Environment two", function2(someInt))
		Context("Environment three", function3(someInt))
		It("should do something for each context", func() {
			Expect(someInt).To(Equal(4321))
		})
	})
})

func function1(number int) func() {
	return func() {
		BeforeEach(func() {
			number = 4321
		})
	}
}

func function2(number int) func() {
	return func() {
		BeforeEach(func() {
			number = 4321
		})
	}
}

func function3(number int) func() {
	return func() {
		BeforeEach(func() {
			number = 4322
		})
	}
}

Should be functionally equal to:

package pkg

import (
	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	"testing"
)

func TestPlayground(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Test Playground")
}

var _ = Describe("Test Playground", func() {
	Context("Functionality1", func() {
		var (
			someInt int
		)
		Context("Environment one", function1(someInt))
		Context("Environment two", function2(someInt))
		Context("Environment three", function3(someInt))
	})
})

func function1(number int) func() {
	return func() {
		BeforeEach(func() {
			number = 4321
		})
		It("should do something for each context", func() {
			Expect(number).To(Equal(4321))
		})
	}
}

func function2(number int) func() {
	return func() {
		BeforeEach(func() {
			number = 4321
		})
		It("should do something for each context", func() {
			Expect(number).To(Equal(4321))
		})
	}
}

func function3(number int) func() {
	return func() {
		BeforeEach(func() {
			number = 4322
		})
		It("should do something for each context", func() {
			Expect(number).To(Equal(4321))
		})
	}
}

Bjohnson131 avatar Dec 09 '20 20:12 Bjohnson131

Could you share a bit more about your use case or point me at some actual code? The example you have in the issue is a bit confusing (specifically: the first two environments set number to 4321 while the third sets it to 4322, which will fail, - so I'm struggling to understand the underlying problem you are trying to solve).

Two quick suggestions based on what you've shared so far:

  1. On the broad theme of shared testing patterns I'd suggest perusing the recommendations in the docs here.

  2. For parametrized testing (which seems closest to what you are trying to do in your example) check out the Table-Driven Tests extension

onsi avatar Dec 09 '20 22:12 onsi

I'm actually wondering if this could be re-opened. It seems like a good way to write invariant conditions into hierarchical tests. Though it probably shouldn't be lumped in with the regular It function for compatibility reasons among others. For example:

Context("When BuildFoo is called on Bar", func() {
    JustBeforeEach(func() {
        out, err = BuildFoo(myBar);
    })
    Whenever("An error occurs", func() bool {
        return err != nil
    }).ItAlways("Returns an empty output", func() {
        Expect(out).ToBe(Empty())
    })
    When("Bar is malformed", func() {
        BeforeEach(func() {
            myBar.Malformed = true
        })
        ItAlways("returns an error", func() {
            Expect(err).To(HaveOccurred())
        })
        It(PreservesInvariants()) // leads to all registered Whenever...ItAlways blocks running within this node
        When("the reason is \"too large\", func() {
            BeforeEach(func() {
                myBar.Reason = "too large"
            })
            It("contains too big in the error", func() {
                Expect(err).ToMatch("too big")
            })
        })
        When("the reason is \"too small\", func() {
            BeforeEach(func() {
                myBar.Reason = "too small"
            })
            It("contains too little in the error", func() {
                Expect(err).ToMatch("too little")
            })
        })
    })
    When("Bar is good", func() {
        BeforeEach(func() {
            myBar.Good = true
        })
        ItAlways("returns no error", func() {
            Expect(err).NotTo(HaveOccurred())
        })
        When("The color is red", func() {
            BeforeEach(func() {
                bar.Color = "red"
            })
            It("Returns fire", func() {
                Expect(out.Element).To(Equal("fire"));
            })
        })
        When("The color is yellow", func() {
            BeforeEach(func() {
                bar.Color = "yellow"
            })
            It("Returns gold", func() {
                Expect(out.Element).To(Equal("gold"));
            })
        })
    })
}

This is equivalent to

Context("When BuildFoo is called on Bar", func() {
    JustBeforeEach(func() {
        out, err = BuildFoo(myBar);
    })
    When("Bar is malformed", func() {
        BeforeEach(func() {
            myBar.Malformed = true
        })
        It("Returns an empty output", func() {
            Expect(out).ToBe(Empty())
        })
        It("returns an error", func() {
            Expect(err).To(HaveOccurred())
        })
        When("the reason is \"too large\", func() {
            BeforeEach(func() {
                myBar.Reason = "too large"
            })
            It("Returns an empty output", func() {
                Expect(out).ToBe(Empty())
            })
            It("returns an error", func() {
                Expect(err).To(HaveOccurred())
            })
            It("contains too big in the error", func() {
                Expect(err).ToMatch("too big")
            })
        })
        When("the reason is \"too small\", func() {
            BeforeEach(func() {
                myBar.Reason = "too small"
            })
            It("Returns an empty output", func() {
                Expect(out).ToBe(Empty())
            })
            It("returns an error", func() {
                Expect(err).To(HaveOccurred())
            })
            It("contains too little in the error", func() {
                Expect(err).ToMatch("too little")
            })
        })
    })
    When("Bar is good", func() {
        BeforeEach(func() {
            myBar.Good = true
        })
        When("The color is red", func() {
            BeforeEach(func() {
                bar.Color = "red"
            })
            It("returns no error", func() {
                Expect(err).NotTo(HaveOccurred())
            })
            It("Returns fire", func() {
                Expect(out.Element).To(Equal("fire"));
            })
        })
        When("The color is yellow", func() {
            BeforeEach(func() {
                bar.Color = "yellow"
            })
            It("returns no error", func() {
                Expect(err).NotTo(HaveOccurred())
            })
            It("Returns gold", func() {
                Expect(out.Element).To(Equal("gold"));
            })
        })
    })
}

Having invariants "out of focus" like this is good because it helps isolate the checks that are unique to each subtree. DescribeTable also works, but it tends to intertwine test setup, execution, and assertions.

FastNav avatar May 24 '21 22:05 FastNav