ubq 0.0.7(beta)

This page as markdown

ubq guide: convert

How to move a project from tap, node:test, vitest, jest, or mocha.

How to move the tests of a project from tap, node:test, vitest, jest, or mocha to ubqtest. This text comes from the installed package. ubq guide convert prints it. Read ubq guide first: it has the API and the rules of ubq.

The steps#

  1. Record the old result. Run the old test command. Write down the count of tests that pass, and each test that fails or is skipped. You compare with this at the end.
  2. Install. npm install --save-dev ubqtest. Then npx ubq doctor, and do each FIX line. A line that says that ubq convert does the work needs no action in this step.
  3. Convert. npx ubq convert test (give the folders and files that have tests). --dry shows the report and changes no file. The command changes the files in place. Make sure that git status is clean before it, so that git diff shows the changes. It is safe to run the command again on one file.
  4. Do each TODO. The command writes // TODO(ubq): ... above each statement that it cannot change. It makes only the changes that are correct in each case, so a TODO can be easy to do by hand: you can see what the code around it does, and the command cannot. grep -rn 'TODO(ubq)' test lists them. The sections below tell how to do each kind. Remove the comment when its statement is done.
  5. Run. npx ubq. Do one failure at a time: npx ubq <file>:<line> runs one test. Read "Failures after the conversion" below before you change a test.
  6. Compare. The count of tests that pass must be the count of step 1, or you must know the reason for each difference. A test that passed before and is skipped or removed now is a defect of the conversion.
  7. Clean up. Set "test": "ubq" in the scripts of package.json. Remove the old test tool, its plugins, and its configuration (.taprc, vitest.config.*, jest.config.*, .mocharc.*, c8 and nyc settings), and the entries for the old tool in types of tsconfig.json. Change the test command of CI. The folder .ubq/ has the run store, and git ignores it with no setup. Commit ubq-snapshots/.

Do not change the source of the project to make a test pass, unless the test found a defect. Do not make an assertion weaker.

What ubq convert changes#

It does not change the name of a file or the source of the project. It changes package.json in one case: for a project of the vitest plugin for Workers, it writes the workers key of the ubq configuration, if the key is not there.

The report has one line for each TODO, with the number of a hint. The full text of each hint is below the files, one time.

Names of test files#

ubq runs files whose names end in .test.ts (or .test.js, .test.mts, .test.tsx, and so on). For tests with different names, do one of these:

Imports of the source#

tap#

tap ubq
t.test(name, async t => { ... }) test(name, async () => { ... })
t.equal(a, b), t.is, t.strictEqual assert.equal(a, b)
t.not(a, b), t.notEqual assert.notEqual(a, b)
t.same(a, b), t.strictSame, t.deepEqual assert.deepEqual(a, b)
t.ok(a), t.notOk(a) assert(a), assert(!a)
t.match(a, pattern), t.has assert.match(a, pattern)
t.throws(fn, expected) assert.throws(fn, expected) (Error if there was none)
await t.rejects(p, expected) await assert.rejects(p, expected)
t.resolveMatch(p, pattern) assert.match(await p, pattern)
t.type(a, 'string'), t.type(a, Class) assert.equal(typeof a, 'string'), assert(a instanceof Class)
t.error(err) assert(!err)
t.pass(), t.fail(msg) assert(true), assert.fail(msg)
t.matchSnapshot(a) assert.snapshot(a)
t.teardown(fn), t.after(fn) cleanup(fn)
t.testdir({ ... }) testdir({ ... })
t.mockImport('../a.js', { '../b.js': { f } }) mockImport(() => import('../a.js'), [[() => import('../b.js'), { f }]])
t.comment(...) console.log(...)
t.end(), t.plan(n) removed

Things to check by hand:

node:test and node:assert#

node ubq
it, test from node:test test
assert.strictEqual, assert.equal assert.equal
assert.deepStrictEqual, assert.deepEqual assert.deepEqual
assert.notStrictEqual assert.notEqual
assert.ok(a) assert(a)
assert.throws(fn), assert.rejects(p) the same with Error as the expected error
assert.ifError(err) assert(!err)
mock.fn(impl), mock.method(object, key) spy(impl), spyOn(object, key)

Things to check by hand:

vitest and jest#

vitest, jest ubq
it, test test
expect(a).toBe(b) assert.equal(a, b)
expect(a).toEqual(b), toStrictEqual assert.deepEqual(a, b)
expect(a).not.toBe(b) assert.notEqual(a, b)
toBeTruthy(), toBeFalsy() assert(a), assert(!a)
toBeNull(), toBeUndefined(), toBeDefined() assert.equal(a, null), assert.equal(a, undefined), assert.notEqual(a, undefined)
toContain(b), toHaveLength(n) assert(a.includes(b)), assert.equal(a.length, n)
toMatch(/re/), toMatch('text') assert.match(a, /re/), assert(a.includes('text'))
toMatchObject(o) assert.match(a, o)
toBeInstanceOf(C) assert(a instanceof C)
toBeGreaterThan(b) and the other comparisons assert(a > b)
expect(fn).toThrow(x) assert.throws(fn, x) (a text becomes a RegExp)
await expect(p).rejects.toThrow(x) await assert.rejects(p, x)
await expect(p).resolves.toEqual(b) assert.deepEqual(await p, b)
toHaveBeenCalled(), toHaveBeenCalledTimes(n) assert(s.calls.length > 0), assert.equal(s.calls.length, n)
toHaveBeenCalledWith(...args) assert.deepEqual(s.calls.at(-1)?.args, [...args])
toMatchSnapshot() assert.snapshot(a)
vi.fn(impl), jest.fn(impl) spy(impl)
vi.spyOn(object, key) spyOn(object, key)
it.skip(name, fn) test(name, { skip: 'the reason' }, fn)
it.todo(name) test(name, { skip: 'todo' }, () => {})
it(name, fn, 10000) test(name, { timeout: 10000 }, fn)

Things to check by hand:

mocha#

The command converts describe and it with node:assert. It does not convert chai or sinon. Write those assertions by hand with the tables above. this.timeout(ms) becomes test(name, { timeout: ms }, fn).

Hooks#

ubq has no beforeEach, afterEach, beforeAll, or afterAll.

Failures after the conversion#

Find the kind of the failure before you change a line.

The report says The cause Do this
the test ran no assertions The old tool passed a test with no check. Add the assertion that the test name promises. If the test only checks that a call does not throw, assert on its result.
expected values to be deeply equal, with a difference of type or prototype The old assertion was loose. Make the expected value exact, or use assert.match.
assert.rejects was not awaited The old tool waited for it. Add await.
after the test ended: ... The test starts work and does not wait for it. await the work. This was a defect in the old test.
the test changed the environment variable ... The test sets process.env and does not put it back. env({ NAME: 'value' }).
the test did not end in 5000 ms, with still open: ... The test leaves a server, a timer, or a process open. Close it with cleanup. The HANG block names the line that opened it.
duplicate test name Two tests of one group have one name. Make the names different.
the test file did not load A syntax error, an import that names no file, or syntax that Node cannot run. Read the line in the report. See "Imports of the source".
the file registered no tests The file is a helper, not a test file. Put it in "exclude".
a TYPE block The type checker runs with each run. Correct the type. A mock must have the types of the real export.
a TYPE block at a converted assertion, such as Argument of type 'string | undefined' is not assignable to parameter of type 'string' expect() took a value of each type. assert is strict: the actual value cannot be undefined where a method is called on it, and the expected value must have the type of the actual value (the parameter is NoInfer<T>). Add ! or ?. to the actual value, or narrow it first with assert(value). If the two types differ on purpose, write assert.deepEqual<unknown>(a, b).

A test that fails in ubq for one of the first six causes was not correct before: it passed for the wrong reason. Correct the test. Do not skip it.

When you are done#

Report to the user: