franela / goblin

Minimal and Beautiful Go testing framework
MIT License
884 stars 79 forks source link

Dependent Tests, skip some subset of tests if the parent test fails #96

Open TJM opened 3 years ago

TJM commented 3 years ago

Perhaps I am just not searching for the right terms, so I am going to try to explain what I am trying to accomplish...

I am currently adding tests to github.com/TJM/go-trello and I have hit a conditions multiple times where tests need to depend on the previous test. For example:

If "Create Board" fails, the other two need to "skip" or "fail fast" or something, but currently it just blows up and I get the go equivalent of a null pointer exception.

I was thinking of adding an assertion at the top, which should cause the test to fail at least (better than the crash I was getting before), but the failure state would look better if it showed them as dependent tests that were skipped :-/

// NOTE: "client" is setup in "init()" currently, but could probably be moved to a g.Before()?

        g.It("should create a board", func() {
            board, err = client.CreateBoard(testBoardName)
            Expect(err).To(BeNil())
            Expect(board).NotTo(BeNil())
            Expect(board.Name).To(Equal(testBoardName))
        })

        g.It("should get a board by ID", func() {
            Expect(board).NotTo(BeNil()) // Create Board needs to have succeeded
            board, err = client.Board(board.ID)
            Expect(err).To(BeNil())
            Expect(board).NotTo(BeNil())
            Expect(board.Name).To(Equal(testBoardName))
        })

        g.It("should change the board background to red", func() {
            Expect(board).NotTo(BeNil()) // Create Board needs to have succeeded
            err = board.SetBackground("red")
            Expect(err).To(BeNil())
            Expect(board.Prefs.Background).To(Equal("red"))
        })

        g.It("should change the description to something", func() {
            Expect(board).NotTo(BeNil()) // Create Board needs to have succeeded
            err = board.SetDescription("something")
            Expect(err).To(BeNil())
            Expect(board.Desc).To(Equal("something"))
        })
marcosnils commented 3 years ago

Dependent tests is a practice that's not usually encouraged since the idea for tests is that they could potentially run in any order.

What you can do instead is to use the before and beforeEach blocks to perform certain operations that will get executed before every test, like creating a new board for example.

marcosnils commented 3 years ago

      g.It("should create a board", func() {
          board, err = client.CreateBoard(testBoardName)
          Expect(err).To(BeNil())
          Expect(board).NotTo(BeNil())
          Expect(board.Name).To(Equal(testBoardName))
      })

      g.It("should get a board by ID", func() {
          Expect(board).NotTo(BeNil()) // Create Board needs to have succeeded
          board, err = client.Board(board.ID)
          Expect(err).To(BeNil())
          Expect(board).NotTo(BeNil())
          Expect(board.Name).To(Equal(testBoardName))
      })

      g.It("should change the board background to red", func() {
          Expect(board).NotTo(BeNil()) // Create Board needs to have succeeded
          err = board.SetBackground("red")
          Expect(err).To(BeNil())
          Expect(board.Prefs.Background).To(Equal("red"))
      })

      g.It("should change the description to something", func() {
          Expect(board).NotTo(BeNil()) // Create Board needs to have succeeded
          err = board.SetDescription("something")
          Expect(err).To(BeNil())
          Expect(board.Desc).To(Equal("something"))
      })

^ With this you can do something like

g.Describe("Board")
   g.BeforeEach(func() {
         // Create new board here before each test
   })
   g.AfterEach(func() {
         // Cleanup created board.
   })
   g.It("should be created with params xxx") // Test that creates a new board
   g.It("should be fetched by id") // Fetch the board created in the beforeEach function
.....

Makes sense?

If creating and destroying the board before each it block is too much you can use before and after to do it once in the same Describe block and re-use it across all the it cases.

TJM commented 3 years ago

Hmm, thanks for the suggestion...

Re: BeforeEach/AfterEach - I am already having issues (429) with hitting my limits on API calls while running tests, so I think I will try to do my best to work with one set of prerequisites for the entire "type" instead of creating/deleting them foreach.

Re: Before/After - I am already attempting to do that to create the prerequisites for the deeper tests... for example: https://github.com/TJM/go-trello/blob/master/card_test.go#L39-L54

... but I don't think it really likes the assertions I put into the before ;)

The "structure" is something like:

trello.Client:
  member:
  board:
    - list:
      - card:
        - checklist:
            - item

... so, for example, in order to test "card" (SetName, SetDescription, AddMember, etc), you need to have a working client, board, and card. I have each of those in the "before" ... maybe it just comes down to having the tests in the wrong place, but it seems crazy to not test checklist items while you have just created a checklist, rather than to tear it all down and build it in another file.

TJM commented 3 years ago

It would be nice to have the ability to indicate dependent tests in order to "skip" them when their prerequisite test fails. However, I re-arranged my tests so that the g.Before() lays out all the pre-requisites for that test. I guess that comes down to a "code organization" thing.. I am "new" to go... obviously. :)

The previous developer (that I forked from) chose to put "CreateBoard" into the "board.go" instead of the "client.go" even though it starts with func (c *Client) ... perhaps that should be in "client.go" anyhow? (shrug)... I moved the test to client_test.go, and added the board prereq to g.Before... but along those same lines func (c *Client) Board(boardID string) (board *Board, err error) is also arguably a "client" operation, but I need a "boardID" to be able to run it, so I left that inside the board_test.go, where I should presumably have a working "board" object (board.ID).

Thanks, Tommy

TJM commented 3 years ago

OK, new question... How do you "fail" the g.Before()?

I have the following error:

--- FAIL: TestMembership (1.31s)
panic: Asserts should be written inside an It() block. [recovered]
    panic: Asserts should be written inside an It() block.

goroutine 35 [running]:
testing.tRunner.func1.1(0x132f280, 0x140b280)
    /usr/local/Cellar/go/1.15.2/libexec/src/testing/testing.go:1076 +0x30d
testing.tRunner.func1(0xc000184600)
    /usr/local/Cellar/go/1.15.2/libexec/src/testing/testing.go:1079 +0x41a
panic(0x132f280, 0x140b280)
    /usr/local/Cellar/go/1.15.2/libexec/src/runtime/panic.go:969 +0x175
github.com/franela/goblin.(*G).errorCommon(0xc00019c4e0, 0xc00022e500, 0x13e, 0x1)
    /Users/tmcneely/go/pkg/mod/github.com/franela/goblin@v0.0.0-20201006155558-6240afcb2eb7/goblin.go:356 +0x176
github.com/franela/goblin.(*G).Fail(0xc00019c4e0, 0x132f280, 0xc00029a3b0)
    /Users/tmcneely/go/pkg/mod/github.com/franela/goblin@v0.0.0-20201006155558-6240afcb2eb7/goblin.go:375 +0x99
github.com/TJM/go-trello.TestMembership.func1(0xc00022e3c0, 0x13e, 0xc0004dcfb8, 0x1, 0x1)
    /Users/tmcneely/go/src/github.com/TJM/go-trello/membership_test.go:30 +0x58

... so it did "sorta" work, in that all the other tests didn't try to run, but as I said before, it doesn't like assertions in the g.Before... what is the proper way to "fail" or can we make it so that g.Before can have assertions?