npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@click-click/testnow

v1.0.2

Published

Unit testing for Typescript

Downloads

13

Readme

testnow

Simple unit testing for Typescript

Installation

npm install --save-dev @click-click/testnow

Configuration

Type declaration of global variables

Just before running your tests, testnow will add its TestCallSupervisor as a global variable $. You need to tell Typescript about this global variable in your declaration file like so:

import testnow from "@click-click/testnow"

declare global {
    var $: typeof testnow.$
}

package.json

If Typescript outputs the JS files of your tests in js/tests/, set the "test" script in your package.json:

{
  ...
  "scripts": {
    "test": "testnow js/tests/",
    ...
  },
  ...
}

In a nutshell:

  • testnow recursively goes through js/tests/ and run the tests of all the files, provided they are .js or .cjs or .mjs files.
  • testnow does not recompile your TS test files. You may have a watcher with your own build setup. In case you do not, you can use the watcher of Typescript or you can prefix the "test" command to force TS compilation before running your tests, like so:
    "test": "tsc --project tsconfig.json & testnow js/tests/"
    Note however that this approach recompiles your whole project so it may be slow if you have a lot of files.

Command-line options

  • onlyLastModified <strictly-positive-integer>

    If specified, testnow ignores any test file whose latest time of modification is above the given number (in milliseconds). This allows you to focus on the tests you are currently writing by leaving out past test files. It also quickens the whole test execution.

    The integer is optional. Default value is 1_800_000ms = 30min.

    Example:

    "test": "testnow js/tests/ onlyLastModified 800000"
  • skipTimeboxedTests

    If specified, testnow skips test with a timeout. The rationale is:

    • if a test requires a timeout, then it must take quite some time compared to other tests
    • such tests should be rare

    Under these assumptions, skipTimeboxedTests quickens the whole test execution at the cost of leaving out only a handful of tests.

    For some perspective, at the time of this writing, I have about 6300 tests free of timeout. They take a bit less than 10 seconds to run, meaning a test takes on average 1.6 milliseconds. On the other hand, the lowest timeout I have is 50ms, others are around 200ms and some are even higher (1s and 10s). So, worst case scenario, the lowest timeboxed test takes 31 more time than a regular test.

    Also, if one timeboxed test has degraded performance or even hits the timeout, it probably won't be alone. Case n°1 is that the tested function has issues, so all its other timeboxed tests will take longer too. Case n°2 is that the problem is deeper in your environment, in which case maybe all timeboxed tests will have degraded performance.

    skipTimeboxedTests is nice when you know the timeboxed tests pass and when you now need to focus on writing other, non-timeboxed tests.

    Example:

    "test": "testnow js/tests/ skipTimeboxedTests"

Aliases

Since you can pass the options to npm test directly, it is a good idea to have a minimal call to testnow in the "test" script:

"test": "testnow js/tests/"

You can then design some specific aliases. I recommend these two:

alias nt='clear && npm test onlyLastModified skipTimeboxedTests' # quick and focused tests
alias nta='clear && npm test' # executes all the tests

Writing tests

Basic example

Assuming this code in file ts/sources/pathname.ts:

export function normalizePathname(pathname: string): string {
  // => no backslash in pathname, only slashes
  return pathname.replace(/\\/gu, '/')
}

You can define your tests in ts/tests/pathname/normalizePathname.ts like this:

import { normalizePathname } from "../../sources/pathname"

$(normalizePathname, '').equals('')
$(normalizePathname, 'foo\\').equals('foo/')
$(normalizePathname, 'foo\\bar').equals('foo/bar')
$(normalizePathname, 'foo\\bar\\').equals('foo/bar')
/*-------------------------------  ------ ---------
            ^                         ^       ^
        call specification          check     expected value
*/

This approach may seem limited but it is actually an open door to powerful ways of expressing tests: please refer to the HOWTO for more ideas.

API

There are two call specifications:

  1. $(fn, ...arguments): will perform the call fn(...arguments) and wait for its completion.
  2. $.stopPast(number, fn, ...arguments): will perform the call fn(...arguments) and wait for <number> milliseconds before considering the test to have timed out. Uses Promise.race and so only works for asynchronous functions.

:white_check_mark: Note that both uses of $ are type-checked: you will get an error if the type of one of the arguments does not match the type specified on the function.

There are two checks: | Check | When to use it? | Expected value | Comparison | | -------- | ----------------------------------- | -------------- | ------------------ | | equals | when the call is expected to return | any value | isDeepStrictEqual: you can pass any object, array, map, etc. | | throws | when the call is expected to throw | an error class | class must match ; no comparison done on the actual fields of the instance |

:warning: If you forget the check part, testnow silently skips the call specification and it won't even appear in the test statistics at the end.

:white_check_mark: The result is also type-checked, which is particularly useful when it is a complex object. There is one case where type-checking has some troubles though... Refer to the section Typechecking issue with overload.

Organizing your tests

testnow does not care about how your tests are organized, nor how your folders and files are named. Test files are just regular code files so you can import any function you want from any file you like. Here is the layout I personally used.

ts/
  sources/
    sourceNameA.ts -> file holding the functions 'foo' and 'bar'
    sourceNameB.ts -> file holding the function 'baz'
    ...

  tests/
    sourceNameA/
      foo.ts       -> test only the function 'foo'
      bar.ts       -> test only the function 'bar'
    sourceNameB/
      baz.ts       -> test only the function 'baz'
    ...

I have started to have other situations but I cannot provide any clear advice, as I'm still thinking this through (like testing a serializer: one test file actually checks that the serialize and deserialize functions are the opposite of one another).

Crucially, as I very rarely use classes, I have not developed any particular organization for them.

Advanced

Programmatic use

To run the tests from your own scripts, testnow exports 3 functions:

  • $ to declare a test case
  • executeTestCallsOfFolder which has all the functional machinery to run the tests
  • executeTestCallsOfFolderByCommandLine which calls the previous one after having parsed the command line and resolved the configuration to use

For reference, here is how is coded the 'testnow' binary script (for use in your package.json scripts):

import('../dist/index.js').then((a) => {
    global.$ = a.$
    a.executeTestCallsOfFolderByCommandLine(a.$)
})

Process

testnow scans the directory you have specified and look for appropriate files, that is:

  1. any JS-like file: .js / .cjs / .mjs
  2. with a young enough modification time (if you specified the option onlyLastModified)

If a file matches the above criteria, then it is dynamically imported:

  • importing never execute the tests: the calls to $ are only collecting test cases.
  • if any code throws during the import, the collect of test cases is stopped and no test will run.
  • if the import succeeded, the test cases are executed one by one, in the exact order of registration.

As a test file is a regular code file, you can use any code, not just calls to $. As such, it enables some approaches documented in the HOWTO.