node-test-example

An example showing node:test, node --test, import 'tap' and tap run being used together, and how they interoperate.

This contains a test/tap.test.js and test/node.test.js, which both run essentially the same tests, one using tap and the other using node:test

The tests fail!

I know. That's kinda the point.

  • npm run test:node Executes both test suite files with the node --test runner.
  • npm run test:tap Execute both test suite files with the tap runner.

In both cases, you can see that the results are pretty similar.

Differences

  • When running with the tap runner, they're almost identical. The main thing is that the node:test doesn't provide per-assertion reporting, so you only see a report on the test block, and possibly the first failure, not all the assertions within it.
  • When running with the node --test runner, the tap test provides diffs and source callsite printing, while the node:test test shows a console.log() of the thrown Error.

Of course, the two runners produce very different output overall, but they should both be pretty sensible.

Personally, I think the tap runner is a lot more useful, and certainly if you write tests in TypeScript (or use tap's import mocking) it's nice to not have to specify the --loader and --import arguments explicitly.

But on the flip side, that fanciness comes with a cost. With TypeScript disabled, tap runs these two tests in about 450ms on my system (350ms or so with coverage disabled), while node --test does it in around 170ms. In both cases, the test/tap.test.js test takes around 150ms to run, and the test/node.test.js takes under 10ms.

Real world tests doing complicated stuff would show a less dramatic difference, so this is in no way a representative benchmark, but as always, performance and features are fundamentally opposed, because features require running code, and not running code is always faster.

The goal of the node:test interoperability in node-tap is to make it possible for you to get the best of both worlds. You could have part of your test suite written as node:test tests, if they don't need t.mockImport or TypeScript, and other tests written in tap.

Coverage

The test:mix and test:cross show using the node --test and tap runners so that they dump coverage into the same folder. Then you can use tap report to report on it.

Enough talk! Show the output!

GitHub strips colors from README.md files. A more representative example with colors can be found on the node-tap website.

Running with tap:

 FAIL  test/node.test.js 2 failed of 4 6.834ms
  suite of tests that fail > uhoh, this one throws
  suite of tests that fail > failer
 FAIL  test/tap.test.js 3 failed of 18 340ms
  suite of tests that fail > uhoh, this one throws > Invalid time value lib/index.mjs:11:43
  suite of tests that fail > failer > should be equal test/tap.test.js:35:7
  suite of tests that fail > failer > should be equal test/tap.test.js:37:7
                                    
                                            
  🌈 TEST COMPLETE 🌈                                                                       
                                                                            
                                                                             
 FAIL  test/node.test.js 2 failed of 4 6.834ms
  suite of tests that fail > uhoh, this one throws
    test/node.test.js                                                       
    20 })                                                                   
    21                                                                      
    22 test('suite of tests that fail', async t => {                        
    23   await t.test('uhoh, this one throws', () => {                      
    ━━━━━━━━━━━━━                                                          
    24     assert.equal(thrower(0), '1970-01-01T00:00:00.000Z')             
    25     assert.equal(thrower(1234567891011), '2009-02-13T23:31:31.011Z') 
    26     assert.equal(thrower({}), 'Invalid Date')                        
    27   })                                                                 
    error origin: lib/index.mjs                                  
     8                                                           
     9 // This is a function that throws, to show how both       
    10 // handle errors.                                         
    11 export const thrower = (n) => new Date(n).toISOString()   
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━               
    12                                                           
    13 // one that fails, to show how failures are handled       
    14 export const failer = (n) => String(n + 1)                
    error: Invalid time value
    code: ERR_TEST_FAILURE
    failureType: testCodeFailure
    name: RangeError
    Date.toISOString (<anonymous>)
    thrower (lib/index.mjs:11:43)
    TestContext.<anonymous> (test/node.test.js:26:18)
    TestContext.<anonymous> (test/node.test.js:23:11)

  suite of tests that fail > failer
    test/node.test.js                                                       
    26     assert.equal(thrower({}), 'Invalid Date')                        
    27   })                                                                 
    28                                                                      
    29   await t.test('failer', () => {                                     
    ━━━━━━━━━━━━━                                                          
    30     assert.equal(failer(1), '2')                                     
    31     assert.equal(failer(-1), '0')                                    
    32     // expect to convert string numbers to Number, but doesn't       
    33     assert.equal(failer('1'), '2')                                   
    error origin: test/node.test.js                                         
    30     assert.equal(failer(1), '2')                                     
    31     assert.equal(failer(-1), '0')                                    
    32     // expect to convert string numbers to Number, but doesn't       
    33     assert.equal(failer('1'), '2')                                   
    ━━━━━━━━━━━━━━                                                         
    34     // expect to convert non-numerics to 0, but it doesn't           
    35     assert.equal(failer({}), '1')                                    
    36   })                                                                 
    37 })                                                                   
    --- expected   
    +++ actual     
    @@ -1,1 +1,1 @@
    -"2"           
    +"11"          
    error: "'11' == '2'"
    code: ERR_ASSERTION
    failureType: testCodeFailure
    name: AssertionError
    operator: ==
    TestContext.<anonymous> (test/node.test.js:33:12)
    TestContext.<anonymous> (test/node.test.js:29:11)

 FAIL  test/tap.test.js 3 failed of 18 340ms
  suite of tests that fail > uhoh, this one throws > Invalid time value
    lib/index.mjs                                                
     8                                                           
     9 // This is a function that throws, to show how both       
    10 // handle errors.                                         
    11 export const thrower = (n) => new Date(n).toISOString()   
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━               
    12                                                           
    13 // one that fails, to show how failures are handled       
    14 export const failer = (n) => String(n + 1)                
    type: RangeError
    tapCaught: testFunctionThrow
    Date.toISOString (<anonymous>)
    thrower (lib/index.mjs:11:43)
    Test.<anonymous> (test/tap.test.js:27:13)

  suite of tests that fail > failer > should be equal
    test/tap.test.js                                                        
    32     t.equal(failer(1), '2')                                          
    33     t.equal(failer(-1), '0')                                         
    34     // expect to convert string numbers to Number, but doesn't       
    35     t.equal(failer('1'), '2')                                        
    ━━━━━━━━━                                                              
    36     // expect to convert non-numerics to 0, but it doesn't           
    37     t.equal(failer({}), '1')                                         
    38     t.end()                                                          
    39   })                                                                 
    --- expected   
    +++ actual     
    @@ -1,1 +1,1 @@
    -2             
    +11            
    compare: ===
    Test.<anonymous> (test/tap.test.js:35:7)
    Test.<anonymous> (test/tap.test.js:31:5)
    test/tap.test.js:23:3

  suite of tests that fail > failer > should be equal
    test/tap.test.js                                                        
    34     // expect to convert string numbers to Number, but doesn't       
    35     t.equal(failer('1'), '2')                                        
    36     // expect to convert non-numerics to 0, but it doesn't           
    37     t.equal(failer({}), '1')                                         
    ━━━━━━━━━                                                              
    38     t.end()                                                          
    39   })                                                                 
    40                                                                      
    41   t.end()                                                            
    --- expected     
    +++ actual       
    @@ -1,1 +1,1 @@  
    -1               
    +[object Object]1
    compare: ===
    Test.<anonymous> (test/tap.test.js:37:7)
    Test.<anonymous> (test/tap.test.js:31:5)
    test/tap.test.js:23:3

Asserts:  17 pass  5 fail  22 of 22 complete
Suites:    0 pass  2 fail    2 of 2 complete

# { total: 22, pass: 17, fail: 5 }
# time=459.924ms

Running with node --test:

 add (0.569917ms)
 stringOrNull (0.063833ms)
 suite of tests that fail
   uhoh, this one throws (0.910959ms)
    RangeError [Error]: Invalid time value
        at Date.toISOString (<anonymous>)
        at thrower (file:///Users/isaacs/dev/tapjs/node-test-example/lib/index.mjs:11:43)
        at TestContext.<anonymous> (file:///Users/isaacs/dev/tapjs/node-test-example/test/node.test.js:26:18)
        at Test.runInAsyncScope (node:async_hooks:206:9)
        at Test.run (node:internal/test_runner/test:631:25)
        at Test.start (node:internal/test_runner/test:542:17)
        at TestContext.test (node:internal/test_runner/test:167:20)
        at TestContext.<anonymous> (file:///Users/isaacs/dev/tapjs/node-test-example/test/node.test.js:23:11)
        at Test.runInAsyncScope (node:async_hooks:206:9)
        at Test.run (node:internal/test_runner/test:631:25)

   failer (0.532708ms)
    AssertionError [ERR_ASSERTION]: '11' == '2'
        at TestContext.<anonymous> (file:///Users/isaacs/dev/tapjs/node-test-example/test/node.test.js:33:12)
        at Test.runInAsyncScope (node:async_hooks:206:9)
        at Test.run (node:internal/test_runner/test:631:25)
        at Test.start (node:internal/test_runner/test:542:17)
        at TestContext.test (node:internal/test_runner/test:167:20)
        at TestContext.<anonymous> (file:///Users/isaacs/dev/tapjs/node-test-example/test/node.test.js:29:11)
        at async Test.run (node:internal/test_runner/test:632:9)
        at async Test.processPendingSubtests (node:internal/test_runner/test:374:7) {
      generatedMessage: true,
      code: 'ERR_ASSERTION',
      actual: '11',
      expected: '2',
      operator: '=='
    }

 suite of tests that fail (1.684292ms)

 add (1.774ms)
 stringOrNull (1.091ms)
 suite of tests that fail
   uhoh, this one throws (10.016ms)
    Error: Invalid time value
    | // This is a function that throws, to show how both
    | // handle errors.
    | export const thrower = (n) => new Date(n).toISOString()
    | ------------------------------------------^
    | 
    | // one that fails, to show how failures are handled
        at Date.toISOString (<anonymous>)
        at thrower (/Users/isaacs/dev/tapjs/node-test-example/lib/index.mjs:11:43)
        at Test.<anonymous> (/Users/isaacs/dev/tapjs/node-test-example/test/tap.test.js:27:13) {
      type: 'RangeError',
      tapCaught: 'testFunctionThrow'
    }

   failer (3.676ms)
    Error: should be equal
    --- expected                                                               
    +++ actual                                                                 
    @@ -1,1 +1,1 @@                                                            
    -2                                                                         
    +11                                                                        
    |     t.equal(failer(-1), '0')
    |     // expect to convert string numbers to Number, but doesn't
    |     t.equal(failer('1'), '2')
    | ------^
    |     // expect to convert non-numerics to 0, but it doesn't
    |     t.equal(failer({}), '1')
        at Test.<anonymous> (/Users/isaacs/dev/tapjs/node-test-example/test/tap.test.js:35:7)
        at Test.<anonymous> (/Users/isaacs/dev/tapjs/node-test-example/test/tap.test.js:31:5)
        at /Users/isaacs/dev/tapjs/node-test-example/test/tap.test.js:23:3 {
      compare: '==='
    }

 suite of tests that fail (17.681ms)

 tests 9
 suites 1
 pass 4
 fail 5
 cancelled 0
 skipped 0
 todo 0
 duration_ms 160.809375

 failing tests:

test at file:/Users/isaacs/dev/tapjs/node-test-example/test/node.test.js:23:11
 uhoh, this one throws (0.910959ms)
  RangeError [Error]: Invalid time value
      at Date.toISOString (<anonymous>)
      at thrower (file:///Users/isaacs/dev/tapjs/node-test-example/lib/index.mjs:11:43)
      at TestContext.<anonymous> (file:///Users/isaacs/dev/tapjs/node-test-example/test/node.test.js:26:18)
      at Test.runInAsyncScope (node:async_hooks:206:9)
      at Test.run (node:internal/test_runner/test:631:25)
      at Test.start (node:internal/test_runner/test:542:17)
      at TestContext.test (node:internal/test_runner/test:167:20)
      at TestContext.<anonymous> (file:///Users/isaacs/dev/tapjs/node-test-example/test/node.test.js:23:11)
      at Test.runInAsyncScope (node:async_hooks:206:9)
      at Test.run (node:internal/test_runner/test:631:25)

test at file:/Users/isaacs/dev/tapjs/node-test-example/test/node.test.js:29:11
 failer (0.532708ms)
  AssertionError [ERR_ASSERTION]: '11' == '2'
      at TestContext.<anonymous> (file:///Users/isaacs/dev/tapjs/node-test-example/test/node.test.js:33:12)
      at Test.runInAsyncScope (node:async_hooks:206:9)
      at Test.run (node:internal/test_runner/test:631:25)
      at Test.start (node:internal/test_runner/test:542:17)
      at TestContext.test (node:internal/test_runner/test:167:20)
      at TestContext.<anonymous> (file:///Users/isaacs/dev/tapjs/node-test-example/test/node.test.js:29:11)
      at async Test.run (node:internal/test_runner/test:632:9)
      at async Test.processPendingSubtests (node:internal/test_runner/test:374:7) {
    generatedMessage: true,
    code: 'ERR_ASSERTION',
    actual: '11',
    expected: '2',
    operator: '=='
  }

test at test/tap.test.js:24:5
 uhoh, this one throws (10.016ms)
  Error: Invalid time value
  | // This is a function that throws, to show how both
  | // handle errors.
  | export const thrower = (n) => new Date(n).toISOString()
  | ------------------------------------------^
  | 
  | // one that fails, to show how failures are handled
      at Date.toISOString (<anonymous>)
      at thrower (/Users/isaacs/dev/tapjs/node-test-example/lib/index.mjs:11:43)
      at Test.<anonymous> (/Users/isaacs/dev/tapjs/node-test-example/test/tap.test.js:27:13) {
    type: 'RangeError',
    tapCaught: 'testFunctionThrow'
  }

test at test/tap.test.js:31:5
 failer (3.676ms)
  Error: should be equal
  --- expected                                                               
  +++ actual                                                                 
  @@ -1,1 +1,1 @@                                                            
  -2                                                                         
  +11                                                                        
  |     t.equal(failer(-1), '0')
  |     // expect to convert string numbers to Number, but doesn't
  |     t.equal(failer('1'), '2')
  | ------^
  |     // expect to convert non-numerics to 0, but it doesn't
  |     t.equal(failer({}), '1')
      at Test.<anonymous> (/Users/isaacs/dev/tapjs/node-test-example/test/tap.test.js:35:7)
      at Test.<anonymous> (/Users/isaacs/dev/tapjs/node-test-example/test/tap.test.js:31:5)
      at /Users/isaacs/dev/tapjs/node-test-example/test/tap.test.js:23:3 {
    compare: '==='
  }