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

[Feature ✨] Configuration params through console #320

Merged
merged 18 commits into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,16 @@ Testy will look for a `.testyrc.json` configuration file in the project root dir
"failFast": false, // enable/disable fail fast mode (stop as soon as a failed test appears)
"randomOrder": false // enable/disable execution of tests in random order
"timeoutMs": 1000 // sets the per-test timeout in milliseconds
"language": "en", // language of the output messages. "es", "it" and "en" supported for now
}
```

You can also pass a configuration through the console when running tests by adding these available options after your test file path:
- `-f` or `--fail-fast` to enable fail fast mode.
- `-r` or `--randomize` to enable the execution of tests in random order.
- `-l xx` or `--language xx` where `xx` must be either `es` for Spanish, `en` for English or `it` for Italian.
These console parameters can be sent in any order and combined as you want.

These are all the configuration parameters you can set. Feel free to change it according to your needs.
When declaring this configuration, every test suite under the `tests` directory (matching files ending with `*test.js`) will be executed.

Expand Down
19 changes: 15 additions & 4 deletions README_es.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ suite('una suite de tests aburrida', () => {
});
```

Una suite representa un agrupamiento de tests, y se define llamando a la función `suite(name, body)`, que toma como parámetro el nombre de este agrupamiendo y una función sin argumentos, que representa el contenido de la suite.
Una suite representa un agrupamiento de tests, y se define llamando a la función `suite(name, body)`, que toma como parámetro el nombre de este agrupamiento y una función sin argumentos, que representa el contenido de la suite.

Un test se escribe llamando a la función `test(name, body)`, que toma como parámetro el nombre del caso de test y una función sin parámetros que representa el cuerpo del test.

Expand Down Expand Up @@ -107,11 +107,22 @@ Testy se puede configurar a través de un archivo llamado `.testyrc.json` que de
"failFast": false, // habilita/deshabilita el modo "fail fast" (detener la ejecución en el primer fallo)
"randomOrder": false // habilita/deshabilita la ejecución de tests en orden aleatorio.
"timeoutMs": 1000 // asigna el tiempo límite de ejecución por cada test (en milisegundos)
"language": "en", // lenguaje que usara la consola. "es", "it" y "en" disponibles por el momento.
}
```

Puedes pasar parámetros de configuración a través de la consola agregando estas opciones después de las rutas de tests
que quieras ejecutar:
- `-f` o `--fail-fast` para habilitar el modo _fail fast_.
- `-r` o `--randomize` para habilitar la ejecución de tests en orden aleatorio.
- `-l xx` o `--language xx` done `xx` debe ser `es` para español, `en` para inglés o `it` para italiano.

Estos parámetros por consola pueden ser enviados en el orden que desees y puedes combinarlos como quieras. Toman
precedencia respecto a los que estén configurados en `.testyrc.json`.

Estos son todos los parámetros de configuración que existen, ajústalos de acuerdo a tus necesidades.
Siguiendo este ejemplo de configuración, lo que se va a ejecutar es cada suite de test dentro del directorio `tests`, cuyos nombres de archivos finalicen con `*test.js`.
Siguiendo este ejemplo de configuración, lo que se va a ejecutar es cada suite de test dentro del directorio `tests`,
cuyos nombres de archivos finalicen con `*test.js`.

### Ejemplos y aserciones disponibles

Expand Down Expand Up @@ -201,7 +212,7 @@ _suite_. `before()` y `after()` reciben una función como parámetro y pueden ut
```

* **Soporte para tests asíncronos**: si el código que estás testeando requiere de `async`, es posible hacer `await`
dentro de la definicion del test y luego escribir las aserciones. También es posible hacer llamados asincrónicos en
dentro de la definición del test y luego escribir las aserciones. También es posible hacer llamados asincrónicos en
`before()` y `after()`. Ejemplo:

```javascript
Expand All @@ -225,7 +236,7 @@ dentro de la definicion del test y luego escribir las aserciones. También es po
});
```
* **Modo "fail-fast"**: cuando está habilitado, se detiene apenas encuentra un test que falle o lance un error. Los tests restantes serán marcados como no ejecutados (_skipped_).
* **Ejecutar tests en orden aleatorio**: una buena suite de tests no depende de un orden particular de tests para ejecutarse correctamentee. Activando esta configuración es una buena forma de asegurar eso.
* **Ejecutar tests en orden aleatorio**: una buena suite de tests no depende de un orden particular de tests para ejecutarse correctamente. Activando esta configuración es una buena forma de asegurar eso.
* **Chequeo estricto de presencia de aserciones**: si un test no evalúa ninguna aserción durante su ejecución, el resultado se considera un error. Básicamente, un test que no tiene aserciones es un "mal" test.
* **Explícitamente, marcar un test como fallido o pendiente**: Ejemplos:

Expand Down
10 changes: 7 additions & 3 deletions doc/decisions/0002-place-utilities-in-utils-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@ Accepted

## Context

Having control of all the utilitary functions the tool might need.
Having control of all the utility functions the tool might need.

## Decision

Use the `Utils` module for any responsibility whose domain does not seem to be part of other objects' essence. Every time you feel you need a "helper" function, it is a good candidate for `Utils`. If you need to rely on `typeof`, it is also a good sign for needing `Utils`. We want to maximize reuse of these functions by having in a single, "static" place. We understand this might be a temporary solution.
Use the `Utils` module for any responsibility whose domain does not seem to be part of other objects' essence. Every
time you feel you need a "helper" function, it is a good candidate for `Utils`. If you need to rely on `typeof`, it is
also a good sign for needing `Utils`. We want to maximize reuse of these functions by having in a single, "static"
place. We understand this might be a temporary solution.

## Consequences

1. We will have a centralized place of all the logic that needs to be placed somewhere.
2. `Utils` module can potentially become a hotspot in the code, or a file that grows at a high pace. A subfolder with particular Utils might be needed at some point in the future.
2. `Utils` module can potentially become a hotspot in the code, or a file that grows at a high pace. A subfolder with
particular utils might be needed at some point in the future.
3. `Utils` is also a good place for interaction with external dependencies, although we want to keep zero dependencies.
6 changes: 3 additions & 3 deletions doc/decisions/0013-support-for-es-modules-only.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ that is only supported in Node, and not in other environments like Deno or the b

This article contains a detailed comparison of CommonJS and ES Modules: https://www.knowledgehut.com/blog/web-development/commonjs-vs-es-modules

This tool uses modules a lot, as it dynamically requires files written by the users of Testy, and the asyncronous
features of ES makes things easier and brings more reliability than synchronous CommonJS modules.
This tool uses modules a lot, as it dynamically requires files written by the users of Testy, and the asynchronous
features of ES makes things easier and brings more reliability than synchronous CommonJS modules.

## Decision

Expand All @@ -25,5 +25,5 @@ which is against the spirit of this project.
## Consequences

* This is a disruptive change for users. Projects that uses CommonJS will need to migrate to ES, at least to load the
tests.
tests.
* This will represent a breaking change, to be released in a new major version.
5 changes: 5 additions & 0 deletions lib/config/configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export class Configuration {
return new this(userConfiguration, defaultConfiguration);
}

static withConfiguration(aConfiguration) {
const defaultConfiguration = require('./default_configuration.json');
const userConfiguration = require(resolvePathFor(CONFIGURATION_FILE_NAME));
return new this({ ...userConfiguration, ...aConfiguration }, defaultConfiguration);
}
constructor(userConfiguration, defaultConfiguration) {
this.#userConfigurationOptions = userConfiguration;
this.#defaultConfigurationOptions = defaultConfiguration;
Expand Down
146 changes: 146 additions & 0 deletions lib/config/parameters_parser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { I18n } from '../i18n/i18n.js';
import { isString } from '../utils.js';

/**
* I transform a list of console parameters into a valid configuration object.
* I can also differentiate between path parameters and configuration parameters.
*/
beluamat29 marked this conversation as resolved.
Show resolved Hide resolved
export class ParametersParser {

static generateRunConfigurationFromParams(params) {
const sanitizedParams = this.sanitizeParameters(params);
return this.generateRunConfigurationFromSanitizedParams(sanitizedParams);
}

static generateRunConfigurationFromSanitizedParams(sanitizedParams) {
let generatedParams = {};
sanitizedParams.forEach(param => {
const runParameter = this.generateRunConfigurationParameter(param);
generatedParams = { ...generatedParams, ...runParameter };
});

return generatedParams;
}

static generateRunConfigurationParameter(param) {
const paramParser = [FailFastConfigurationParameterParser, RandomizeConfigurationParameterParser, LanguageConfigurationParameterParser, InvalidConfigurationParameter].find(configurationParameterParser => configurationParameterParser.canHandle(param));
return paramParser.handle(param);
}

static sanitizeParameters(params) {
const languageParamIndex = params.findIndex(param => param === '-l' || param === '--language');
if (languageParamIndex >= 0) {
return this.sanitizeLanguageParamOptions(params, languageParamIndex);
}
return params;
}

static sanitizeLanguageParamOptions(params, languageParamIndex) {
const languageOption = this.validateLanguageOption(params, languageParamIndex);
const languageConfig = [`-l ${languageOption}`];
this.removeLanguageParameters(params, languageParamIndex);
return [...params, ...languageConfig];
}

static validateLanguageOption(params, languageParamIndex) {
const languageOption = params[languageParamIndex + 1];
const supportedLanguages = I18n.supportedLanguages();
if (!supportedLanguages.includes(languageOption)) {
I18n.unsupportedLanguageException(languageOption, supportedLanguages);
}
return languageOption;
}

static removeLanguageParameters(params, languageParamIndex) {
params.splice(languageParamIndex, 2);
}

static getPathsAndConfigurationParams(allParams) {
const firstConfigParamIndex = allParams.findIndex(param => this.isRawConfigurationParam(param));

if (firstConfigParamIndex >= 0) {
const paramsAfterFirstConfigurationParam = allParams.slice(firstConfigParamIndex);
const thereIsPathParamAfterConfigParams = paramsAfterFirstConfigurationParam.some(param => !this.isRawConfigurationParam(param));
if (thereIsPathParamAfterConfigParams) {
throw new Error('Run configuration parameters should always be sent at the end of test paths routes');
}
return {
pathsParams: allParams.slice(0, firstConfigParamIndex),
configurationParams: allParams.slice(firstConfigParamIndex),
};
}

return {
pathsParams: allParams,
configurationParams: [],
};
}

static isConfigurationParam(param) {
return this.isFailFastParam(param) || this.isRandomizeParam(param) || this.isLanguageParam(param);
}

static isRawConfigurationParam(param) {
// pre sanitization
const supportedLanguages = I18n.supportedLanguages();
return this.isConfigurationParam(param) || supportedLanguages.includes(param);
}

static isFailFastParam(string) {
return isString(string) && (string === '-f' || string === '--fail-fast');
}

static isRandomizeParam(string) {
return isString(string) && (string === '-r' || string === '--randomize');
}

static isLanguageParam(param) {
const options = param.split(' ');
return isString(options[0]) && (options[0] === '-l' || options[0] === '--language');
};
}

class FailFastConfigurationParameterParser {

static canHandle(consoleParam) {
return ParametersParser.isFailFastParam(consoleParam);
}

static handle(_consoleParam) {
return { failFast: true };
}
}

class RandomizeConfigurationParameterParser {

static canHandle(consoleParam) {
return ParametersParser.isRandomizeParam(consoleParam);
}

static handle(_consoleParam) {
return { randomOrder: true };
}
}

class LanguageConfigurationParameterParser {

static canHandle(consoleParam) {
return ParametersParser.isLanguageParam(consoleParam);
}

static handle(consoleParam) {

const options = consoleParam.split(' ');
return { language: options[1] };
}
}

class InvalidConfigurationParameter {
static canHandle(_consoleParam) {
return true;
}

static handle(configurationParam) {
throw new Error(`Cannot parse invalid run configuration parameter ${configurationParam}. Please run --help option to check available options.`);
beluamat29 marked this conversation as resolved.
Show resolved Hide resolved
}
}
4 changes: 2 additions & 2 deletions lib/core/equality_assertion_strategy.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class EqualityAssertionStrategy {
return false;
}

evaluate(_actual, _expected, _criterie) {
evaluate(_actual, _expected, _criteria) {
return subclassResponsibility();
}
}
Expand All @@ -41,7 +41,7 @@ class BothPartsUndefined extends EqualityAssertionStrategy {
return isUndefined(actual) && isUndefined(expected);
}

evaluate(_actual, _expected, _criterie) {
evaluate(_actual, _expected, _criteria) {
return {
comparisonResult: undefined,
overrideFailureMessage: () => I18nMessage.of('equality_assertion_failed_due_to_undetermination'),
Expand Down
6 changes: 5 additions & 1 deletion lib/i18n/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ class I18n {
return `Translation key '${key}' not found in translations for language: ${languageCode}!`;
}

static unsupportedLanguageException(languageCode, supportedLanguages) {
throw new Error(`Language '${languageCode}' is not supported. Allowed values: ${supportedLanguages.join(', ')}`);
}

constructor(languageCode, translations = TRANSLATIONS) {
this.#assertLanguageIsSupported(languageCode, translations);
this.#languageCode = languageCode;
Expand All @@ -55,7 +59,7 @@ class I18n {
#assertLanguageIsSupported(languageCode, translations) {
const supportedLanguages = I18n.supportedLanguages(translations);
if (!supportedLanguages.includes(languageCode)) {
throw new Error(`Language '${languageCode}' is not supported. Allowed values: ${supportedLanguages.join(', ')}`);
I18n.unsupportedLanguageException(languageCode, supportedLanguages);
}
}

Expand Down
2 changes: 1 addition & 1 deletion lib/i18n/i18n_messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ class SingleI18nMessage extends I18nMessage {
}

toJson() {
return { key: this.#key, oarams: this.#params };
return { key: this.#key, params: this.#params };
}
}

Expand Down
27 changes: 19 additions & 8 deletions lib/script_action.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { InvalidConfigurationError } from './errors.js';

// Change to import assertions when reaching Node 18
import { createRequire } from 'module';
import { ParametersParser } from './config/parameters_parser.js';
const require = createRequire(import.meta.url);

export const ScriptAction = {
Expand Down Expand Up @@ -58,12 +59,20 @@ const ShowHelp = {

async execute(_params) {
const { description } = this._packageData();
console.log(`Testy: ${description}\n`);
console.log('Usage: \n');
console.log(' testy [-h --help] [-v --version] ...test files or folders...\n');
console.log('Options: \n');
console.log(' -h, --help Displays this text');
console.log(' -v, --version Displays the current version');
console.log(`
Testy: ${description}

Usage:

testy [-h --help] [-v --version] ...test files or folders...

Options:
-h, --help Displays this text
-v, --version Displays the current version
-f, --fail-fast Enables fail fast option for running tests
-r, --randomize Enables random order option for running tests
-l, --language xx Sets a language for running tests. Available options are 'es' for Spanish, 'en' for English and 'it' for Italian
`);
this._exitSuccessfully();
},
};
Expand All @@ -78,8 +87,10 @@ const RunTests = {

async execute(params) {
try {
const testyInstance = Testy.configuredWith(Configuration.current());
await testyInstance.run(params);
const { pathsParams, configurationParams } = ParametersParser.getPathsAndConfigurationParams(params);
const selectedConfiguration = ParametersParser.generateRunConfigurationFromParams(configurationParams);
const testyInstance = Testy.configuredWith(Configuration.withConfiguration(selectedConfiguration));
await testyInstance.run(pathsParams);
this._exitSuccessfully();
} catch (error) {
if (error instanceof InvalidConfigurationError) {
Expand Down
Loading
Loading