Skip to content

test.each: %s is not substituted for non-string values in the test title #37821

Description

@kjanat

What version of Bun is running?

1.3.14+0d9b296a

What platform is your computer?

Linux 7.1.6-1-MANJARO x86_64 GNU/Linux

What steps can reproduce the bug?

In a test.each / describe.each title, %s is substituted only when the value at that
position is already a string. A number leaves the literal %s in the name.

import { expect, test } from 'bun:test';

test.each(['alpha'])('string via %s: %s', (v) => expect(v).toBeDefined());
test.each([1])('integer via %s: %s', (v) => expect(v).toBeDefined());
test.each([0.8])('float via %s: %s', (v) => expect(v).toBeDefined());

// Per argument, not per row: the string substitutes and the number beside it does not.
test.each([['x', 1]])('mixed tuple via %s: %s then %s', (a, b) => expect([a, b]).toBeDefined());

// The other specifiers, for comparison.
test.each([[1, 2]])('integers via %i: %i and %i', (a, b) => expect([a, b]).toBeDefined());
test.each([0.8])('float via %i: %i', (v) => expect(v).toBeDefined());
test.each([0.8])('float via %d: %d', (v) => expect(v).toBeDefined());
test.each([0.8])('float via %f: %f', (v) => expect(v).toBeDefined());
test.each([0.8])('float via %p: %p', (v) => expect(v).toBeDefined());

The default reporter prints no per-test names when stdout is not a terminal, so the resolved
titles are easiest to read out of the junit reporter:

bun test --reporter-outfile /dev/stdout --reporter junit repro.test.ts | grep 'testcase name'

What is the expected behavior?

Jest documents %s as "String" and formats titles through util.format, where %s
stringifies any value (util.format('%s', 1) is '1'). All nine titles above should carry
their value.

What do you see instead?

table value specifier resulting title
'alpha' %s string via %s: alpha
1 %s integer via %s: %s
0.8 %s float via %s: %s
['x', 1] %s twice mixed tuple via %s: x then %s
[1, 2] %i integers via %i: 1 and 2
0.8 %i float via %i: %i
0.8 %d float via %d: 0.8
0.8 %f float via %f: 0.8
0.8 %p float via %p: 0.8

Two things follow. The substitution is decided per argument rather than per row: in the mixed
tuple the string is replaced and the number in the same title is not. And each specifier is
gated on its own type, %s on string and %i on integer, where %d, %f and %p accept
both.

Additional information

A table of numeric cases produces N tests sharing one title, which makes them
indistinguishable in a failure list and gives reporters keyed on test names (junit, CI
annotations, flake tracking) repeated identical entries:

✓ walking flight slide_ladder > a sim %s m below its foot is not held up by it
✓ walking flight slide_ladder > a sim %s m below its foot is not held up by it
✓ walking flight slide_ladder > a sim %s m below its foot is not held up by it

Workaround: use %d for numeric columns, which handles integers and floats, or convert the
column to a string before it reaches the title.

Related but distinct, both closed: #23206 ($name interpolation adds quotes) and #20566
(object-syntax placeholders not replaced).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions