Testování kódu Solid.js nad rámec vtipu

Začali jste tedy psát aplikaci nebo knihovnu v Solid.js a TypeScript – což je skvělá volba – ale nyní chcete vše testovat co nejrychleji, abyste se vyhnuli regresím.

Již víme, jak to udělat s jest , ale i když je to docela pohodlné a docela snadné nastavit, je to také značně pomalé a poněkud tvrdohlavé. Na rozdíl od lehčích testovacích zařízení má také vestavěné rozhraní API pro transformaci kódu, prostředí DOM založené na jsdom a volí browser ve výchozím nastavení podmíněné exporty.

Co tedy potřebujeme ke spuštění našich testů bez jest je:

  1. Transformace kódu
  2. Prostředí DOM
  3. Výběr browser export

solid-registr

Abych ušetřil ještě více vašeho drahocenného času, všechnu tuto práci jsem již udělal za vás. Stačí nainstalovat

npm i --save-dev solid-register jsdom

a spusťte svého testovacího běžce s

# test runner that supports the `-r` register argument
$testrunner -r solid-register ...

# test runner without support for the `r` argument
node -r solid-register node_modules/.bin/$testrunner ...

Testovací běžec

Kromě žertu máte jistě spoustu možností:

  • uvu (nejrychlejší, ale postrádá některé funkce)
  • tape (rychlé, modulární, rozšiřitelné, mnoho vidlic nebo rozšíření jako supertape, tabe, tappedout)
  • ava (stále rychle)
  • bron (malé, téměř žádné funkce, rychlé)
  • karma (trochu pomalejší, ale velmi dospělý)
  • test-turtle (poněkud pomalejší pro úplný test, ale spouští pouze testy, které testují soubory, které selhaly nebo se změnily od posledního spuštění)
  • jasmine (poněkud plně vybavený testovací systém, na kterém je vtip částečně založen)

a pravděpodobně mnohem více; Nemohl jsem je všechny otestovat, takže se zaměřím na uvu a tape . Oba podporují argument registru, takže vše, co musíte udělat, je nainstalovat je

npm -i --save-dev uvu
# or
npm -i --save-dev tape

a přidejte skript do svého projektu:

{
  "scripts": {
    "test": "uvu -r solid-register"
  }
}
// or
{
  "scripts": {
    "test": "tape -r solid-register"
  }
}

Nyní můžete své projekty testovat pomocí npm test .

Testování vlastního primitiva (háku)

Představte si, že máte znovu použitelnou reaktivní funkci pro Solid.js, která nic nevykresluje, a proto nepotřebujete používat render() . Jako příklad otestujme funkci, která vrací několik slov nebo text „Lorem ipsum“:

const loremIpsumWords = 'Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'.split(/\s+/);

const createLorem = (words: Accessor<number> | number) => {
  return createMemo(() => {
    const output = [],
      len = typeof words === 'function' ? words() : words;
    while (output.length <= len) {
      output.push(...loremIpsumWords);
    }

    return output.slice(0, len).join(' ');
  });
};

Potřebujeme zabalit akce našeho testu do reaktivního kořenového adresáře, abychom umožnili předplatné přístupových prvků jako words . Pro uvu , vypadá to takto (na pásce jsou tvrzení v prvním argumentu, že test hovor přijímá, vše ostatní je velmi podobné):

import { createEffect, createRoot, createSignal } from "solid-js";
import { suite } from 'uvu';
import * as assert from 'uvu/assert';

const testLorem = suite('createLorem');

testLorem('it updates the result when words update', async () => {
  const input = [3, 2, 5],
  expectedOutput = [
    'Lorem ipsum dolor',
    'Lorem ipsum',
    'Lorem ipsum dolor sit amet'
  ];
  const actualOutput = await new Promise<string[]>(resolve => createRoot(dispose => {
    const [words, setWords] = createSignal(input.shift() ?? 3);
    const lorem = createLorem(words);

    const output: string[] = [];
    createEffect(() => {
      // effects are batched, so the escape condition needs
      // to run after the output is complete:
      if (input.length === 0) {
        dispose();
        resolve(output);
      }
      output.push(lorem());
      setWords(input.shift() ?? 0);
    });
  }));

  assert.equal(actualOutput, expectedOutput, 'output differs');
});

testLorem.run();

Testovací směrnice (use:... )

Dále chceme otestovat @solid-primitive/fullscreen primitivní, které funguje jako direktiva a odhaluje něco podobného jako následující API:

export type FullscreenDirective = (
  ref: HTMLElement,
  active: Accessor<boolean | FullscreenOptions>
) => void;

a používá se takto v Solid.js:

const [fs, setFs] = createSignal(false);
return <div use:FullscreenDirective={fs}>...</div>;

Můžete namítnout, že se chcete vyhnout detailům implementace, a proto vykreslit komponentu přesně jako ta výše, ale nemusíme vykreslovat nic, protože by to znamenalo, že bychom testovali podrobnosti implementace rozhraní direktivy Solid.js.

Takže se můžete podívat na test v solid-primitives úložiště.

Testování komponent

Nejprve musíme nainstalovat solid-testing-library . Bohužel nemůžeme použít @testing-library/jest-dom zde, ale hlavní rozšíření jest's expect jsou snadno replikovatelné.

npm i --save-dev solid-testing-library

Chceme otestovat následující jednoduchou komponentu:

import { createSignal, Component, JSX } from 'solid-js';

export const MyComponent: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => {
  const [clicked, setClicked] = createSignal(false);
  return <div {...props} role="button" onClick={() => setClicked(true)}>
    {clicked() ? 'Test this!' : 'Click me!'}
  </div>;
};

Náš test nyní vypadá takto:

import { suite } from 'uvu';
import * as assert from 'uvu/assert';
import { screen, render, fireEvent } from 'solid-testing-library';
import { MyComponent } from './my-component';

const isInDom = (node: Node): boolean => !!node.parentNode && 
  (node.parentNode === document || isInDom(node.parentNode));

const test = suite('MyComponent');

test('changes text on click', async () => {
  await render(() => <MyComponent />);
  const component = await screen.findByRole('button', { name: 'Click me!' });
  assert.ok(isInDom(component));
  fireEvent.click(component);
  assert.ok(isInDom(await screen.findByRole('button', { name: 'Test this!' })));
});

Více chybějících funkcí

V porovnání s jest , v uvu chybí ještě více funkcí a tape :

  • jednoduché zesměšňování/špionáž
  • časovač zesměšňuje
  • sbírka pokrytí kódu
  • režimu sledování
  • rozšiřitelná tvrzení
  • testování snímku

S uvu , mnoho z těchto funkcí lze přidat prostřednictvím externích pomocníků; některé jsou uvedeny v examples , např. coverage a watch a některé další tam nejsou zdokumentovány jako snoop přidat špiony.

Pro tape , existuje celá řada modulů.

Ale pamatujte:funkce, které nespouštíte, neztrácejí čas.

Ať vaše testy zachytí všechny chyby!

Ale jak jsem to udělal?

Kompilace kódu

Node má API, které nám umožňuje připojit se k načítání souborů require() 'd a zaregistrujte transpilační kód.

Máme opět tři možnosti, jak to udělat za nás:

  1. babel-register používá babel k transpilaci kódu; je rychlý, ale nepodporuje kontrolu typu
  2. ts-node používá ts-server k transpilaci kódu a poskytuje typovou bezpečnost na úkor kompilace
  3. S babelem můžeme uvést naše vlastní řešení, které nám umožňuje používat různé předvolby pro různé soubory

babel-register

Chcete-li používat babel-register, musíme nainstalovat

npm i --save-dev @babel/core @babel/register \
@babel/preset-env @babel/preset-typescript \
babel-preset-solid

Nyní jej musíme použít v našem compilation-babel.ts zkombinovat to s možnostmi potřebnými ke kompilaci našich pevných souborů:

require('@babel/register')({
  "presets": [
    "@babel/preset-env",
    "babel-preset-solid",
    "@babel/preset-typescript"
  ],
  extensions: ['.jsx', '.tsx', '.ts', '.mjs']
});

ts-node

Zatímco hlavním bodem tohoto balíčku je poskytnout interaktivní konzolu pro strojový skript, můžete ji také použít ke spuštění strojového skriptu přímo v uzlu. Můžeme to nainstalovat takto:

npm i --save-dev ts-jest babel-preset-solid @babel/preset-env

Po instalaci jej můžeme použít v našem compilation-ts-node.ts :

require('ts-node').register({ babelConfig: {
  presets: ['babel-preset-solid', '@babel/preset-env']
} });

Naše vlastní řešení

Proč bychom chtěli vlastní řešení? Oba babel-register a ts-jest pouze nám umožňují nastavit jedinou sadu předvoleb pro kompilaci modulů, což znamená, že některé předvolby mohou běžet zbytečně (např. kompilace strojopisu pro soubory .js). To nám také umožňuje pracovat se soubory, o které se tato řešení nestarají (viz bonusové kapitoly).

Jako přípravu vytváříme náš solid-register adresář a v něm spusťte naše repo a nainstalujte naše požadavky:

npm init
npm i --save-dev @babel/core @babel/preset-env \
@babel/preset-typescript babel-preset-solid \
typescript @types/node

Jak babel-register a ts-jest automaticky kompilovat importy? Používají (bohužel zastaralé a žalostně nedostatečně zdokumentované, ale stále funkční) require.extensions API, aby se vložili do procesu načítání modulu uzlu.

API je poměrně jednoduché:

// pseudo code to explain the API,
// it's a bit more complex in reality:
require.extensions[extension: string = '.js'] =
  (module: module, filename: string) => {
    const content = readFromCache(module)
      ?? fs.readFileSync(filename, 'UTF-8');
    module._compile(content, filename);
  };

Abychom to zjednodušili, vytvořili jsme vlastní src/register-extension.ts s následující metodou, kterou můžeme později znovu použít:

export const registerExtension = (
  extension: string | string[],
  compile: (code: string, filename: string) => string
) => {
  if (Array.isArray(extension)) {
    extension.forEach(ext => registerExtension(ext, compile));
  } else {
    const modLoad = require.extensions[extension] ?? require.extensions['.js'];
    require.extensions[extension] = (module: NodeJS.Module, filename: string) => {
      const mod = module as NodeJS.Module  & { _compile: (code) => void };
      const modCompile = mod._compile.bind(mod);
      mod._compile = (code) => modCompile(compile(code, filename));
      modLoad(mod, filename);
    }
  }
};

Nyní můžeme začít kompilovat náš pevný kód vytvořením souboru src/compile-solid.ts obsahující:

const { transformSync } = require('@babel/core');
const presetEnv = require('@babel/preset-env');
const presetSolid = require('babel-preset-solid');
const presetTypeScript = require('@babel/preset-typescript');

import { registerExtension } from "./register-extension";

registerExtension('.jsx', (code, filename) =>
  transformSync(code, { filename, presets: [presetEnv, presetSolid] }));

registerExtension('.ts', (code, filename) =>
  transformSync(code, { filename, presets: [presetEnv, presetTypeScript] }));

registerExtension('.tsx', (code, filename) =>
  transformSync(code, { filename, presets: [presetEnv, presetSolid, presetTypeScript] }));

Bonus #1:Aliasy názvu souboru

Pokud nechceme použít --conditions flag pro výběr verze prohlížeče, můžeme také použít aliasy pro určitá jména souborů, abychom přinutili uzel vybrat exporty prohlížeče ze solid. Za tímto účelem vytvoříme src/compile-aliases.ts;

const aliases = {
  'solid-js\/dist\/server': 'solid-js/dist/dev',
  'solid-js\/web\/dist\/server': 'solid-js/web/dist/dev'
  // add your own here
};
const alias_regexes = Object.keys(aliases)
  .reduce((regexes, match) => { 
    regexes[match] = new RegExp(match);
    return regexes;
  }, {});
const filenameAliasing = (filename) => 
  Object.entries(aliases).reduce(
    (name, [match, replace]) => 
      !name && alias_regexes[match].test(filename)
      ? filename.replace(alias_regexes[match], replace)
      : name,
    null) ?? filename;

const extensions = ['.js', '.jsx', '.ts', '.tsx'];

extensions.forEach(ext => {
  const loadMod = require.extensions[ext] ?? require.extensions['.js'];
  require.extensions[ext] = (module: NodeJS.Module, filename: string) => {
    loadMod(module, filenameAliasing(filename));
  };
});

Bonus č. 2:Zavaděč CSS

Když importujeme soubor „file.css“, obvykle říkáme našemu systému sestavení, aby načetl kód css do aktuální stránky pomocí svého interního zavaděče, a pokud se jedná o modul CSS, v importu uvedl názvy tříd.

Poskytnutím našeho vlastního zavaděče pro '.css' a '.module.css' , můžeme mít stejné zkušenosti v node a umožnit našemu DOM skutečně přistupovat ke stylům.

Takže napíšeme následující kód v našem vlastním src/compile-css.ts :

import { registerExtension } from "./register-extension";

const loadStyles = (filename: string, styles: string) =>
  `if (!document.querySelector(\`[data-filename="${filename}"]\`)) {
  const div = document.createElement('div');
  div.innerHTML = \`<style data-filename="${filename}">${styles}</style>\`;
  document.head.appendChild(div.firstChild);
  styles.replace(/@import (["'])(.*?)\1/g, (_, __, requiredFile) => {
    try {
      require(requiredFile);
    } catch(e) {
      console.warn(\`attempt to @import css \${requiredFile}\` failed); }
    }
  });
}`;

const toCamelCase = (name: string): string =>
  name.replace(/[-_]+(\w)/g, (_, char) => char.toUpperCase());

const getModuleClasses = (styles): Record<string, string> => {
  const identifiers: Record<string, string> = {};
  styles.replace(
    /(?:^|}[\r\n\s]*)(\.\w[\w-_]*)|@keyframes\s+([\{\s\r\n]+?)[\r\n\s]*\{/g,
    (_, classname, animation) => {
      if (classname) {
        identifiers[classname] = identifiers[toCamelCase(classname)] = classname;
      }
      if (animation) {
        identifiers[animation] = identifiers[toCamelCase(animation)] = animation;
      }
    }
  );
  return identifiers;
};

registerExtension('.css', (styles, filename) => loadStyles(filename, styles));
registerExtension('.module.css', (styles, filename) =>
  `${loadStyles(filename, styles)}
module.exports = ${JSON.stringify(getModuleClasses(styles))};`);

Bonus č. 3:načítání prostředků

Vite server z solidjs/templates/ts starter nám umožňuje získat cesty z importu aktiv. Nyní byste se měli vrtat a pravděpodobně byste mohli napsat src/compile-assets.ts vy sám:

import { registerExtension } from "./register-extension";

const assetExtensions = ['.svg', '.png', '.gif', '.jpg', '.jpeg'];

registerExtension(assetExtensions, (_, filename) => 
  `module.exports = "./assets/${filename.replace(/.*\//, '')}";`
);

K dispozici je také podpora pro ?raw cesty ve vite. Pokud chcete, můžete tuto část rozšířit, abyste je podpořili; aktuální verzi solid-register v době psaní tohoto článku pro něj ještě není podpora.

Prostředí DOM

Pokud jde o kompilaci, máme různé možnosti pro prostředí DOM:

  • jsdom, plně funkční, ale pomalý, výchozí možnost v jest
  • šťastný, lehčí
  • propojenost, nejrychlejší, ale postrádá základní funkce

Bohužel happy-dom není v současné době plně testován a linkedom opravdu nebude fungovat s solid-testing-library , takže jejich používání se v tuto chvíli nedoporučuje.

jsdom

Vzhledem k tomu, že jsdom je v podstatě určen k použití takto, registrace je jednoduchá:

import { JSDOM } from 'jsdom';

const { window } = new JSDOM(
  '<!doctype html><html><head></head><body></body></html>',
  { url: 'https://localhost:3000' }
);
Object.assign(globalThis, window);

happy-dom

import { Window } from 'happy-dom';

const window = new Window();
window.location.href = 'https://localhost:3000';

for (const key of Object.keys(window)) {
  if ((globalThis as any)[key] === undefined && key !== 'undefined') {
    (globalThis as any)[key] = (window as any)[key];
  }
}

linkedom

K vytvoření našeho prostředí DOM postačí následující:

// prerequisites
const parseHTML = require('linkedom').parseHTML;
const emptyHTML = `<!doctype html>
<html lang="en">
  <head><title></title></head>
  <body></body>
</html>`;

// create DOM
const {
    window,
    document,
    Node,
    HTMLElement,
    requestAnimationFrame,
    cancelAnimationFrame,
    navigator
} = parseHTML(emptyHTML);

// put DOM into global context
Object.assign(globalThis, {
    window,
    document,
    Node,
    HTMLElement,
    requestAnimationFrame,
    cancelAnimationFrame,
    navigator
});

Nakonec to všechno můžete spojit s nějakou funkcí čtení konfigurace, jako jsem to udělal já. Pokud někdy budete muset vytvořit podobný balíček pro svůj vlastní transpilovaný rámec, doufám, že na tento článek narazíte a pomůže vám.

Děkuji za trpělivost, doufám, že jsem to příliš nevyčerpal.