Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] TestSuite fails with Tap 18, even though all asserts pass or skip #964

Closed
3 tasks done
Chaz0r opened this issue Oct 25, 2023 · 15 comments
Closed
3 tasks done
Labels
bug something not go good

Comments

@Chaz0r
Copy link

Chaz0r commented Oct 25, 2023

Is there an existing issue for this?

  • I have searched the existing issues

Have you read the CONTRIBUTING guide on posting bugs, and CODE_OF_CONDUCT?

  • yes I read the things

This issue exists in the latest tap version

  • I am using the latest tap

Description

Hi folks,

I started to upgrade to the latest Tap 18 Version from the latest Tap 16 version. My backend service is a Typescript service with fastify in its core.

We are structuring our TestSuites like this:

export default class MyTest extends ServiceTestSuite {
    defineTests() {
        //here are the tests
    }
}

It is important for us to define the tests in that function because we are doing plenty of things before the tests should be executed. We are not using the before and after methods of tap here because the things we do in the ServiceTestSuite are things which should happen even before the before method.

A working test looked like this with tap 16:

export default class MyTest extends ServiceTestSuite {
    defineTests() {
        this.test('my test', async (t: Tap.Tap) => {
            t.pass("works")
        })
    }
}

this.test has been defined in the TestSuite like this:

tap = require('tap')
test = this.tap.test

This worked pretty fine in Tap 16.

Now I started to migrate to 18.
I changed the tap version to the latest 18 and installed the @tapjs/test package.
After that I looked into the docs and figured out, that the imports have changed to

import t from "tap"
import { Test } from '@tapjs/test'

What I then did was to use those imports in the Test Class like this:

import t from "tap"
import { Test } from '@tapjs/test'

export default class MyTest extends ServiceTestSuite {
    defineTests() {
        t.test('my test', async (t: Test) => {
            t.pass("works")
        })
    }
}

And with that I have some kind of Schrödingers test visibility.
This fails like it should:

export default class MyTest  extends ServiceTestSuite {
    defineTests() {
        t.test('my test', async (t: Test) => {
            t.equal(1,2)
        })
    }
}

It correctly says which assertion failed and why.

export default class MyTest  extends ServiceTestSuite {
    defineTests() {
        t.test('my test', async (t: Test) => {
            t.equal(1,1)
        })
    }
}

This does not show any assertion error (which is fine), but in the end, the whole TestSuite failes because now tests were found:

 FAIL  tests/tests/elasticsearch/MyTest.ts 1 OK 1.783s
 ~ no tests found

Asserts:  1 pass  0 fail  1 skip  1 of 2 complete
Suites:   0 pass  1 fail  0 skip  1 of 1 complete

I removed all other TestSuites for the moment so this one is the only one that gets executed.
Also it looks like it works when I create a completely new TestSuite, where the tests are not defined inside a function. At least I don' get the issue there.

Reproduction

At the moment no idea :(

Environment

pnpm latest version
tap latest version (only run via pnpm)
node 20.6.1

@Chaz0r Chaz0r added the bug something not go good label Oct 25, 2023
@Chaz0r Chaz0r changed the title [BUG] TestSuite fails with Tap 18, even though all asserts pass [BUG] TestSuite fails with Tap 18, even though all asserts pass or skip Oct 25, 2023
@tengla
Copy link

tengla commented Oct 25, 2023

I had difficulties with this too, but found a mysterious workaround (I work in a ts/fastify environment). It goes something like this

alias tap=node_modules/.bin/tap
tap --files --disable-coverage --reporter=tap --allow-incomplete-coverage src/**/*test.ts

@isaacs
Copy link
Member

isaacs commented Oct 25, 2023

@tengla That --files --disable-coverage is trying to set the files config to ['--disable-coverage'], which is likely not what you want?

@isaacs
Copy link
Member

isaacs commented Oct 25, 2023

@Chaz0r so, couple of things here:

You probably do not want to install @tapjs/test as a top-level dep. Instead, what you ought to do is use the Test class that's exported by tap itself.

But also, you can avoid even having to do that, by just letting TS figure out the argument type from the function type.

https://github.com/tapjs/gh-964/blob/main/test/service-test-suite.ts

import t from 'tap'

export default abstract class ServiceTestSuite {
  test = t.test.bind(t)
  constructor () {
    this.defineTests()
  }
  abstract defineTests(): void
}

At some point, you have to actually instantiate that class and call the defineTests() method, are you doing that somehow?

Can you share the full code that's in play here, and the output of these commands?

tap versions
tap config list
tap list
tap -Rtap

@tengla
Copy link

tengla commented Oct 25, 2023

@tengla That --files --disable-coverage is trying to set the files config to ['--disable-coverage'], which is likely not what you want?

my bad, what made the non zero exit was actually lacking test coverage.

@Chaz0r
Copy link
Author

Chaz0r commented Oct 26, 2023

You probably do not want to install @tapjs/test as a top-level dep. Instead, what you ought to do is use the Test class that's exported by tap itself.

If I dont do that, tap directly fails with the following error:

node:internal/process/esm_loader:48
      internalBinding('errors').triggerUncaughtException(
                                ^

[Error: Cannot find package '@tapjs/test' imported from /home/cb/svw/MyService/x] {
  code: 'ERR_MODULE_NOT_FOUND'
}

I dont have an import in my service directly, so it looks like the import is missing somewhere implicitly. After I install the package manually, the issue is "fixed".

But also, you can avoid even having to do that, by just letting TS figure out the argument type from the function type.

https://github.com/tapjs/gh-964/blob/main/test/service-test-suite.ts

import t from 'tap'

export default abstract class ServiceTestSuite {
  test = t.test.bind(t)
  constructor () {
    this.defineTests()
  }
  abstract defineTests(): void
}

I will check that!

At some point, you have to actually instantiate that class and call the defineTests() method, are you doing that somehow?

Yep, directly under the class definition. Pretty similar to this:

export default class MyTest extends ServiceTestSuite {
    //here all the test code
}

const myInstance = new MyTest()
myInstance.run()

Can you share the full code that's in play here, and the output of these commands?

tap versions
tap config list
tap list
tap -Rtap

Output of these commands are the following:
tap versions:

tap: 18.5.2
"@tapjs/config": 2.4.9
"@tapjs/core": 1.4.2
"@tapjs/run": 1.4.9
"@tapjs/stack": 1.2.6
"@tapjs/test": 1.3.13
tap-parser: 15.3.0
tap-yaml: 2.2.0
tcompare: 6.4.3
plugins:
  "@tapjs/after": 1.1.13
  "@tapjs/after-each": 1.1.13
  "@tapjs/asserts": 1.1.13
  "@tapjs/before": 1.1.13
  "@tapjs/before-each": 1.1.13
  "@tapjs/filter": 1.2.13
  "@tapjs/fixture": 1.2.13
  "@tapjs/intercept": 1.2.13
  "@tapjs/mock": 1.2.11
  "@tapjs/node-serialize": 1.2.2
  "@tapjs/snapshot": 1.2.13
  "@tapjs/spawn": 1.1.13
  "@tapjs/stdin": 1.1.13
  "@tapjs/typescript": 1.3.2
  "@tapjs/worker": 1.1.13

tap config list

# vim: set filetype=yaml :
color: true
coverage-report:
  - text
exclude:
  - "**/@(fixture*(s)|dist)/**"
include:
  - "**/@(test?(s)|__test?(s)__)/**/*.@(js|cjs|mjs|tap|cts|jsx|mts|ts|tsx)"
  - "**/*.@(test?(s)|spec).@(js|cjs|mjs|tap|cts|jsx|mts|ts|tsx)"
  - "**/test?(s).@(js|cjs|mjs|tap|cts|jsx|mts|ts|tsx)"
jobs: 12
reporter: base
snapshot-clean-cwd: true
timeout: 30

tap list

tests/TestSuite.ts
tests/TestConfig.ts
tests/ServiceTestsuite.ts
tests/mocks/PartnerDataFaker.ts
tests/mocks/MockPartnerManagerApiClient.ts
tests/mocks/EsClientMock.ts
tests/tests/partnermanager/VoucherValidatorTest.ts
tests/tests/partnermanager/PartnerValidatorTest.ts
tests/tests/partnermanager/ImportVoucherToDocumentConverterTest.ts
tests/tests/partnermanager/ImportPartnerToDocumentConverterTest.ts
tests/tests/jobs/PartnerManagerImportJobTest.ts
tests/tests/elasticsearch/FilterTest.ts

tap -Rtap

TAP version 14
1..12
# Subtest: tests/mocks/EsClientMock.ts
    1..0 # no tests found
ok 1 - tests/mocks/EsClientMock.ts # SKIP no tests found

# Subtest: tests/mocks/MockPartnerManagerApiClient.ts
    1..0 # no tests found
ok 2 - tests/mocks/MockPartnerManagerApiClient.ts # SKIP no tests found

# Subtest: tests/mocks/PartnerDataFaker.ts
    1..0 # no tests found
ok 3 - tests/mocks/PartnerDataFaker.ts # SKIP no tests found

# Subtest: tests/ServiceTestsuite.ts
    1..0 # no tests found
ok 4 - tests/ServiceTestsuite.ts # SKIP no tests found

# Subtest: tests/TestConfig.ts
    1..0 # no tests found
ok 5 - tests/TestConfig.ts # SKIP no tests found

# Subtest: tests/tests/elasticsearch/FilterTest.ts
    # Subtest: test filter without sorter exception
        ok 1 - this passes
        1..1
    ok 1 - test filter without sorter exception # time=2.292ms
    
    1..1
ok 6 - tests/tests/elasticsearch/FilterTest.ts # time=2928.297ms

# Subtest: tests/tests/jobs/PartnerManagerImportJobTest.ts
    1..0 # no tests found
ok 7 - tests/tests/jobs/PartnerManagerImportJobTest.ts # SKIP no tests found

# Subtest: tests/tests/partnermanager/ImportPartnerToDocumentConverterTest.ts
    1..0 # no tests found
ok 8 - tests/tests/partnermanager/ImportPartnerToDocumentConverterTest.ts # SKIP no tests found

# Subtest: tests/tests/partnermanager/ImportVoucherToDocumentConverterTest.ts
    1..0 # no tests found
ok 9 - tests/tests/partnermanager/ImportVoucherToDocumentConverterTest.ts # SKIP no tests found

# Subtest: tests/tests/partnermanager/PartnerValidatorTest.ts
    1..0 # no tests found
ok 10 - tests/tests/partnermanager/PartnerValidatorTest.ts # SKIP no tests found

# Subtest: tests/tests/partnermanager/VoucherValidatorTest.ts
    1..0 # no tests found
ok 11 - tests/tests/partnermanager/VoucherValidatorTest.ts # SKIP no tests found

# Subtest: tests/TestSuite.ts
    1..0 # no tests found
ok 12 - tests/TestSuite.ts # SKIP no tests found

# { total: 12, pass: 1, skip: 11 }
# time=3073.042ms

Regarding the full code:
That would probably a bit to much for all the things we are doing. I hope this represents enough:

//abstract application is the main app that starts our whole application. doing tests "inside" our application makes way more easy and worked fine with tap 16 and it looks like this still works pretty well
import t from "tap"
export abstract class ServiceTestSuite extends AbstractApplication {

    async startApp(): Promise<void> {
        //start the service and execute tests
    }

    async registerDependencies() {
        await super.registerDependencies()
        this.registerBaseMockDependencies()
    }

    private async registerBaseMockDependencies() {
        //register mocks here to the DI
    }

    abstract configureBaseMockDependencies(): NameAndRegistrationPair<Cradle>[] 

    private async makeTestsuiteReadyWithOrm() {
        await this.fastify.orm.initialize().then(async () => {
                await this.makeTestSuiteReady()
            }
        ).catch((error) => console.log(error))
    }

    private async makeTestSuiteReady() {
        await this.fastify.ready()
        await this.registerBefore()
        await this.registerGlobalBeforeFunctions()
        await this.registerAfter()
        await this.registerGlobalAfterFunctions()
        await this.registerJobs()

        await this.defineTests()
    }

    private async registerGlobalBeforeFunctions() {
        // eslint-disable-next-line @typescript-eslint/no-empty-function
        await t.beforeEach(async () => {
        	//currently empty, wont be in the future
        })

        // eslint-disable-next-line @typescript-eslint/no-empty-function
        await t.before(async () => {
        	//currently empty, wont be in the future
        })
    }

    private async registerGlobalAfterFunctions() {
        await t.afterEach(async () => {
            //we refresh everything here so every test has a clean DI and clean DB
            await this.refreshFastify()
        })

        await t.teardown(async () => {
            this.fastify.close()
        })
    }

    abstract defineTests(): void

    async changeDependencies(nameAndRegistrationPairs: NameAndRegistrationPair<Cradle>[]) {
        //here we can change dependencies for mocked dependencies
    }

    async registerJobs() {
        const jobRegistrator = new JobRegistrator(this.fastify)
        jobRegistrator.registerJobs()
    }


    async refreshFastify() {
        //this resets all dependencies
    }

    abstract registerBefore(): void

    abstract registerAfter(): void

    getLogStreams(): SpeedifyLogStream[] | null {
        return null
    }
}


import t from "tap"
import { Test } from '@tapjs/test'

export default class FilterTest extends ServiceTestSuite {

    defineTests(): void {
       t.test('test filter without sorter exception', async (test: Test) => {
            test.pass('this passes')
       })
    }
    registerBefore(): void {

    }
    registerAfter(): void {
    
    }

}

const testInstance = new FilterTest()
testInstance.startApp()

What I figured out: If I remove the tap import and the before- and after functions and only use the imported tap inside the Test class, the issue seems to be gone.

Edit:
It looks like the overall issue is really if I import tap in the ServiceTestSuite, do something with tap there and import it again in the test and do something with tap there.
If I save test inside a variable like you suggested, and operate on tap via the variable, it works.

Beside that I still need the @tapjs/test package, otherwise I get the error I mentioned above.

@isaacs
Copy link
Member

isaacs commented Oct 26, 2023

The error when you don't have @tapjs/test at the top level is because you're importing @tapjs/test in your test. You can remove that, and the type annotation on the t.test callback, since it's already typed.

Or, if you prefer, you can import t, { Test } from 'tap' to get the exact copy of the Test class that tap is using.

@Chaz0r
Copy link
Author

Chaz0r commented Oct 26, 2023

The error when you don't have @tapjs/test at the top level is because you're importing @tapjs/test in your test. You can remove that, and the type annotation on the t.test callback, since it's already typed.

Or, if you prefer, you can import t, { Test } from 'tap' to get the exact copy of the Test class that tap is using.

Nope, I removed all of those imports. I still get the error.

Also another problem that drives me crazy with version 18 (16 did not have that problem) is the following:
The TestSuite is in a library that we include as a regular dependency in our service.

Like you suggested, I saved the test variable like this:
test = t.test.bind(t)
That works.

The beforeEach and afterEach functions can only be used on t and not on t.test.
So I saved t also in my testsuite into a variable like this:

tap: typeof t = t

Technically in terms of tests, everything works now. All tests are running, all assertions green, nothing failing.

But if I start my service now, the service stops because tap is running into a timeout. The service itself does not have any references to tap. I commented all the tests so the service even does not have any tests implemented at the moment.

And only having the TestSuite in my library and the library including into my service results in the following error:

TAP version 14
not ok 1 - timeout!

signal: null
handles:
- type: Server
events:
- request
- connection
- listening
- clientError
connectionKey: 4:0.0.0.0:3000
- type: Socket
events:
- end
- error
- timeout
- close
- data
- type: TLSSocket
events:
- close
- end
- newListener
- secure
- session
- free
- timeout
- agentRemove
- error
- data
- drain
expired: TAP
test: TAP

After a lot of trial and error I figured out which line causes the problem and its the following:
tap: typeof t = t

t is imported like this: import t from 'tap'.

If I comment this line, the service starts again. If I re-add the line, the service fails after about 30 seconds with the error above.

@isaacs
Copy link
Member

isaacs commented Oct 26, 2023

Nope, I removed all of those imports. I still get the error.

That's not possible. You might be getting a different error about @tapjs/test trying to load and not being found, but if you remove the line that's importing @tapjs/test, and the error persists, then the error wasn't coming from that line, it was coming from somewhere else or is a different error (or both).

But if I start my service now, the service stops because tap is running into a timeout.

So, if I understand this correctly, you're importing tap, and calling some before/after registration methods, in your actual production application? If so, that's.... unusual. I've never even thought about doing that.

It's shutting down after 30 seconds because the root test is registering itself on the process. Then it times out after the default test timeout of 30 seconds, because tests are intended to complete in less time than that. The expectation is that you wouldn't import t from 'tap' and then do stuff with that t object if it's not a test. You can suppress the timeout by setting TAP_TIMEOUT=0 in the environment, but... idk, this is really weird. In an "intriguing" sense, I'm not judging, it's just very interesting and curious to me. I would like to know more 😅

I feel like I really would be of more help to you here if I could get access to see some actual code failing locally. If it's too hard to carve it down a minimal reproduction, and you don't want to share a link publicly, feel free to DM me on the tapjs matrix chat or any discords/slacks we might both be in. I'm also happy to sign an MNDA if it's a private company thing.

@Chaz0r
Copy link
Author

Chaz0r commented Oct 26, 2023

Thanks for the reply.
Regarding the error about @tapjs/test: yes I removed the imports and the exact error I posted above occures. Then I changed the import of Test so it gets imported from tap and the error occures. I even removed all tests beside one for testing. So I only had the TestSuite class and one class extending from the TestSuite containing only one test for debugging. So not much where that could come from. But it is the exact same error as I mentioned above (:

Regarding the other issue: We use a Custom library for our services. This library contains plenty of code a service can use. Inside that Library there exists the TestSuite class. This class is NOT used in the production code. It is only used in the tests. The production code does not use test logic, test classes, tap or whatever is used for testing. It only exists in the lib so we can use it in tests. If we wouldnt have tests in the service, this TestSuite class would only float around in the library.
And this is exactly what I did. I deleted all tests so I can be sure there is only one place where tap exists: Inside the Library within the TestSuite.
And inside that TestSuite I import tap like I mentioned above. And saving t into a variable results in tap execution in Production Code...somehow.

Regarding code....yep....this is not that easy. It just is not a simple company topic, it is more a huge credit institute topic, which makes sharing code pretty hard >.<

@Chaz0r Chaz0r closed this as not planned Won't fix, can't repro, duplicate, stale Oct 27, 2023
@isaacs
Copy link
Member

isaacs commented Oct 27, 2023

Did you figure it out?

@MattInReality
Copy link

@Chaz0r I've found this as a result of some odd behaviour in my IDE, can you run the tests in an earlier version of node and see if they run without the error? I have a suite testing Fastify also, though way more rudimental than yours.

Webstorm was hanging the suite while all tests passed, but running the tests from the terminal completed successfully. Then I checked node version and the terminal was running Node version 18.17.0. When I changed the terminal version to 20.8.0 it hung like it does in Webstorms run config (which is also using 20.8.0).

@digitalsadhu
Copy link

Unsure if related but all codebases I've updated to v18 have started returning error code 1 even though the tests all look like they pass. I've tried disabling code coverage in case it had something to do with this but no dice.

@beliven-daniele-sarnari
Copy link

beliven-daniele-sarnari commented Nov 7, 2023

Unsure if related but all codebases I've updated to v18 have started returning error code 1 even though the tests all look like they pass. I've tried disabling code coverage in case it had something to do with this but no dice.

You need to put all these three flags in order to fully disable code coverage check:
--disable-coverage --allow-incomplete-coverage --allow-empty-coverage

@isaacs
Copy link
Member

isaacs commented Nov 7, 2023

Technically you only need --allow-incomplete-coverage if you're generating coverage but not full coverage.

@isaacs
Copy link
Member

isaacs commented Nov 7, 2023

@digitalsadhu please post a new issue, with the info that the bug report asks for. "Looks like they all pass" is not sufficient detail to help you in any way.

@tapjs tapjs locked as resolved and limited conversation to collaborators Nov 7, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
bug something not go good
Projects
None yet
Development

No branches or pull requests

6 participants