From 982a2c0bce665569f05cf82b8023119da12894bb Mon Sep 17 00:00:00 2001 From: Marin Bareta Date: Thu, 30 Jan 2025 10:10:30 +0100 Subject: [PATCH 1/5] Add code example --- examples/app.js | 35 +++++++++++++++++++ examples/unit-testing-examples.md | 57 +++++++++++++++++++++++++++++++ recipes/automated-testing.md | 4 ++- 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 examples/app.js create mode 100644 examples/unit-testing-examples.md diff --git a/examples/app.js b/examples/app.js new file mode 100644 index 0000000..d835675 --- /dev/null +++ b/examples/app.js @@ -0,0 +1,35 @@ +const http = require('http'); +const PasswordValidator = require('password-validator'); + +const PORT = 8000; +const MIN_LENGTH = 5; +const MAX_LENGTH = 255; + +function validatePassword(password) { + const passwordValidator = new PasswordValidator() + .is().min(MIN_LENGTH) + .is().max(MAX_LENGTH) + .has().uppercase() + .has().lowercase() + .has().digits(2) + .has().symbols(2); + const result = passwordValidator.validate(password, { details: true }); + return { isValid: result.length === 0, details: result }; +} + +async function handleRequest(req, res) { + console.log('Incoming request', new Date().toISOString(), req.url); + try { + const { isValid, details } = validatePassword(req.body?.password); + const statusCode = isValid ? 200 : 400; + res.writeHead(statusCode, { 'Content-Type': 'application/json' }); + if (statusCode === 400) res.write(JSON.stringify(details)); + } catch { + res.writeHead(500); + } + res.end(); +} + +http + .createServer(handleRequest) + .listen(PORT, () => console.log('App running on port', PORT)); diff --git a/examples/unit-testing-examples.md b/examples/unit-testing-examples.md new file mode 100644 index 0000000..7cc22f4 --- /dev/null +++ b/examples/unit-testing-examples.md @@ -0,0 +1,57 @@ +# Unit Testing Examples + +## Code we will be testing + +In this example, we will set up an API endpoint which checks if the password is +valid according to business logic rules. The rules are that password needs to be +between 5 and 255 characters with at least one uppercase letter, at least one +lowercase letter, at least 2 numbers and at least 2 special characters. + + +```javascript +const http = require('http'); +const PasswordValidator = require('password-validator'); + +const PORT = 8000; +const MIN_LENGTH = 5; +const MAX_LENGTH = 255; + +function validatePassword(password) { + const passwordValidator = new PasswordValidator() + .is().min(MIN_LENGTH) + .is().max(MAX_LENGTH) + .has().uppercase() + .has().lowercase() + .has().digits(2) + .has().symbols(2); + const result = passwordValidator.validate(password, { details: true }); + return { isValid: result.length === 0, details: result }; +} + +async function handleRequest(req, res) { + try { + const { isValid, details } = validatePassword(req.body?.password); + const statusCode = isValid ? 200 : 400; + res.writeHead(statusCode, { 'Content-Type': 'application/json' }); + if (statusCode === 400) res.write(JSON.stringify(details)); + } catch { + res.writeHead(500); + } + res.end(); +} + +http + .createServer(handleRequest) + .listen(PORT, () => console.log('App running on port', PORT)); +``` + +## Antipatterns + +### Antipattern 1 + +Unit testing handle request function. We don't want to unit test the `handleRequest` +function which handles routing and HTTP request/response. We can test that with API +and/or integration tests. + + + diff --git a/recipes/automated-testing.md b/recipes/automated-testing.md index a5c5086..8955458 100644 --- a/recipes/automated-testing.md +++ b/recipes/automated-testing.md @@ -88,6 +88,8 @@ changes, we can make minimal changes to the test suite and/or mocked data. - Mocking infrastructure parts such as database I/O - instead, revert the control by using the `AppService`, `Command` or `Query` to integrate unit implementing business logic with the infrastructure layer of the application - Monkey-patching dependencies used by the unit - instead, pass the dependencies through the constructor or method, so that you can pass the mocks or stubs in the test +[Test Examples](../examples/unit-testing-examples.md) + ### Integration Tests @@ -306,4 +308,4 @@ behavior. These are better suited for E2E testing. - Visual tests should complement, not replace other types of tests like E2E tests. Over-relying on them can leave functional gaps in coverage. - Blindly updating snapshots without investigating failures undermines the -purpose of visual testing and risks missing real issues. \ No newline at end of file +purpose of visual testing and risks missing real issues. From fd9934fe7bbf6c257bab833f7ff5a6174cc77eaf Mon Sep 17 00:00:00 2001 From: Marin Bareta Date: Thu, 30 Jan 2025 10:12:14 +0100 Subject: [PATCH 2/5] Remove app.js --- examples/.DS_Store | Bin 0 -> 6148 bytes examples/app.js | 35 ----------------------------------- 2 files changed, 35 deletions(-) create mode 100644 examples/.DS_Store delete mode 100644 examples/app.js diff --git a/examples/.DS_Store b/examples/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 console.log('App running on port', PORT)); From 1935a7ab0bd43224e97d7956b2180a37e69dc588 Mon Sep 17 00:00:00 2001 From: Marin Bareta Date: Thu, 30 Jan 2025 13:40:39 +0100 Subject: [PATCH 3/5] Add more examples --- examples/unit-testing-examples.md | 59 ++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/examples/unit-testing-examples.md b/examples/unit-testing-examples.md index 7cc22f4..958317a 100644 --- a/examples/unit-testing-examples.md +++ b/examples/unit-testing-examples.md @@ -13,7 +13,7 @@ const http = require('http'); const PasswordValidator = require('password-validator'); const PORT = 8000; -const MIN_LENGTH = 5; +const MIN_LENGTH = 10; const MAX_LENGTH = 255; function validatePassword(password) { @@ -45,6 +45,34 @@ http .listen(PORT, () => console.log('App running on port', PORT)); ``` +## Writing tests + +Let's write some tests for the `validatePassword` function to make sure it works +as described. + +```javascript +describe('validatePassword', function() { + describe('successfully validates password when it', function() { + it('is "Testing12!?"', function() { + const { isValid } = validatePassword('Testing12!?'); + expect(isValid).to.be(true); + }); + }); + + describe('fails to validate password when it', function() { + it('is not the correct length', function() { + const { isValid } = validatePassword('Test12!?'); + expect(isValid).to.be(false); + }); + + it('does not contain uppercase letter', function() { + const { isValid } = validatePassword('test12!?'); + expect(isValid).to.be(false); + }); + }); +}); +``` + ## Antipatterns ### Antipattern 1 @@ -53,5 +81,34 @@ Unit testing handle request function. We don't want to unit test the `handleRequ function which handles routing and HTTP request/response. We can test that with API and/or integration tests. +### Antipattern 2 + +We write single unit test for each scenario. While this example is overly simple +and maybe we don't need 20 unit tests to cover this function, it is a good idea +to split testing for each criteria. For example, one test to cover password min +length, one to cover uppercase letter, one to cover min number of required digits +etc. When tests are written like this, it is easier to pinpoint which criteria +caused the code to fail and how to fix it. + +```javascript +// WARNING: this is an example of an antipattern, do not write tests like this + +describe('validatePassword', function() { + describe('successfully validates password when it', function() { + it('is valid', function() { + const { isValid } = validatePassword('Testing12!?'); + expect(isValid).to.be(true); + }); + }); + + describe('fails to validate password when it', function() { + it('is invalid', function() { + const { isValid } = validatePassword(' '); + expect(isValid).to.be(false); + }); + }); +}); +``` + From d5d055d9a8dc959671e79a7311f85bc3e843c154 Mon Sep 17 00:00:00 2001 From: Marin Bareta Date: Thu, 30 Jan 2025 13:41:11 +0100 Subject: [PATCH 4/5] Remove unneeded file --- examples/.DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 examples/.DS_Store diff --git a/examples/.DS_Store b/examples/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 Date: Fri, 31 Jan 2025 09:30:22 +0100 Subject: [PATCH 5/5] Refactor tests and add sandboxed code playground --- examples/unit-testing-examples.md | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/examples/unit-testing-examples.md b/examples/unit-testing-examples.md index 958317a..5560a81 100644 --- a/examples/unit-testing-examples.md +++ b/examples/unit-testing-examples.md @@ -7,16 +7,17 @@ valid according to business logic rules. The rules are that password needs to be between 5 and 255 characters with at least one uppercase letter, at least one lowercase letter, at least 2 numbers and at least 2 special characters. +[Sandbox Code Examples 🔗](https://stackblitz.com/edit/node-jwphcjdk?file=unit-tests.js) ```javascript -const http = require('http'); -const PasswordValidator = require('password-validator'); +// password-validator.js + +import PasswordValidator from 'password-validator'; -const PORT = 8000; const MIN_LENGTH = 10; const MAX_LENGTH = 255; -function validatePassword(password) { +export function validatePassword(password) { const passwordValidator = new PasswordValidator() .is().min(MIN_LENGTH) .is().max(MAX_LENGTH) @@ -27,6 +28,14 @@ function validatePassword(password) { const result = passwordValidator.validate(password, { details: true }); return { isValid: result.length === 0, details: result }; } +``` + +```javascript +// app.js + +import http from 'http'; + +const PORT = 8000; async function handleRequest(req, res) { try { @@ -51,23 +60,27 @@ Let's write some tests for the `validatePassword` function to make sure it works as described. ```javascript +import { describe, mock, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { validatePassword } from './password-validator.js'; + describe('validatePassword', function() { describe('successfully validates password when it', function() { it('is "Testing12!?"', function() { const { isValid } = validatePassword('Testing12!?'); - expect(isValid).to.be(true); + assert.equal(isValid, true); }); }); describe('fails to validate password when it', function() { it('is not the correct length', function() { const { isValid } = validatePassword('Test12!?'); - expect(isValid).to.be(false); + assert.equal(isValid, false); }); it('does not contain uppercase letter', function() { const { isValid } = validatePassword('test12!?'); - expect(isValid).to.be(false); + assert.equal(isValid, false); }); }); }); @@ -97,14 +110,14 @@ describe('validatePassword', function() { describe('successfully validates password when it', function() { it('is valid', function() { const { isValid } = validatePassword('Testing12!?'); - expect(isValid).to.be(true); + assert.equal(isValid, true); }); }); describe('fails to validate password when it', function() { it('is invalid', function() { const { isValid } = validatePassword(' '); - expect(isValid).to.be(false); + assert.equal(isValid, false); }); }); });