Writing tests

Test suites

Inside a test file, tests are grouped together inside Test suites. They are created with the describe method:

describe('myFunc()', () => {
  test('typical usage', () => {
    // test code with assertions...
  })

  test('error case', () => {
    // test code with assertions...
  })
})

You can however put tests outside of a describe function: they will be added to an anonymous test suite automatically:

test('some test without describe', () => {
  // ...
})

Suite hooks

You can execute some code at some specific point in your test suite with the following hooks:

  • beforeAll: called before the first test is run in the suite.
  • afterAll: called after all tests are completed in the suite.
  • beforeEach: called before each test in the suite.
  • afterEach: called after each test in the suite.

Example:

import { createReactiveFileSystem } from 'reactive-fs'

let fs

describe('reactive fs: list()', () => {
  beforeEach(async () => {
    fs = await createReactiveFileSystem({ /* ... */ })
  })

  afterEach(() => {
    fs.destroy()
  })

  test('scan for files', () => {
    expect(fs.list().sort()).toEqual(['sub/waf.js', 'meow.js'].sort())
  })

  test('exclude sub directories', () => {
    expect(fs.list('.', { excludeSubDirectories: true }).sort()).toEqual(['meow.js'].sort())
  })

  // more tests...
})

Assertions

Peeky uses expect from Jest to check for assertions in your tests.

Look at some examples below:

test('check assertions with expect', () => {
  expect(42).toBe(42)
  expect(21).not.toBe(42)
  expect([1, 2, 3]).toEqual([1, 2, 3])
  expect(undefined).toBeUndefined()
  expect(null).toBeNull()
  expect(1).toBeTruthy()
})

For the complete list of available assertions, see expect docs.

Flags

Using test flags, you can control how your tests are run.

Only

Tests with this flag will be the only ones run in the current test suite.

test.only('should work', () => {
  expect(1).toBe(1)
})

Skip

The tests with this flag will not be run.

test.skip('should work', () => {
  expect(1).toBe(1)
})

Todo

The tests with this flag will not be run. The handler function is optional - useful to add tests that you want to write later.

test.todo('test my function')

Mocking

If you need mocks/stubs/spies in your tests, Peeky includes Sinon out-of-the-box.

test('use a sinon mock', () => {
  const spy = sinon.fake()
  spy()
  expect(spy.callCount).toBe(1)
})

Learn more at the sinon docs.

Module mocking

WARNING

This API is experimental and may change in the future.

Sometimes it's useful to replace the existing implementation of a module used in the file you are testing. You can use peeky.mockModule to stub a module - it will replace the real module with the fake implementation you provide.

Example:

// bar.ts

import { foo } from './foo'

export function bar (meow) {
  return foo(meow)
}
// bar.spec.ts

peeky.mockModule('./foo.ts', {
  foo (count) {
    return count + 1
  },
})

// Should be imported after relevant modules are mocked
// Example comment to disable the ESLint rule:
/* eslint-disable-next-line import/first */
import { bar } from './bar'

describe('mock module', () => {
  test('mock a module during the test', () => {
    expect(bar(42)).toBe(43)
  })
})

You can also use dynamic import:

// bar.spec.ts

peeky.mockModule('./foo.ts', {
  foo (count) {
    return count + 1
  },
})

describe('mock module', () => {
  test('mock a module during the test', async () => {
    const { bar } = await import('./bar')
    expect(bar(42)).toBe(43)
  })
})

TIP

Please include the file extension when mocking a module so it can be resolved correctly.

Text Snapshots

The toMatchSnapshot assertion allows you to store text that will be compared in future runs of the test. It is very useful to detect regressions.

Example:

/* @peeky {
  runtimeEnv: 'dom'
} */

// Include template compiler
import { createApp } from 'vue/dist/vue.esm-bundler'

describe('vue', () => {
  test('create vue app', () => {
    const app = createApp({
      data () {
        return {
          msg: 'hello',
        }
      },
      template: '<div>{{ msg }}</div>',
    })
    expect(document.body.innerHTML).toMatchSnapshot()
  })
})

The first time, it will create a __snapshots__ folder with a vue.spec.js.snap file that stores the expected value. Next time the test is run, Peeky will load and compare the stored snapshot with the new value.

TIP

You should commit the content of the __snapshots__ folder so snapshots are correctly stored for future runs.

By default Peeky will use the suite and test titles to generate the snapshot name. You can specify a hint to help distinguish the snapshot names in case you have multiple ones in a single test:

test('many snapshots', () => {
  expect('Meow!').toMatchSnapshot('cat')
  expect('Waf!').toMatchSnapshot('dog')
})

Retry

WARNING

This API is experimental and may change in the future.

With peeky.retry(), you can enclose code in a fail-safe so it will retry automatically if an error is thrown inside.

Example:

describe('peeky.retry()', () => {
  test('must not retry when no error', async () => {
    const spy = sinon.fake()
    expect(spy.callCount).toBe(0)
    await peeky.retry(() => spy(), 10)
    expect(spy.callCount).toBe(1)
  })

  test('retry multiple times', async () => {
    let times = 0
    const spy = sinon.fake(() => {
      times++
      if (times < 4) {
        throw new Error('Not enough times')
      }
    })
    expect(spy.callCount).toBe(0)
    await peeky.retry(() => spy(), 5)
    expect(spy.callCount).toBe(4)
  })

  test('retry multiple times and bail out', async () => {
    const spy = sinon.fake(() => {
      throw new Error('Not enough times')
    })
    expect(spy.callCount).toBe(0)
    let error: Error
    try {
      await peeky.retry(() => spy(), 5)
    } catch (e) {
      error = e
    }
    expect(spy.callCount).toBe(5)
    expect(error).not.toBeUndefined()
  })
})