From 564e42a7adfb87a61e09955d03ef554724cea42d Mon Sep 17 00:00:00 2001 From: Nahuel Garbezza Date: Wed, 26 Oct 2022 20:24:36 -0300 Subject: [PATCH 1/2] :memo: resolve small typos and line breaks on ADRs --- doc/decisions/0003-zero-dependencies.md | 6 ++++-- doc/decisions/0004-console-ui-and-formatter.md | 6 ++++-- doc/decisions/0005-avoiding-test-doubles.md | 9 ++++++--- doc/decisions/0006-avoid-offensive-vocabulary.md | 7 +++++-- ...-mutation-testing-practices-and-quality-thresholds.md | 2 +- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/doc/decisions/0003-zero-dependencies.md b/doc/decisions/0003-zero-dependencies.md index 88da6ed1..8e283de7 100644 --- a/doc/decisions/0003-zero-dependencies.md +++ b/doc/decisions/0003-zero-dependencies.md @@ -12,9 +12,11 @@ Keeping the tool simple, minimal, easy to install and understand. ## Decision -Do not depend on any NPM package unless there's a strong reason to do that. Keep the zero dependence counter as much as possible. +Do not depend on any NPM package unless there's a strong reason to do that. Keep the zero dependence counter as much +as possible. ## Consequences -1. We will be "reimplementing" things, like file handling functions, or `isEmpty` or `isUndefined` type of queries, but that's fine. We want to keep control of that, and we can keep that in a single place, `Utils` (see decision 0002). +1. We will be "reimplementing" things, like file handling functions, or `isEmpty` or `isUndefined` type of queries, but +that's fine. We want to keep control of that, and we can keep that in a single place, `Utils` (see decision 0002). 2. If there's a security problem, we will be 100% sure our code is causing it. diff --git a/doc/decisions/0004-console-ui-and-formatter.md b/doc/decisions/0004-console-ui-and-formatter.md index 60996722..81b4bae5 100644 --- a/doc/decisions/0004-console-ui-and-formatter.md +++ b/doc/decisions/0004-console-ui-and-formatter.md @@ -12,10 +12,12 @@ Reducing complexity in the `ConsoleUI` object and make it more reusable and test ## Decision -`ConsoleUI` now only knows when to output things, but not the contents of messages, which is now responsibility of a `Formatter`. This object can be replaced by other formatters in the future. +`ConsoleUI` now only knows when to output things, but not the contents of messages, which is now responsibility of a +`Formatter`. This object can be replaced by other formatters in the future. ## Consequences * `Formatter` becomes testable, as well as `ConsoleUI` * Responsibilities are better split -* There are more control on Node built-in modules, `Formatter` now talks to `console` object, and `ConsoleUI` to `process` +* There are more control on Node built-in modules, `Formatter` now talks to `console` object, and `ConsoleUI` to +`process` diff --git a/doc/decisions/0005-avoiding-test-doubles.md b/doc/decisions/0005-avoiding-test-doubles.md index 94f03532..65aba8d4 100644 --- a/doc/decisions/0005-avoiding-test-doubles.md +++ b/doc/decisions/0005-avoiding-test-doubles.md @@ -8,14 +8,17 @@ Accepted ## Context -Tests against the real system instead of test doubles was proven to be useful detecting bugs and exercising better our system. +Tests against the real system instead of test doubles was proven to be useful detecting bugs and exercising better our +system. ## Decision -Avoid introducing test doubles as much as possible. The only allowed exception is to stub/simulate external systems we can't control. Library code should never be stubbed. +Avoid introducing test doubles as much as possible. The only allowed exception is to stub/simulate external systems we +can't control. Library code should never be stubbed. ## Consequences - Testing with all the real objects that are used in the tool. - No need to maintain test doubles and their protocol that should match the real objects' protocols. -- Potentially more setup code, objects could be hard to setup because there are no shortcuts. This can be solved by factory methods and/or test helpers. +- Potentially more setup code, objects could be hard to setup because there are no shortcuts. This can be solved by +factory methods and/or test helpers. diff --git a/doc/decisions/0006-avoid-offensive-vocabulary.md b/doc/decisions/0006-avoid-offensive-vocabulary.md index f2b20a2b..fe90f6d0 100644 --- a/doc/decisions/0006-avoid-offensive-vocabulary.md +++ b/doc/decisions/0006-avoid-offensive-vocabulary.md @@ -8,11 +8,14 @@ Accepted ## Context -There is an initiative about questioning the technical vocabulary we use, and avoiding some words that are widely used and may offend people. This is a project that adheres to this initiative, therefore... +There is an initiative about questioning the technical vocabulary we use, and avoiding some words that are widely used +and may offend people. This is a project that adheres to this initiative, therefore... ## Decision -Remove all existing technical vocabulary that might be offensive, and prevent those terms to be added in the future. For instance, the use of "master/slave" replaced by "main/replica" (or similar), or "whitelist/blacklist" by "safelist/blocklist" (or similar). +Remove all existing technical vocabulary that might be offensive, and prevent those terms to be added in the future. +For instance, the use of "master/slave" replaced by "main/replica" (or similar), or "whitelist/blacklist" by +"safelist/blocklist" (or similar). ## Consequences diff --git a/doc/decisions/0011-mutation-testing-practices-and-quality-thresholds.md b/doc/decisions/0011-mutation-testing-practices-and-quality-thresholds.md index fd3e878f..5fa377d3 100644 --- a/doc/decisions/0011-mutation-testing-practices-and-quality-thresholds.md +++ b/doc/decisions/0011-mutation-testing-practices-and-quality-thresholds.md @@ -1,4 +1,4 @@ -# 10. Use Mutation Testing and define quality thresholds +# 11. Use Mutation Testing and define quality thresholds Date: 2022-10-24 From 73df44808f62fce91a411955fed967eb4a1ea65d Mon Sep 17 00:00:00 2001 From: Nahuel Garbezza Date: Wed, 26 Oct 2022 20:38:39 -0300 Subject: [PATCH 2/2] :memo: add more JSDoc across the codebase --- lib/asserter.js | 86 ++++++++++++++++++++++++++++++++++++++++++- lib/assertion.js | 91 ++++++++++++++++++++++++++++------------------ lib/test.js | 5 +++ lib/test_runner.js | 4 ++ testy.js | 16 +++++--- 5 files changed, 158 insertions(+), 44 deletions(-) diff --git a/lib/asserter.js b/lib/asserter.js index 5cb48aee..17f7771f 100644 --- a/lib/asserter.js +++ b/lib/asserter.js @@ -8,12 +8,30 @@ const { I18nMessage } = require('./i18n'); const { isStringWithContent } = require('./utils'); class FailureGenerator extends TestResultReporter { + /** + * Makes the current test to explicitly fail, indicating an exceptional scenario. + * + * @example + * fail.with('The code should not reach this point') + * + * @param {String} description A reason to explain why we are explicitly failing. + * @returns {void} + */ with(description) { - this.report(TestResult.failure(description || I18nMessage.of('explicitly_failed'))); + const messageToReport = description || I18nMessage.of('explicitly_failed'); + this.report(TestResult.failure(messageToReport)); } } class PendingMarker extends TestResultReporter { + /** + * Indicates a test is not ready to be evaluated until the end to produce a final result, so it will be reported as a + * pending result ({@link TestResult#explicitlyMarkedAsPending}). If no reason is provided, an error result + * ({@link TestResult#error}) will be reported instead. + * + * @param {String} reason A required explanation to indicate why this test is not ready. + * @returns {void} + */ dueTo(reason) { if (isStringWithContent(reason)) { this.report(TestResult.explicitlyMarkedAsPending(reason)); @@ -21,21 +39,68 @@ class PendingMarker extends TestResultReporter { this.report(TestResult.error(this.invalidReasonErrorMessage())); } } - + invalidReasonErrorMessage() { return I18nMessage.of('invalid_pending_reason'); } } +/** + * I am the entry point for generating different types of assertions. + */ class Asserter extends TestResultReporter { + /** + * Starts an assertion. A call to this method needs to be chained with an expectation, otherwise it does not + * represent a valid assertion. + * + * @example using the {@link isEqualTo} assertion + * assert.that(3 + 4).isEqualTo(7) + * + * @example using the {@link isEmpty} assertion + * assert.that("").isEmpty() + * + * @example using the {@link isNearTo} assertion + * assert.that(0.1 + 0.2).isNearTo(0.3) + * + * @param {*} actual Ths object under test. + * @returns {Assertion} An object that you can use to build an assertion. + */ that(actual) { return new Assertion(this._runner, actual); } + /** + * Expects a given object to be strictly equal to `true`. Other "truthy" values according to Javascript rules + * will be considered not true. + * + * This is a shortcut of the {@link that} syntax followed by a {@link isTrue} assertion. + * + * @example + * assert.isTrue(3 < 4) + * + * @example equivalent version + * assert.that(3 < 4).isTrue() + * + * @param {*} actual - The object you expect to be `true`. + */ isTrue(actual) { return this.that(actual).isTrue(); } + /** + * Expects a given object to be strictly equal to `false`. Other "falsey" values according to Javascript rules + * will be considered not true. + * + * This is a shortcut of the {@link that} syntax followed by a {@link isFalse} assertion. + * + * @example + * assert.isFalse(4 < 3) + * + * @example equivalent version + * assert.that(4 < 3).isFalse() + * + * @param {*} actual - The object you expect to be `false`. + */ isFalse(actual) { return this.that(actual).isFalse(); } @@ -56,6 +121,23 @@ class Asserter extends TestResultReporter { return this.that(actual).isNotNull(); } + /** + * Expects two given objects to be equal according to a default or custom criteria. + * This is a shortcut of the {@link that} syntax followed by a {@link isEqualTo} assertion. + * + * @example + * assert.areEqual(3 + 4, 7) + * + * @example equivalent version + * assert.that('3' + '4').isEqualTo('34') + * + * @example custom criteria + * assert.areEqual([2, 3], ['x', 'y']], (a, b) => a.length === b.length) + * + * @param {*} actual - The object under test. + * @param {*} expected - The object that you are expecting the `actual` to be. + * @param {Function} [criteria] - A two-argument function to be used to compare `actual` and `expected`. Optional. + */ areEqual(actual, expected, criteria) { return this.that(actual).isEqualTo(expected, criteria); } diff --git a/lib/assertion.js b/lib/assertion.js index 66e6ea3c..944073ea 100644 --- a/lib/assertion.js +++ b/lib/assertion.js @@ -7,66 +7,85 @@ const { I18nMessage } = require('./i18n'); const { prettyPrint, isUndefined, isRegex, notNullOrUndefined, numberOfElements, convertToArray } = require('./utils'); +/** + * I represent an assertion we want to make on a specific object (called the `actual`), against an expectation, in the + * context of a {@link TestRunner}. + * + * I have multiple ways to write expectations, represented by my public instance methods. + * + * When the expectation is evaluated, it reports the results to the {@link TestRunner}. + */ class Assertion extends TestResultReporter { constructor(runner, actual) { super(runner); this._actual = actual; } - + // Boolean assertions - + + /** + * Expects the actual object to be strictly equal to `true`. Other "truthy" values according to Javascript rules + * will be considered not true. + * Another way of writing this assertion is to use the {@link Asserter.isTrue} method. + * + * @example + * assert.that(3 < 4).isTrue() + * + * @example equivalent version + * assert.isTrue(3 < 4) + */ isTrue() { this._reportAssertionResult( this._actual === true, () => I18nMessage.of('expectation_be_true', this._actualResultAsString()), ); } - + isFalse() { this._reportAssertionResult( this._actual === false, () => I18nMessage.of('expectation_be_false', this._actualResultAsString()), ); } - + // Undefined value assertions - + isUndefined() { this._reportAssertionResult( isUndefined(this._actual), () => I18nMessage.of('expectation_be_undefined', this._actualResultAsString()), ); } - + isNotUndefined() { this._reportAssertionResult( !isUndefined(this._actual), () => I18nMessage.of('expectation_be_defined', this._actualResultAsString()), ); } - + // Null value assertions - + isNull() { this._reportAssertionResult( this._actual === null, () => I18nMessage.of('expectation_be_null', this._actualResultAsString()), ); } - + isNotNull() { this._reportAssertionResult( this._actual !== null, () => I18nMessage.of('expectation_be_not_null', this._actualResultAsString()), ); } - + // Equality assertions - + isEqualTo(expected, criteria) { this._equalityAssertion(expected, criteria, true); } - + isNotEqualTo(expected, criteria) { this._equalityAssertion(expected, criteria, false); } @@ -80,58 +99,58 @@ class Assertion extends TestResultReporter { isNotIdenticalTo(expected) { this._identityAssertion(expected, false); } - + // Collection assertions - + includes(expectedObject, equalityCriteria) { const resultIsSuccessful = convertToArray(this._actual).find(element => this._areConsideredEqual(element, expectedObject, equalityCriteria)); const failureMessage = () => I18nMessage.of('expectation_include', this._actualResultAsString(), prettyPrint(expectedObject)); this._reportAssertionResult(resultIsSuccessful, failureMessage); } - + isIncludedIn(expectedCollection, equalityCriteria) { const resultIsSuccessful = expectedCollection.find(element => this._areConsideredEqual(element, this._actual, equalityCriteria)); const failureMessage = () => I18nMessage.of('expectation_be_included_in', this._actualResultAsString(), prettyPrint(expectedCollection)); this._reportAssertionResult(resultIsSuccessful, failureMessage); } - + doesNotInclude(expectedObject, equalityCriteria) { const resultIsSuccessful = !convertToArray(this._actual).find(element => this._areConsideredEqual(element, expectedObject, equalityCriteria)); const failureMessage = () => I18nMessage.of('expectation_not_include', this._actualResultAsString(), prettyPrint(expectedObject)); this._reportAssertionResult(resultIsSuccessful, failureMessage); } - + isNotIncludedIn(expectedCollection, equalityCriteria) { const resultIsSuccessful = !expectedCollection.find(element => this._areConsideredEqual(element, this._actual, equalityCriteria)); const failureMessage = () => I18nMessage.of('expectation_be_not_included_in', this._actualResultAsString(), prettyPrint(expectedCollection)); this._reportAssertionResult(resultIsSuccessful, failureMessage); } - + includesExactly(...objects) { const resultIsSuccessful = this._haveElementsConsideredEqual(this._actual, objects); const failureMessage = () => I18nMessage.of('expectation_include_exactly', this._actualResultAsString(), prettyPrint(objects)); this._reportAssertionResult(resultIsSuccessful, failureMessage); } - + isEmpty() { const resultIsSuccessful = numberOfElements(this._actual || {}) === 0 && notNullOrUndefined(this._actual); const failureMessage = () => I18nMessage.of('expectation_be_empty', this._actualResultAsString()); this._reportAssertionResult(resultIsSuccessful, failureMessage); } - + isNotEmpty() { const setValueWhenUndefined = this._actual || {}; const resultIsSuccessful = numberOfElements(setValueWhenUndefined) > 0; const failureMessage = () => I18nMessage.of('expectation_be_not_empty', this._actualResultAsString()); this._reportAssertionResult(resultIsSuccessful, failureMessage); } - + // Exception assertions - + raises(errorExpectation) { try { this._actual.call(); @@ -142,7 +161,7 @@ class Assertion extends TestResultReporter { this._reportAssertionResult(assertionPassed, errorMessage); } } - + doesNotRaise(notExpectedError) { try { this._actual.call(); @@ -153,7 +172,7 @@ class Assertion extends TestResultReporter { this._reportAssertionResult(!errorCheck, failureMessage); } } - + doesNotRaiseAnyErrors() { try { this._actual.call(); @@ -162,23 +181,23 @@ class Assertion extends TestResultReporter { this.reportFailure(I18nMessage.of('expectation_no_errors', prettyPrint(error))); } } - + // Numeric assertions - + isNearTo(number, precisionDigits = 4) { const result = Number.parseFloat((this._actual).toFixed(precisionDigits)) === number; const failureMessage = () => I18nMessage.of('expectation_be_near_to', this._actualResultAsString(), number.toString(), precisionDigits.toString()); this._reportAssertionResult(result, failureMessage); } - + // String assertions - + matches(regex) { const result = this._actual.match(regex); const failureMessage = () => I18nMessage.of('expectation_match_regex', this._actualResultAsString(), regex); this._reportAssertionResult(result, failureMessage); } - + // Private _identityAssertion(expected, shouldBeIdentical) { @@ -194,12 +213,12 @@ class Assertion extends TestResultReporter { this._reportAssertionResult(resultIsSuccessful, expectationMessage); } } - + _equalityAssertion(expected, criteria, shouldBeEqual) { const { comparisonResult, additionalFailureMessage, overrideFailureMessage } = EqualityAssertionStrategy.evaluate(this._actual, expected, criteria); const resultIsSuccessful = shouldBeEqual ? comparisonResult : !comparisonResult; - + if (isUndefined(comparisonResult)) { this._reportAssertionResult(false, overrideFailureMessage); } else { @@ -209,11 +228,11 @@ class Assertion extends TestResultReporter { this._reportAssertionResult(resultIsSuccessful, finalMessage); } } - + _areConsideredEqual(objectOne, objectTwo, equalityCriteria) { return EqualityAssertionStrategy.evaluate(objectOne, objectTwo, equalityCriteria).comparisonResult; } - + _checkIfErrorMatchesExpectation(errorExpectation, actualError) { if (isRegex(errorExpectation)) { return errorExpectation.test(actualError); @@ -221,7 +240,7 @@ class Assertion extends TestResultReporter { return this._areConsideredEqual(actualError, errorExpectation); } } - + _reportAssertionResult(wasSuccess, failureMessage) { if (wasSuccess) { this.reportSuccess(); @@ -229,11 +248,11 @@ class Assertion extends TestResultReporter { this.reportFailure(failureMessage.call()); } } - + _actualResultAsString() { return prettyPrint(this._actual); } - + _haveElementsConsideredEqual(collectionOne, collectionTwo) { const collectionOneArray = Array.from(collectionOne); const collectionTwoArray = Array.from(collectionTwo); diff --git a/lib/test.js b/lib/test.js index 389d4393..d2d5f821 100644 --- a/lib/test.js +++ b/lib/test.js @@ -3,6 +3,11 @@ const TestResult = require('./test_result'); const { isStringWithContent, isFunction, isUndefined } = require('./utils'); +/** + * I am an executable test, part of a {@link TestSuite} and executed by a {@link TestRunner}. + * After the execution, I know the result ({@link TestResult}). Tests must contain at least one assertion. + * See {@link Asserter} and {@link Assertion} for more details on how to write those. + */ class Test { constructor(name, body, callbacks) { this._initializeName(name); diff --git a/lib/test_runner.js b/lib/test_runner.js index cc975636..18bcebab 100644 --- a/lib/test_runner.js +++ b/lib/test_runner.js @@ -5,6 +5,10 @@ const TestSuite = require('./test_suite'); const FailFast = require('./fail_fast'); const { shuffle } = require('./utils'); +/** + * I am responsible for executing test suites ({@link TestSuite}), collecting the results of each test of each suite + * and report the aggregated results using a callback-based mechanism. + */ class TestRunner { constructor(callbacks) { this._suites = []; diff --git a/testy.js b/testy.js index 5bb8c3a7..2308c74e 100644 --- a/testy.js +++ b/testy.js @@ -12,7 +12,7 @@ const testRunner = new TestRunner(ui.testRunnerCallbacks()); /** * Object used for writing assertions. Assertions are created with method calls to this object. - * Please refer to the comment of each assertion for more information. + * Please refer to the comment of each assertion for more information ({@link Assertion}). * * @example * assert.isFalse(3 > 4) @@ -45,15 +45,19 @@ const fail = new FailureGenerator(testRunner); const pending = new PendingMarker(testRunner); /** - * Defines a new test. + * Defines a new test. Each test belongs to a test suite and defines assertions in the body. + * + * For info about assertions, take a look at the {@link assert} object. + * + * Tests are represented internally as instances of {@link Test}. * * @example * test('arithmetic works', () => { * assert.areEqual(3 + 4, 7); * }); * - * @param {String} name - How you would like to call the test. Non-empty string. - * @param {Function} testBody - The test definition, written as a zero-argument function. + * @param {String} name How you would like to call the test. Non-empty string. + * @param {Function} testBody The test definition, written as a zero-argument function. * @returns {void} */ function test(name, testBody) { @@ -71,8 +75,8 @@ function test(name, testBody) { * }); * }); * - * @param {String} name - How you would like to call the suite. Non-empty string. - * @param {Function} suiteBody - The suite definition, written as a zero-argument function. + * @param {String} name How you would like to call the suite. Non-empty string. + * @param {Function} suiteBody The suite definition, written as a zero-argument function. * @returns {void} */ function suite(name, suiteBody) {