Home Manual Reference Source
Manual » Tutorial

Test Structure

Our test files run in parallel, but the files themselves should run from top to bottom. Knowing this we can organize our tests with describes, tests, beforeAll, afterAll, beforeEach, and afterEach to hopefully give us good readability and reporting on what we care about from the tests.

As an example, lets look at the following test file, which is in a pretty bad state (Note: code may be pseudo-code):

test("Can adopt a cat", () => {
  dbSetupController.populateDbWithCats(5)
  client.loginController.login()
  client.adoptionController.viewCatList()
  const cats = client.catListController.getCats()
  expect(cats.length).toBe(5)
  client.catListController.selectCatNamed("Garfield")
  const currentCatName = client.catDetailsController.getCurrentCatName()
  expect(currentCatName).toBe("Garfield")
  client.catDetailsController.clickAdopt()
  client.checkoutController.addLitterBox()
  client.checkoutsController.addCatnip()
  client.checkoutController.addCatFood()
  const totalPrice = client.checkoutController.getTotalPrice()
  expect(totalPrice).toBe("$39.15")
  client.checkoutController.finishAdoption()
  const checkoutComplete = client.checkoutController.isCheckoutComplete()
  expect(checkoutComplete).toBe.true
  dbSetupController.resetDb()
})

This test works, as in, it all passes. But, it's doing more than what it's title may indicate. Sure, it's checking out if you can adopt a cat but it's checking a few more things along the way. You have to find out what your test is asking to determine what you need to expect/assert inside the test. So, maybe we don't care about all of the assertions except for the final isCheckoutComplete() one? Or are all of our other assertions still completely valid?

For this scenario, lets pretend that we want to know about all of our other assertions. But first, lets start tearing apart the test. Can you see which parts are setup/teardown and not really a part of the test? Let's refactor this a bit to leverage before/after blocks...

beforeAll(() => {
  dbSetupController.populateDbWithCats(5)
  client.loginController.login()
})

afterAll(() => {
  dbSetupController.resetDb()
})

test("Can adopt a cat", () => {
  client.adoptionController.viewCatList()
  const cats = client.catListController.getCats()
  expect(cats.length).toBe(5)
  client.catListController.selectCatNamed("Garfield")
  const currentCatName = client.catDetailsController.getCurrentCatName()
  expect(currentCatName).toBe("Garfield")
  client.catDetailsController.clickAdopt()
  client.checkoutController.addLitterBox()
  client.checkoutsController.addCatnip()
  client.checkoutController.addCatFood()
  const totalPrice = client.checkoutController.getTotalPrice()
  expect(totalPrice).toBe("$39.15")
  client.checkoutController.finishAdoption()
  const checkoutComplete = client.checkoutController.isCheckoutComplete()
  expect(checkoutComplete).toBe.true 
})

This structure seperates our tests from our test setup/teardown. The next bit is that we are going to split up our test in multiple tests, but wrap it all with a describe block.

describe('Cat adoption process', () => {
  beforeAll(() => {
    dbSetupController.populateDbWithCats(5)
    client.loginController.login()
  })

  afterAll(() => {
    dbSetupController.resetDb()
  })

  test("displays 5 cats in list", () => {
    client.adoptionController.viewCatList()
    const cats = client.catListController.getCats()
    expect(cats.length).toBe(5)
  })

  test("name matches selected cat", () => {
    client.catListController.selectCatNamed("Garfield")
    const currentCatName = client.catDetailsController.getCurrentCatName()
    expect(currentCatName).toBe("Garfield")
  })

  test("price total updates properly", () => {
    client.catDetailsController.clickAdopt()
    client.checkoutController.addLitterBox()
    client.checkoutsController.addCatnip()
    client.checkoutController.addCatFood()
    const totalPrice = client.checkoutController.getTotalPrice()
    expect(totalPrice).toBe("$39.15")
  })

  test("can checkout completely", () => {
    client.checkoutController.finishAdoption()
    const checkoutComplete = client.checkoutController.isCheckoutComplete()
    expect(checkoutComplete).toBe.true 
  })
})

This is, generally, what our finished tests shoud look like. When run with jest we get a nice output for each test we have. In the code, we have clean seperation of concerns. This setup also gives up flexibility in the future as we can nest additional describe blocks to further organize our suites.