This article has been translated on the basis of machine translation. If there are any mistakes, please fix it.pull request

Table Driven Tests with Jest

Learn how to test by Table Driven Tests with Jest. I explain two notations of array format and tagged template literal format. It also introduces type inference and assertion methods when written in TypeScript.

4/1/20225 min read
..
hero image

Introduction

Table Driven Tests is primarily the recommended testing method in Go lang. Define a complete test case as a table, including the input and expected results, and test iteratively against the test object. We can write the test suite only once and pass in the test data. If you have a lot of copy and paste when creating tests, it's likely that you can refactor your test cases into a table.

By the way, Go lang's Official states:

Writing good tests is not trivial, but in many situations a lot of ground can be covered with table-driven tests

jest also supports Table Driven Test, so I'd like to share how.

How to write a test case

You can express test cases in two ways with jest. Consider the following case where there is a conditional branch as a test target.

Function to convert *.html other than index.html to */index.html

1
2
3
4
5
6
7
8
9
10
11
import { dirname, join, parse } from 'path'
export const path2IndexHtml = (path: string): string => {
const EXT = '.html'
const INDEX = 'index'
const { ext, name, dir } = parse(path)
if(ext !== EXT) return path
if(name === INDEX) return path
return join(dir, name, `${INDEX}${EXT}`)
}
index.tsts

This function itself was used when the Server Side Generation implementation generating a file. Is it a rare case🙃

Test with an array of table

The first way to write it is to define the table as an array and pass it. Write as follows.

1
2
3
describe.each(table)(name, fn, timeout)
it.each(table)(name, fn, timeout)
test.each(table)(name, fn, timeout)
ts

Some objects have a same each method. The specific test case is as follows.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { path2IndexHtml } from '../src'
describe('path2IndexHtml', () => {
const table = [
['', ''],
['index.html', 'index.html'],
['/index.html', '/index.html'],
['index.css', 'index.css'],
['about.css', 'about.css'],
['about/index.css', 'about/index.css'],
['about.html', 'about/index.html'],
['hoge/about.html', 'hoge/about/index.html'],
['/hoge/about.html', '/hoge/about/index.html'],
['aindex.html', 'aindex/index.html'],
['indexa.html', 'indexa/index.html'],
['/about/index.html', '/about/index.html'],
]
it.each(table)('pattern1: path2IndexHtml(%s) = %s', (path, expected, fa) => {
expect(path2IndexHtml(path)).toBe(expected)
})
})
index.spec.tsts

Describe the test case in a two-dimensional array. The order of the elements in the array is passed as an argument to fn. Also, for name, specify the title of the test suite. You can generate a unique test title by injecting parameters that follow the format of printf. Please check here for details. The parameters are passed in the order of the elements in the array.

By the way, if you pass a one-dimensional array, it will be converted internally as [1, 2, 3] -> [[1], [2], [3]].

Also, the parameters passed to fn are type inferred for TypeScript. In the above example, table is of type string[][], so the argument to fn is inferred to be ... args: string []. If you want to infer as a tuple, add as const to table and it will be inferred well.

The each method also accepts generic types, so you can specify the type as follows:

1
it.each<string[]>(table)
index.spec.tsts

When run the test, got the following output:

1
2
3
4
5
6
7
8
9
10
11
12
13
path2IndexHtml
✓ path2IndexHtml() ->
✓ path2IndexHtml(index.html) -> index.html
✓ path2IndexHtml(/index.html) -> /index.html
✓ path2IndexHtml(index.css) -> index.css
✓ path2IndexHtml(about.css) -> about.css
✓ path2IndexHtml(about/index.css) -> about/index.css (1 ms)
✓ path2IndexHtml(about.html) -> about/index.html
✓ path2IndexHtml(hoge/about.html) -> hoge/about/index.html
✓ path2IndexHtml(/hoge/about.html) -> /hoge/about/index.html
✓ path2IndexHtml(aindex.html) -> aindex/index.html
✓ path2IndexHtml(indexa.html) -> indexa/index.html
✓ path2IndexHtml(/about/index.html) -> /about/index.html
bash

The parameters are embedded in the test title. I was able to test various parameters with a minimal test suite.

Test with tagged template literals

You can also represent a table with a tagged template literal.

The interface looks like this:

1
2
3
4
5
6
7
8
9
describe.each`
table
`(name, fn, timeout)
it.each`
table
`(name, fn, timeout)
test.each`
table
`(name, fn, timeout)
ts

Write the same test as the above example, it will look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
describe('path2IndexHtml', () => {
it.each`
path | expected
${''} | ${''}
${'index.html'} | ${'index.html'}
${'/index.html'} | ${'/index.html'}
${'about.css'} | ${'about.css'}
${'about/index.css'} | ${'about/index.css'}
${'about.html'} | ${'about/index.html'}
${'hoge/about.html'} | ${'hoge/about/index.html'}
`('path2IndexHtml($path) -> $expected', ({ path, expected }) => {
expect(path2IndexHtml(path)).toBe(expected)
})
})
index.spec.tsts

The first line of table specifies the variable name. Subsequent lines describe the test case with the $ {value} syntax. Even the string type must be enclosed in ${}.

Since it is passed in the form of an object to the argument of fn, it is better to receive it by destructuring assignment.

If you use a parameter for the test title of name, you can access the variable in the form $name.

The advantage of this notation is that you can write test cases in the form of a table. However, for test cases with a lot of string, ${} and quotes are not very easy to see.

Also, in this notation, the type inference of the argument of fn becomes any type. This is unavoidable because it is a tagged template literal and cannot accept generics.

If you really want to type, define the type in the fn function.

1
2
3
4
'path2IndexHtml($path) -> $expected',
({ path, expected }: { path: string; expected: string }) => {
expect(path2IndexHtml(path)).toBe(expected)
}
index.spec.tsts

The test results will be the same for both, so it's a good idea to use different notations depending on the test case and preference.


Edit this page on GitHub

Other Article

Comments