├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── chapter_01-the_basics_of_unit_testing └── SimpleParser │ ├── InvalidOperationError.js │ ├── simpleParser.homemadetest.js │ └── simpleParser.js ├── chapter_02-a-first-unit-test ├── LogAn │ ├── ArgumentError.js │ ├── logAnalyzer.js │ └── logAnalyzer.test.js └── MemCalculator │ ├── memCalculator.js │ └── memCalculator.test.js ├── chapter_03-using-stubs-to-break-dependencies └── LogAn │ ├── ArgumentError.js │ ├── alwaysValidFakeExtensionManager.js │ ├── extensionManager.js │ ├── fakeExtensionManager.js │ ├── fileExtensionManager.js │ ├── fileNameExtensions.config.json │ ├── logAnalyzer.class.js │ ├── logAnalyzer.js │ ├── logAnalyzer.test.js │ └── testableLogAnalyzer.class.js ├── chapter_04-interaction-testing-using-mock-objects └── LogAn │ ├── errors │ └── ArgumentError.js │ ├── extensionManager.js │ ├── fakes │ ├── alwaysValidFakeExtensionManager.js │ ├── fakeEmailService.js │ ├── fakeExtensionManager.js │ ├── fakeThrowsErrorWebService.js │ └── fakeWebService.js │ ├── fileExtensionManager.js │ ├── fileNameExtensions.config.json │ ├── logAnalyzer.class.js │ ├── logAnalyzer.js │ ├── logAnalyzer.test.js │ ├── logAnalyzer2.js │ ├── logAnalyzer2.test.js │ ├── testableLogAnalyzer.class.js │ └── webService.js ├── chapter_05-isolation-frameworks ├── errors │ └── ArgumentError.js ├── fakes │ └── fakeWebService.js ├── logAnalyzer.js └── logAnalyzer.test.js ├── chapter_07-test-hierarchies-and-organization └── abstractTestInfrastructureClassPattern │ ├── configurationManager.js │ ├── configurationManager.test.js │ ├── logAnalyzer.js │ ├── logAnalyzer.test.js │ ├── loggingFacility.js │ └── testsUtils │ └── baseTests.js ├── loggerWebService ├── index.js └── server.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alberto Delgado Roda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | The Art of Unit Testing, Second Edition 4 | 5 |

6 |

7 | The Art of Unit Testing, Second Edition
8 |

9 | 10 | The Art of Unit Testing is a book written by [Roy Osherove](http://osherove.com/) 11 | 12 | The book code examples are written in C# and the tools that appears in it are from the .NET community. 13 | 14 | ## Goal of this repository 15 | 16 |

17 | 18 | Node.js 23 | 24 |

25 | 26 | I would like that people that works using Node.js could enjoy of the knowledge that this book offers to its readers. Therefore I going to use Node.js to write the code examples and I going to use tools typically related with it. :heart_eyes: 27 | 28 | The style of the code and the chosen tools are 100% my decision. (The good and the bad parts! :kissing_closed_eyes:) 29 | 30 | ## How study the repository 31 | 32 | 1. For every chapter of the book has I created a directory 33 | where appears the final version of the code in the mentioned chapter. 34 | 35 | 2. Every commit has a reference to the chapter related. Any change I needed to do has a commit, with the objective of follow the flow of the book. 36 | 37 | **Note**: If you want to open the links in another tab, just do a CTRL+click on the link. 38 | 39 | #### Chapters 40 | 41 | 1. _The basics of unit testing_ 42 | 43 |
git commits done during the chapter :shipit: 44 |

45 | - Initial commit
46 | - In order to commit formatted code I installed prettier, pretty-quick and husky
47 | - update README
48 | - preparing the simpleParser example, creating InvalidOperation custom error
49 | - creating simpleParser example
50 | - creating a test manually to do basic tests to simpleParser, I'm not using unit testing frameworks, yet 51 |

52 |
53 | 54 | 2. _A first unit test_ 55 | 56 |
git commits done during the chapter :shipit: 57 |

58 | - starting LogAn, the project that we are going to use in the next chapters
59 | - install jest
60 | - renaming homemade test of the chapter 01 to avoid conflicts with jest
61 | - creating logAnalyzer and its test, that show us that the SUT have a bug
62 | - fix isValidLogFileName in order to fix the test
63 | - adding two more tests, one of them intetionally fails
64 | - fix in isValidLogFilename to fix the test
65 | - refactoring the code of thest using the parameterized tests technique
66 | - adding setup to the test
67 | - returns error if the filename is empty
68 | - add test that assert that the error is thrown
69 | - adding state logAnalyzer
70 | - first state-based test for logAnalyzer
71 | - add a inmemory calculator to continuing trying state-based testing
72 | - add a memory calculator in order to test it with state-based testing
73 | - update readme
74 | - remove .vscode from repository
75 |

76 |
77 | 78 | 3. _Using stubs to break dependencies_ 79 | 80 |
git commits done during the chapter :shipit: 81 |

82 | - copy code from chapter 02 to the chapter 03 folder in order to continue with the book
83 | - install @types/node, with that, I can work with types and node.js modules without vscode warnings
84 | - check the validity of the file extension in a disk file, that creates an external dependency, unit tests are broken
85 | - fixing logAnalyzer tests, right now they are integration tests and not unit tests, meh
86 | - extracting a factory function that touches the filesystem and calling it
87 | - creating a fake extension manager, the name of a fake is because we can use it as a stub or a mock, depending of the test.
88 | - create a new fake that is ready to be configurable to use in test
89 | - fixing alwaysValidFakeExtensionManager, I didn't create the function return
90 | - I created a seam in logAnalyzer, that seam enable the possibility of inject the dependency while are calling the function
91 | - using the fakeManagerExtension to fix the failed test from the previous commit
92 | - creating a extension manager factory that allows to set the extension manager to return before execute it, the default manager it returns is fileExtensionManager
93 | - using extensionManagerFactory I created an integration test, because the test is making use of a external dependency, the filesystem
94 | - using extensionManagerFactory I created an unit test, because the test is making use of a fake extension manager, therefore I'm not using the filesystem that is an external dependency
95 | - I have changed the isValid method from the fakes to return a promise instead of a boolean
96 | - In order to use the technique Extract and Override I needed to create a new file using logAnalyzer as a class and create a virtual method
97 | - testing the the new class using the technique Extract and Override
98 |

99 |
100 | 101 | 4. _Interaction testing using mock objects_ 102 | 103 |
git commits done during the chapter :shipit: 104 |

105 | - copy code from chapter 03 to the chapter 04 folder in order to continue with the book
106 | - organizing a bit more the files to improve the 'first glance' effect of LogAn folder
107 | - create a new object that fake a call to a web service
108 | - create a basic webservice in order to create an example that I want to create from the book
109 | - adding a parameter that allows to pass an object webService to logAnalyzer
110 | - creating the real webService connector
111 | - fixing fakeWebService, create getter and return both object with the two functions
112 | - create a unit test that use fakeWebService as a mock
113 | - adding another fake named emailService, and create a new test where the webService is used as stub and the emailService as a mock
114 |

115 |
116 | 117 | 5. _Isolation (mocking) frameworks_ 118 | 119 |
git commits done during the chapter :shipit: 120 |

121 | - copy logAnalyzer.js, errors and fakes from the folder of chapter 04 in order to continue with the book
122 | - creating a test that use a fake handwritten
123 | - instead of use a handwriting fake I create it using jest mocking module!
124 | - check that the logError method is called with the expected error message using jest mocking system functions
125 |

126 |
127 | 128 | 6. _Digging deeper into isolation frameworks_ 129 | 130 |
git commits done during the chapter :shipit: 131 |

132 | No commits here, this chapter go deep in the explanation about isolation frameworks, interesting concepts! :grin: 133 |

134 |
135 | 136 | 7. _test hierarchies and organization_ 137 | 138 |
git commits done during the chapter :shipit: 139 |

140 | - initial code to create a test api for the application, the name of the first strategy is abstract test infrastructure class pattern
141 | - refactored solution based in abstract test infrastructure class pattern
142 | 143 | - Also Wonderful explanations about concepts as: 144 | 145 | 1. Focus in the importance of separation of unit tests and integration tests. 146 | 2. Separate them by speed: slows, fasts, etc 147 | 3. Create nightly builds, CI builds, etc 148 | 4. More and more :grin: if you want to know all, [buy the book](https://www.amazon.com/Art-Unit-Testing-examples/dp/1617290890) 149 | 150 |

151 |
152 | 153 | 8. _The pillars of good unit tests_ 154 | 155 |
git commits done during the chapter :shipit: 156 |

157 | - No commits here, in this chapter Roy speaks deeply about: 158 | 159 | 1. Writing trustworthy tests 160 | 2. Writing maintainable tests 161 | 3. Writing readable tests 162 | 4. Exploring naming conventions for unit tests. 163 | 164 |
165 | Thanks to the content of this chapter I have said to myself many times "aha", this chapter by itself triggers a message,and the message is: "buy the book!" :grin: I enjoyed a lot this chapter. 166 | 167 |

168 |
169 | 170 | 9. _Integration unit testing into the organization_ 171 | 172 |
git commits done during the chapter :shipit: 173 |

174 | - No commits here, wonderful chapter!, this book is a drug for me(a good drug!) :smile: 175 |

176 |
177 | 178 | 10. _Working with legacy code_ 179 | 180 | 181 |
git commits done during the chapter :shipit: 182 |

183 | - No commits here, several amazing concepts! :smile: :smile: 184 |

185 |
186 | 187 | 11. _Design and testability_ 188 | 189 | 190 |
git commits done during the chapter :shipit: 191 |

192 | - No commits here. I finished the read and study of the book, to read this book should be a must in my opinion 193 |

194 |
195 | 196 | :grin: :grin: :grin: 197 | 198 | ## How to use 199 | 200 | ### Setup 201 | 202 | Install all the dependencies: 203 | 204 | ```bash 205 | npm install 206 | ``` 207 | 208 | If you want to execute the all tests I created: 209 | 210 | ```bash 211 | npm run test 212 | ``` 213 | 214 | ## Final thoughts 215 | 216 | I hope you enjoy the repository as much I while I was writing it :smiley: 217 | 218 | I strongly encourage that you should [buy the book](https://www.amazon.com/Art-Unit-Testing-examples/dp/1617290890), it is a masterpiece. 219 | -------------------------------------------------------------------------------- /chapter_01-the_basics_of_unit_testing/SimpleParser/InvalidOperationError.js: -------------------------------------------------------------------------------- 1 | class InvalidOperationException extends Error {} 2 | 3 | module.exports = InvalidOperationException; 4 | -------------------------------------------------------------------------------- /chapter_01-the_basics_of_unit_testing/SimpleParser/simpleParser.homemadetest.js: -------------------------------------------------------------------------------- 1 | const simpleParser = require('./simpleParser'); 2 | 3 | function returnsZeroWhenEmptyString() { 4 | try { 5 | const p = simpleParser(); 6 | const result = p.parseAndSum(''); 7 | if (result !== 0) { 8 | console.error(`***** returnsZeroWhenEmptyString *****: 9 | ------- 10 | Parse and sum should have returned 0 on an empty string 11 | `); 12 | } 13 | } catch (e) { 14 | console.error(e); 15 | } 16 | } 17 | 18 | function returnsNumberWhenSendOneNumber() { 19 | try { 20 | const p = simpleParser(); 21 | const result = p.parseAndSum('1'); 22 | if (Number.isNaN(result)) { 23 | console.error(`***** returnsNumberWhenSendOneNumber *****: 24 | ------- 25 | Parse and sum should have returned a Number 26 | `); 27 | } 28 | } catch (e) { 29 | console.error(e); 30 | } 31 | } 32 | 33 | returnsZeroWhenEmptyString(); 34 | returnsNumberWhenSendOneNumber(); 35 | -------------------------------------------------------------------------------- /chapter_01-the_basics_of_unit_testing/SimpleParser/simpleParser.js: -------------------------------------------------------------------------------- 1 | const InvalidOperationError = require('./InvalidOperationError'); 2 | 3 | function simpleParser() { 4 | /** 5 | * @param {string} numbers 6 | * @return {number} 7 | */ 8 | function parseAndSum(numbers) { 9 | if (numbers.length === 0) { 10 | return 0; 11 | } 12 | if (!numbers.includes(',')) { 13 | return Number(numbers); 14 | } else { 15 | throw new InvalidOperationError( 16 | 'I can only handle 0 or 1 numbers for now!' 17 | ); 18 | } 19 | } 20 | 21 | return { 22 | parseAndSum, 23 | }; 24 | } 25 | 26 | module.exports = simpleParser; 27 | -------------------------------------------------------------------------------- /chapter_02-a-first-unit-test/LogAn/ArgumentError.js: -------------------------------------------------------------------------------- 1 | class ArgumentError extends Error {} 2 | 3 | module.exports = ArgumentError; 4 | -------------------------------------------------------------------------------- /chapter_02-a-first-unit-test/LogAn/logAnalyzer.js: -------------------------------------------------------------------------------- 1 | const ArgumentError = require('./ArgumentError'); 2 | 3 | function logAnalyzer() { 4 | /** 5 | * @type {boolean} 6 | */ 7 | let wasLastFileNameValid; 8 | 9 | /** 10 | * @return {boolean} 11 | */ 12 | function getWasLastFileNameValid() { 13 | return wasLastFileNameValid; 14 | } 15 | 16 | /** 17 | * @param {string} fileName 18 | * @return {boolean} 19 | */ 20 | function isValidLogFileName(fileName) { 21 | wasLastFileNameValid = false; 22 | 23 | if (fileName === '') { 24 | throw new ArgumentError('filename has to be provided'); 25 | } 26 | 27 | if (!fileName.toUpperCase().endsWith('.SLF')) { 28 | return false; 29 | } 30 | 31 | wasLastFileNameValid = true; 32 | return true; 33 | } 34 | 35 | return { 36 | getWasLastFileNameValid, 37 | isValidLogFileName, 38 | }; 39 | } 40 | 41 | module.exports = logAnalyzer; 42 | -------------------------------------------------------------------------------- /chapter_02-a-first-unit-test/LogAn/logAnalyzer.test.js: -------------------------------------------------------------------------------- 1 | const logAnalyzer = require('./logAnalyzer'); 2 | 3 | let logAnalyzerInstance; 4 | beforeEach(() => { 5 | logAnalyzerInstance = logAnalyzer(); 6 | }); 7 | 8 | describe.each([ 9 | ['johndoe.js', false], 10 | ['johndoe.slf', true], 11 | ['johndoe.SLF', true], 12 | ])('isValidLogFileName("%s"))', (fileName, expected) => { 13 | it(`bad extension returns ${expected}`, () => { 14 | const result = logAnalyzerInstance.isValidLogFileName(fileName); 15 | expect(result).toBe(expected); 16 | }); 17 | }); 18 | 19 | describe('isValidLogFileName', () => { 20 | it('empty filename throws error', () => { 21 | function emptyLogFileName() { 22 | logAnalyzerInstance.isValidLogFileName(''); 23 | } 24 | 25 | expect(emptyLogFileName).toThrow('filename has to be provided'); 26 | }); 27 | 28 | /** 29 | * an example of state-based testing 30 | */ 31 | it.each` 32 | fileName | expected 33 | ${'johndoe.foo'} | ${false} 34 | ${'johndoe.slf'} | ${true} 35 | `( 36 | 'when called there changes wasLastFileNameValid that returns $expected', 37 | ({ fileName, expected }) => { 38 | logAnalyzerInstance.isValidLogFileName(fileName); 39 | const result = logAnalyzerInstance.getWasLastFileNameValid(); 40 | 41 | expect(result).toBe(expected); 42 | } 43 | ); 44 | }); 45 | -------------------------------------------------------------------------------- /chapter_02-a-first-unit-test/MemCalculator/memCalculator.js: -------------------------------------------------------------------------------- 1 | function memCalculator() { 2 | /** 3 | * @type {number} 4 | */ 5 | let sum = 0; 6 | 7 | /** 8 | * @param {number} number 9 | */ 10 | function add(number) { 11 | sum += number; 12 | } 13 | 14 | /** 15 | * @return {number} 16 | */ 17 | function result() { 18 | /** 19 | * @type {number}; 20 | */ 21 | const temp = sum; 22 | sum = 0; 23 | 24 | return temp; 25 | } 26 | 27 | return { 28 | add, 29 | result, 30 | }; 31 | } 32 | 33 | module.exports = memCalculator; 34 | -------------------------------------------------------------------------------- /chapter_02-a-first-unit-test/MemCalculator/memCalculator.test.js: -------------------------------------------------------------------------------- 1 | const memCalculator = require('./memCalculator'); 2 | 3 | let memCalculatorInstance; 4 | beforeEach(() => { 5 | memCalculatorInstance = memCalculator(); 6 | }); 7 | 8 | describe('result', () => { 9 | it('by default returns zero', () => { 10 | const lastResult = memCalculatorInstance.result(); 11 | expect(lastResult).toBe(0); 12 | }); 13 | 14 | it('changes when call add', () => { 15 | const expectedResult = 12; 16 | 17 | memCalculatorInstance.add(5); 18 | memCalculatorInstance.add(7); 19 | 20 | const lastResult = memCalculatorInstance.result(); 21 | 22 | expect(lastResult).toBe(expectedResult); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /chapter_03-using-stubs-to-break-dependencies/LogAn/ArgumentError.js: -------------------------------------------------------------------------------- 1 | class ArgumentError extends Error {} 2 | 3 | module.exports = ArgumentError; 4 | -------------------------------------------------------------------------------- /chapter_03-using-stubs-to-break-dependencies/LogAn/alwaysValidFakeExtensionManager.js: -------------------------------------------------------------------------------- 1 | function alwaysValidFakeExtensionManager() { 2 | /** 3 | * @param {string} fileName 4 | * @return {Promise} 5 | */ 6 | async function isValid(fileName) { 7 | return true; 8 | } 9 | 10 | return { 11 | isValid, 12 | }; 13 | } 14 | 15 | module.exports = alwaysValidFakeExtensionManager; 16 | -------------------------------------------------------------------------------- /chapter_03-using-stubs-to-break-dependencies/LogAn/extensionManager.js: -------------------------------------------------------------------------------- 1 | const fileExtensionManagerFactory = require('./fileExtensionManager'); 2 | 3 | function extensionManager() { 4 | let customManager = null; 5 | 6 | function create() { 7 | if (customManager !== null) { 8 | return customManager; 9 | } 10 | 11 | return fileExtensionManagerFactory(); 12 | } 13 | 14 | function setManager(manager) { 15 | customManager = manager; 16 | } 17 | 18 | return { 19 | create, 20 | setManager, 21 | }; 22 | } 23 | 24 | module.exports = extensionManager; 25 | -------------------------------------------------------------------------------- /chapter_03-using-stubs-to-break-dependencies/LogAn/fakeExtensionManager.js: -------------------------------------------------------------------------------- 1 | function fakeExtensionManager() { 2 | /** 3 | * @type {boolean} 4 | */ 5 | let valid = false; 6 | 7 | /** 8 | * @param {boolean} value 9 | */ 10 | function willBeValid(value) { 11 | valid = value; 12 | } 13 | 14 | /** 15 | * @param {string} fileName 16 | */ 17 | async function isValid(fileName) { 18 | return valid; 19 | } 20 | 21 | return { 22 | willBeValid, 23 | isValid, 24 | }; 25 | } 26 | module.exports = fakeExtensionManager; 27 | -------------------------------------------------------------------------------- /chapter_03-using-stubs-to-break-dependencies/LogAn/fileExtensionManager.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const util = require('util'); 3 | 4 | const readFilePromisied = util.promisify(fs.readFile); 5 | 6 | function fileExtensionManager() { 7 | /** 8 | * @param {string} fileName 9 | * @return {Promise} 10 | */ 11 | async function isValid(fileName) { 12 | const fileNameExtensions = await readFilePromisied( 13 | `${__dirname}/fileNameExtensions.config.json`, 14 | 'utf8' 15 | ).then(fileContent => JSON.parse(fileContent).extensions); 16 | 17 | const isValidExtension = fileNameExtensions.some( 18 | function checkFileNameExtension(extension) { 19 | if ( 20 | fileName 21 | .toUpperCase() 22 | .endsWith(`.${extension.toUpperCase()}`) 23 | ) { 24 | return true; 25 | } 26 | 27 | return false; 28 | } 29 | ); 30 | 31 | return isValidExtension; 32 | } 33 | 34 | return { 35 | isValid, 36 | }; 37 | } 38 | 39 | module.exports = fileExtensionManager; 40 | -------------------------------------------------------------------------------- /chapter_03-using-stubs-to-break-dependencies/LogAn/fileNameExtensions.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensions": ["slf", "sql"] 3 | } 4 | -------------------------------------------------------------------------------- /chapter_03-using-stubs-to-break-dependencies/LogAn/logAnalyzer.class.js: -------------------------------------------------------------------------------- 1 | const ArgumentError = require('./ArgumentError'); 2 | const fileExtensionManagerFactory = require('./fileExtensionManager'); 3 | 4 | class LogAnalyzer { 5 | constructor() { 6 | /** 7 | * @type {boolean} 8 | */ 9 | this.wasLastFileNameValid = false; 10 | } 11 | /** 12 | * @return {boolean} 13 | */ 14 | getWasLastFileNameValid() { 15 | return this.wasLastFileNameValid; 16 | } 17 | 18 | /** 19 | * a virtual function created to use "Extract and Override" technique 20 | */ 21 | getManager() { 22 | return fileExtensionManagerFactory(); 23 | } 24 | 25 | /** 26 | * @param {string} fileName 27 | * @return {Promise} 28 | */ 29 | async isValidLogFileName(fileName) { 30 | this.wasLastFileNameValid = false; 31 | 32 | if (fileName === '') { 33 | throw new ArgumentError('filename has to be provided'); 34 | } 35 | 36 | const result = await this.getManager().isValid(fileName); 37 | 38 | if (!result) { 39 | return false; 40 | } 41 | 42 | this.wasLastFileNameValid = true; 43 | return true; 44 | } 45 | } 46 | 47 | module.exports = LogAnalyzer; 48 | -------------------------------------------------------------------------------- /chapter_03-using-stubs-to-break-dependencies/LogAn/logAnalyzer.js: -------------------------------------------------------------------------------- 1 | const ArgumentError = require('./ArgumentError'); 2 | 3 | function logAnalyzer(extensionManager) { 4 | /** 5 | * @type {boolean} 6 | */ 7 | let wasLastFileNameValid; 8 | 9 | /** 10 | * @return {boolean} 11 | */ 12 | function getWasLastFileNameValid() { 13 | return wasLastFileNameValid; 14 | } 15 | 16 | /** 17 | * @param {string} fileName 18 | * @return {Promise} 19 | */ 20 | async function isValidLogFileName(fileName) { 21 | wasLastFileNameValid = false; 22 | 23 | if (fileName === '') { 24 | throw new ArgumentError('filename has to be provided'); 25 | } 26 | 27 | const result = await extensionManager.isValid(fileName); 28 | 29 | if (!result) { 30 | return false; 31 | } 32 | 33 | wasLastFileNameValid = true; 34 | return true; 35 | } 36 | 37 | return { 38 | getWasLastFileNameValid, 39 | isValidLogFileName, 40 | }; 41 | } 42 | 43 | module.exports = logAnalyzer; 44 | -------------------------------------------------------------------------------- /chapter_03-using-stubs-to-break-dependencies/LogAn/logAnalyzer.test.js: -------------------------------------------------------------------------------- 1 | const logAnalyzerFactory = require('./logAnalyzer'); 2 | const fakeExtensionManagerFactory = require('./fakeExtensionManager'); 3 | const extensionManagerFactory = require('./extensionManager'); 4 | 5 | // imported to try the technique "Extract and override" 6 | const TestableLogAnalyzerClass = require('./testableLogAnalyzer.class'); 7 | 8 | let myFakeExtensionManager; 9 | 10 | beforeEach(() => { 11 | myFakeExtensionManager = fakeExtensionManagerFactory(); 12 | }); 13 | 14 | describe.each([ 15 | ['johndoe.js', false], 16 | ['johndoe.slf', true], 17 | ['johndoe.SLF', true], 18 | ])('isValidLogFileName("%s"))', (fileName, expected) => { 19 | it(`bad extension returns ${expected}`, async () => { 20 | myFakeExtensionManager.willBeValid(expected); 21 | 22 | const logAnalyzer = logAnalyzerFactory(myFakeExtensionManager); 23 | const result = await logAnalyzer.isValidLogFileName(fileName); 24 | expect(result).toBe(expected); 25 | }); 26 | }); 27 | 28 | describe('isValidLogFileName', () => { 29 | it('empty filename throws error', async () => { 30 | async function emptyLogFileName() { 31 | const logAnalyzer = logAnalyzerFactory(myFakeExtensionManager); 32 | 33 | return logAnalyzer.isValidLogFileName(''); 34 | } 35 | 36 | await expect(emptyLogFileName()).rejects.toThrow( 37 | 'filename has to be provided' 38 | ); 39 | }); 40 | 41 | /** 42 | * an example of state-based testing 43 | */ 44 | it.each` 45 | fileName | expected 46 | ${'johndoe.foo'} | ${false} 47 | ${'johndoe.slf'} | ${true} 48 | `( 49 | 'when called there changes wasLastFileNameValid that returns $expected', 50 | async ({ fileName, expected }) => { 51 | myFakeExtensionManager.willBeValid(expected); 52 | 53 | const logAnalyzer = logAnalyzerFactory(myFakeExtensionManager); 54 | await logAnalyzer.isValidLogFileName(fileName); 55 | const result = logAnalyzer.getWasLastFileNameValid(); 56 | 57 | expect(result).toBe(expected); 58 | } 59 | ); 60 | 61 | /** 62 | * an example of use of "injecting a fake just before a method call" 63 | * right now I'm not injecting a fake, and extensionManager is returning fileExtensionManager, 64 | * therefore this test is an integration test and not a unit test!!!! 65 | */ 66 | it('return true using extensionManagerFactory', async () => { 67 | const extensionManager = extensionManagerFactory(); 68 | const logAnalyzer = logAnalyzerFactory(extensionManager.create()); 69 | const result = await logAnalyzer.isValidLogFileName('johndoe.sql'); 70 | 71 | expect(result).toBe(true); 72 | }); 73 | 74 | /** 75 | * an example of use of "injecting a fake just before a method call" 76 | * In this case I'm setting a fake extension manager, that converts this in a unit test!!, because 77 | * right now I have not external dependencies. 78 | */ 79 | it('return true using extensionManagerFactory', async () => { 80 | myFakeExtensionManager.willBeValid(true); 81 | const extensionManager = extensionManagerFactory(); 82 | extensionManager.setManager(myFakeExtensionManager); 83 | 84 | const logAnalyzer = logAnalyzerFactory(extensionManager.create()); 85 | const result = await logAnalyzer.isValidLogFileName('johndoe.sql'); 86 | 87 | expect(result).toBe(true); 88 | }); 89 | 90 | /** 91 | * I'm using the tecnique "Extract and override", this technique has several steps: 92 | * 93 | * step 1: create a virtual function in the unit under test(logAnalyzer.js in this case) 94 | * that returns the real extension manager, the one that works with the filesystem 95 | * 96 | * step 2: create a class that extends of it 97 | * 98 | * step3: use this new class to create the tests!! :) 99 | */ 100 | it('return false using testableLogAnalyzer', async () => { 101 | const expected = false; 102 | myFakeExtensionManager.willBeValid(expected); 103 | 104 | const logAnalyzer = new TestableLogAnalyzerClass( 105 | myFakeExtensionManager 106 | ); 107 | const result = await logAnalyzer.isValidLogFileName('johndoe.ts'); 108 | 109 | expect(result).toBe(expected); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /chapter_03-using-stubs-to-break-dependencies/LogAn/testableLogAnalyzer.class.js: -------------------------------------------------------------------------------- 1 | const fakeExtensionManagerFactory = require('./fakeExtensionManager'); 2 | const logAnalizer = require('./logAnalyzer.class'); 3 | 4 | class TestableLogAnalyzer extends logAnalizer { 5 | constructor(extensionManager) { 6 | super(); 7 | this.manager = extensionManager; 8 | } 9 | getManager() { 10 | return this.manager; 11 | } 12 | } 13 | 14 | module.exports = TestableLogAnalyzer; 15 | -------------------------------------------------------------------------------- /chapter_04-interaction-testing-using-mock-objects/LogAn/errors/ArgumentError.js: -------------------------------------------------------------------------------- 1 | class ArgumentError extends Error {} 2 | 3 | module.exports = ArgumentError; 4 | -------------------------------------------------------------------------------- /chapter_04-interaction-testing-using-mock-objects/LogAn/extensionManager.js: -------------------------------------------------------------------------------- 1 | const fileExtensionManagerFactory = require('./fileExtensionManager'); 2 | 3 | function extensionManager() { 4 | let customManager = null; 5 | 6 | function create() { 7 | if (customManager !== null) { 8 | return customManager; 9 | } 10 | 11 | return fileExtensionManagerFactory(); 12 | } 13 | 14 | function setManager(manager) { 15 | customManager = manager; 16 | } 17 | 18 | return { 19 | create, 20 | setManager, 21 | }; 22 | } 23 | 24 | module.exports = extensionManager; 25 | -------------------------------------------------------------------------------- /chapter_04-interaction-testing-using-mock-objects/LogAn/fakes/alwaysValidFakeExtensionManager.js: -------------------------------------------------------------------------------- 1 | function alwaysValidFakeExtensionManager() { 2 | /** 3 | * @param {string} fileName 4 | * @return {Promise} 5 | */ 6 | async function isValid(fileName) { 7 | return true; 8 | } 9 | 10 | return { 11 | isValid, 12 | }; 13 | } 14 | 15 | module.exports = alwaysValidFakeExtensionManager; 16 | -------------------------------------------------------------------------------- /chapter_04-interaction-testing-using-mock-objects/LogAn/fakes/fakeEmailService.js: -------------------------------------------------------------------------------- 1 | function fakeEmailService() { 2 | let lastMessage; 3 | 4 | /** 5 | * @param {string} message 6 | */ 7 | function sendEmail(message) { 8 | lastMessage = message; 9 | } 10 | 11 | function getLastMessage() { 12 | return lastMessage; 13 | } 14 | 15 | return { 16 | sendEmail, 17 | getLastMessage, 18 | }; 19 | } 20 | 21 | module.exports = fakeEmailService; 22 | -------------------------------------------------------------------------------- /chapter_04-interaction-testing-using-mock-objects/LogAn/fakes/fakeExtensionManager.js: -------------------------------------------------------------------------------- 1 | function fakeExtensionManager() { 2 | /** 3 | * @type {boolean} 4 | */ 5 | let valid = false; 6 | 7 | /** 8 | * @param {boolean} value 9 | */ 10 | function willBeValid(value) { 11 | valid = value; 12 | } 13 | 14 | /** 15 | * @param {string} fileName 16 | */ 17 | async function isValid(fileName) { 18 | return valid; 19 | } 20 | 21 | return { 22 | willBeValid, 23 | isValid, 24 | }; 25 | } 26 | module.exports = fakeExtensionManager; 27 | -------------------------------------------------------------------------------- /chapter_04-interaction-testing-using-mock-objects/LogAn/fakes/fakeThrowsErrorWebService.js: -------------------------------------------------------------------------------- 1 | function fakeThrowsErrorWebService() { 2 | /** 3 | * @param {string} message 4 | */ 5 | function logError(message) { 6 | throw new Error("can't log"); 7 | } 8 | 9 | return { 10 | logError, 11 | }; 12 | } 13 | 14 | module.exports = fakeThrowsErrorWebService; 15 | -------------------------------------------------------------------------------- /chapter_04-interaction-testing-using-mock-objects/LogAn/fakes/fakeWebService.js: -------------------------------------------------------------------------------- 1 | function fakeWebService() { 2 | let lastError; 3 | 4 | /** 5 | * @param {string} message 6 | */ 7 | function logError(message) { 8 | lastError = message; 9 | } 10 | 11 | function getLastError() { 12 | return lastError; 13 | } 14 | 15 | return { 16 | logError, 17 | getLastError, 18 | }; 19 | } 20 | 21 | module.exports = fakeWebService; 22 | -------------------------------------------------------------------------------- /chapter_04-interaction-testing-using-mock-objects/LogAn/fileExtensionManager.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const util = require('util'); 3 | 4 | const readFilePromisied = util.promisify(fs.readFile); 5 | 6 | function fileExtensionManager() { 7 | /** 8 | * @param {string} fileName 9 | * @return {Promise} 10 | */ 11 | async function isValid(fileName) { 12 | const fileNameExtensions = await readFilePromisied( 13 | `${__dirname}/fileNameExtensions.config.json`, 14 | 'utf8' 15 | ).then(fileContent => JSON.parse(fileContent).extensions); 16 | 17 | const isValidExtension = fileNameExtensions.some( 18 | function checkFileNameExtension(extension) { 19 | if ( 20 | fileName 21 | .toUpperCase() 22 | .endsWith(`.${extension.toUpperCase()}`) 23 | ) { 24 | return true; 25 | } 26 | 27 | return false; 28 | } 29 | ); 30 | 31 | return isValidExtension; 32 | } 33 | 34 | return { 35 | isValid, 36 | }; 37 | } 38 | 39 | module.exports = fileExtensionManager; 40 | -------------------------------------------------------------------------------- /chapter_04-interaction-testing-using-mock-objects/LogAn/fileNameExtensions.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensions": ["slf", "sql"] 3 | } 4 | -------------------------------------------------------------------------------- /chapter_04-interaction-testing-using-mock-objects/LogAn/logAnalyzer.class.js: -------------------------------------------------------------------------------- 1 | const ArgumentError = require('./errors/ArgumentError'); 2 | const fileExtensionManagerFactory = require('./fileExtensionManager'); 3 | 4 | class LogAnalyzer { 5 | constructor() { 6 | /** 7 | * @type {boolean} 8 | */ 9 | this.wasLastFileNameValid = false; 10 | } 11 | /** 12 | * @return {boolean} 13 | */ 14 | getWasLastFileNameValid() { 15 | return this.wasLastFileNameValid; 16 | } 17 | 18 | /** 19 | * a virtual function created to use "Extract and Override" technique 20 | */ 21 | getManager() { 22 | return fileExtensionManagerFactory(); 23 | } 24 | 25 | /** 26 | * @param {string} fileName 27 | * @return {Promise} 28 | */ 29 | async isValidLogFileName(fileName) { 30 | this.wasLastFileNameValid = false; 31 | 32 | if (fileName === '') { 33 | throw new ArgumentError('filename has to be provided'); 34 | } 35 | 36 | const result = await this.getManager().isValid(fileName); 37 | 38 | if (!result) { 39 | return false; 40 | } 41 | 42 | this.wasLastFileNameValid = true; 43 | return true; 44 | } 45 | } 46 | 47 | module.exports = LogAnalyzer; 48 | -------------------------------------------------------------------------------- /chapter_04-interaction-testing-using-mock-objects/LogAn/logAnalyzer.js: -------------------------------------------------------------------------------- 1 | const ArgumentError = require('./errors/ArgumentError'); 2 | 3 | function logAnalyzer(extensionManager, webService) { 4 | /** 5 | * @type {boolean} 6 | */ 7 | let wasLastFileNameValid; 8 | 9 | /** 10 | * @return {boolean} 11 | */ 12 | function getWasLastFileNameValid() { 13 | return wasLastFileNameValid; 14 | } 15 | 16 | /** 17 | * @param {string} fileName 18 | * @return {Promise} 19 | */ 20 | async function isValidLogFileName(fileName) { 21 | wasLastFileNameValid = false; 22 | 23 | if (fileName === '') { 24 | throw new ArgumentError('filename has to be provided'); 25 | } 26 | 27 | const result = await extensionManager.isValid(fileName); 28 | 29 | if (!result) { 30 | return false; 31 | } 32 | 33 | wasLastFileNameValid = true; 34 | return true; 35 | } 36 | 37 | /** 38 | * @param {string} fileName 39 | */ 40 | function analyze(fileName) { 41 | if (fileName.length < 8) { 42 | webService.logError(`Filename too short: ${fileName}`); 43 | } 44 | } 45 | 46 | return { 47 | getWasLastFileNameValid, 48 | isValidLogFileName, 49 | analyze, 50 | }; 51 | } 52 | 53 | module.exports = logAnalyzer; 54 | -------------------------------------------------------------------------------- /chapter_04-interaction-testing-using-mock-objects/LogAn/logAnalyzer.test.js: -------------------------------------------------------------------------------- 1 | const logAnalyzerFactory = require('./logAnalyzer'); 2 | const fakeExtensionManagerFactory = require('./fakes/fakeExtensionManager'); 3 | const extensionManagerFactory = require('./extensionManager'); 4 | 5 | // imported to try the technique "Extract and override" 6 | const TestableLogAnalyzerClass = require('./testableLogAnalyzer.class'); 7 | 8 | // this fake will be used as a mock 9 | const fakeWebServiceFactory = require('./fakes/fakeWebService'); 10 | 11 | let myFakeExtensionManager; 12 | 13 | beforeEach(() => { 14 | myFakeExtensionManager = fakeExtensionManagerFactory(); 15 | }); 16 | 17 | describe.each([ 18 | ['johndoe.js', false], 19 | ['johndoe.slf', true], 20 | ['johndoe.SLF', true], 21 | ])('isValidLogFileName("%s"))', (fileName, expected) => { 22 | it(`bad extension returns ${expected}`, async () => { 23 | myFakeExtensionManager.willBeValid(expected); 24 | 25 | const logAnalyzer = logAnalyzerFactory(myFakeExtensionManager); 26 | const result = await logAnalyzer.isValidLogFileName(fileName); 27 | expect(result).toBe(expected); 28 | }); 29 | }); 30 | 31 | describe('isValidLogFileName', () => { 32 | it('empty filename throws error', async () => { 33 | async function emptyLogFileName() { 34 | const logAnalyzer = logAnalyzerFactory(myFakeExtensionManager); 35 | 36 | return logAnalyzer.isValidLogFileName(''); 37 | } 38 | 39 | await expect(emptyLogFileName()).rejects.toThrow( 40 | 'filename has to be provided' 41 | ); 42 | }); 43 | 44 | /** 45 | * an example of state-based testing 46 | */ 47 | it.each` 48 | fileName | expected 49 | ${'johndoe.foo'} | ${false} 50 | ${'johndoe.slf'} | ${true} 51 | `( 52 | 'when called there changes wasLastFileNameValid that returns $expected', 53 | async ({ fileName, expected }) => { 54 | myFakeExtensionManager.willBeValid(expected); 55 | 56 | const logAnalyzer = logAnalyzerFactory(myFakeExtensionManager); 57 | await logAnalyzer.isValidLogFileName(fileName); 58 | const result = logAnalyzer.getWasLastFileNameValid(); 59 | 60 | expect(result).toBe(expected); 61 | } 62 | ); 63 | 64 | /** 65 | * an example of use of "injecting a fake just before a method call" 66 | * right now I'm not injecting a fake, and extensionManager is returning fileExtensionManager, 67 | * therefore this test is an integration test and not a unit test!!!! 68 | */ 69 | it('return true using extensionManagerFactory', async () => { 70 | const extensionManager = extensionManagerFactory(); 71 | const logAnalyzer = logAnalyzerFactory(extensionManager.create()); 72 | const result = await logAnalyzer.isValidLogFileName('johndoe.sql'); 73 | 74 | expect(result).toBe(true); 75 | }); 76 | 77 | /** 78 | * an example of use of "injecting a fake just before a method call" 79 | * In this case I'm setting a fake extension manager, that converts this in a unit test!!, because 80 | * right now I have not external dependencies. 81 | */ 82 | it('return true using extensionManagerFactory', async () => { 83 | myFakeExtensionManager.willBeValid(true); 84 | const extensionManager = extensionManagerFactory(); 85 | extensionManager.setManager(myFakeExtensionManager); 86 | 87 | const logAnalyzer = logAnalyzerFactory(extensionManager.create()); 88 | const result = await logAnalyzer.isValidLogFileName('johndoe.sql'); 89 | 90 | expect(result).toBe(true); 91 | }); 92 | 93 | /** 94 | * I'm using the tecnique "Extract and override", this technique has several steps: 95 | * 96 | * step 1: create a virtual function in the unit under test(logAnalyzer.js in this case) 97 | * that returns the real extension manager, the one that works with the filesystem 98 | * 99 | * step 2: create a class that extends of it 100 | * 101 | * step3: use this new class to create the tests!! :) 102 | */ 103 | it('return false using testableLogAnalyzer', async () => { 104 | const expected = false; 105 | myFakeExtensionManager.willBeValid(expected); 106 | 107 | const logAnalyzer = new TestableLogAnalyzerClass( 108 | myFakeExtensionManager 109 | ); 110 | const result = await logAnalyzer.isValidLogFileName('johndoe.ts'); 111 | 112 | expect(result).toBe(expected); 113 | }); 114 | }); 115 | 116 | describe('analyze', () => { 117 | let fakeWebService; 118 | beforeEach(() => { 119 | fakeWebService = fakeWebServiceFactory(); 120 | }); 121 | 122 | it('too short file calls webService', () => { 123 | const logAnalyzer = logAnalyzerFactory( 124 | myFakeExtensionManager, 125 | fakeWebService 126 | ); 127 | 128 | const fileName = 'johndoe'; 129 | const expectedMessage = `Filename too short: ${fileName}`; 130 | 131 | logAnalyzer.analyze(fileName); 132 | expect(fakeWebService.getLastError()).toBe(expectedMessage); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /chapter_04-interaction-testing-using-mock-objects/LogAn/logAnalyzer2.js: -------------------------------------------------------------------------------- 1 | function logAnalyzer2(webService, emailService) { 2 | /** 3 | * @param {string} fileName 4 | */ 5 | function analyze(fileName) { 6 | if (fileName.length < 8) { 7 | try { 8 | webService.logError(`Filename too short: ${fileName}`); 9 | } catch (e) { 10 | emailService.sendEmail("can't log"); 11 | } 12 | } 13 | } 14 | 15 | return { 16 | analyze, 17 | }; 18 | } 19 | 20 | module.exports = logAnalyzer2; 21 | -------------------------------------------------------------------------------- /chapter_04-interaction-testing-using-mock-objects/LogAn/logAnalyzer2.test.js: -------------------------------------------------------------------------------- 1 | const logAnalyzer2Factory = require('./logAnalyzer2'); 2 | 3 | // this fake right now will be used as a stub 4 | const fakeThrowsErrorWebServiceFactory = require('./fakes/fakeThrowsErrorWebService'); 5 | 6 | // this fake right now will be used as a mock 7 | const fakeEmailFactory = require('./fakes/fakeEmailService'); 8 | 9 | describe('analyze', () => { 10 | let fakeThrowsErrorWebWebService; 11 | let fakeEmailService; 12 | beforeEach(() => { 13 | fakeThrowsErrorWebWebService = fakeThrowsErrorWebServiceFactory(); 14 | fakeEmailService = fakeEmailFactory(); 15 | }); 16 | 17 | it('too short file calls webService', () => { 18 | const logAnalyzer = logAnalyzer2Factory( 19 | fakeThrowsErrorWebWebService, 20 | fakeEmailService 21 | ); 22 | 23 | const expectedMessage = "can't log"; 24 | 25 | logAnalyzer.analyze('johndoe'); 26 | expect(fakeEmailService.getLastMessage()).toBe(expectedMessage); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /chapter_04-interaction-testing-using-mock-objects/LogAn/testableLogAnalyzer.class.js: -------------------------------------------------------------------------------- 1 | const fakeExtensionManagerFactory = require('./fakes/fakeExtensionManager'); 2 | const logAnalizer = require('./logAnalyzer.class'); 3 | 4 | class TestableLogAnalyzer extends logAnalizer { 5 | constructor(extensionManager) { 6 | super(); 7 | this.manager = extensionManager; 8 | } 9 | getManager() { 10 | return this.manager; 11 | } 12 | } 13 | 14 | module.exports = TestableLogAnalyzer; 15 | -------------------------------------------------------------------------------- /chapter_04-interaction-testing-using-mock-objects/LogAn/webService.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios').default; 2 | 3 | function webService() { 4 | /** 5 | * @param {string} message 6 | */ 7 | async function logError(message) { 8 | return axios({ 9 | method: 'POST', 10 | url: 'http://localhost:3000', 11 | data: message, 12 | }); 13 | } 14 | 15 | return { 16 | logError, 17 | }; 18 | } 19 | 20 | module.exports = webService; 21 | -------------------------------------------------------------------------------- /chapter_05-isolation-frameworks/errors/ArgumentError.js: -------------------------------------------------------------------------------- 1 | class ArgumentError extends Error {} 2 | 3 | module.exports = ArgumentError; 4 | -------------------------------------------------------------------------------- /chapter_05-isolation-frameworks/fakes/fakeWebService.js: -------------------------------------------------------------------------------- 1 | function fakeWebService() { 2 | let lastError; 3 | 4 | /** 5 | * @param {string} message 6 | */ 7 | function logError(message) { 8 | lastError = message; 9 | } 10 | 11 | function getLastError() { 12 | return lastError; 13 | } 14 | 15 | return { 16 | logError, 17 | getLastError, 18 | }; 19 | } 20 | 21 | module.exports = fakeWebService; 22 | -------------------------------------------------------------------------------- /chapter_05-isolation-frameworks/logAnalyzer.js: -------------------------------------------------------------------------------- 1 | const ArgumentError = require('./errors/ArgumentError'); 2 | 3 | function logAnalyzer(extensionManager, webService) { 4 | /** 5 | * @type {boolean} 6 | */ 7 | let wasLastFileNameValid; 8 | 9 | /** 10 | * @return {boolean} 11 | */ 12 | function getWasLastFileNameValid() { 13 | return wasLastFileNameValid; 14 | } 15 | 16 | /** 17 | * @param {string} fileName 18 | * @return {Promise} 19 | */ 20 | async function isValidLogFileName(fileName) { 21 | wasLastFileNameValid = false; 22 | 23 | if (fileName === '') { 24 | throw new ArgumentError('filename has to be provided'); 25 | } 26 | 27 | const result = await extensionManager.isValid(fileName); 28 | 29 | if (!result) { 30 | return false; 31 | } 32 | 33 | wasLastFileNameValid = true; 34 | return true; 35 | } 36 | 37 | /** 38 | * @param {string} fileName 39 | */ 40 | function analyze(fileName) { 41 | if (fileName.length < 8) { 42 | webService.logError(`Filename too short: ${fileName}`); 43 | } 44 | } 45 | 46 | return { 47 | getWasLastFileNameValid, 48 | isValidLogFileName, 49 | analyze, 50 | }; 51 | } 52 | 53 | module.exports = logAnalyzer; 54 | -------------------------------------------------------------------------------- /chapter_05-isolation-frameworks/logAnalyzer.test.js: -------------------------------------------------------------------------------- 1 | const logAnalyzerFactory = require('./logAnalyzer'); 2 | 3 | let myFakeExtensionManager; 4 | 5 | describe('analyze', () => { 6 | let fakeWebService; 7 | beforeEach(() => { 8 | fakeWebService = { 9 | logError: jest.fn(), 10 | }; 11 | }); 12 | 13 | it('too short file calls webService with the correct error message', () => { 14 | const logAnalyzer = logAnalyzerFactory( 15 | myFakeExtensionManager, 16 | fakeWebService 17 | ); 18 | 19 | const fileName = 'johndoe'; 20 | const expectedMessage = `Filename too short: ${fileName}`; 21 | 22 | logAnalyzer.analyze(fileName); 23 | expect(fakeWebService.logError).toHaveBeenCalledTimes(1); 24 | expect(fakeWebService.logError).toHaveBeenCalledWith(expectedMessage); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /chapter_07-test-hierarchies-and-organization/abstractTestInfrastructureClassPattern/configurationManager.js: -------------------------------------------------------------------------------- 1 | const loggingFacility = require('./loggingFacility'); 2 | 3 | function configurationManager() { 4 | /** 5 | * @param {string} configName 6 | */ 7 | function isConfigured(configName) { 8 | loggingFacility.log(`checking ${configName}`); 9 | } 10 | 11 | return { 12 | isConfigured, 13 | }; 14 | } 15 | 16 | module.exports = configurationManager; 17 | -------------------------------------------------------------------------------- /chapter_07-test-hierarchies-and-organization/abstractTestInfrastructureClassPattern/configurationManager.test.js: -------------------------------------------------------------------------------- 1 | const configurationManagerFactory = require('./configurationManager'); 2 | const { fakeTheLogger, tearDown } = require('./testsUtils/baseTests'); 3 | 4 | describe('isConfigured', () => { 5 | it('logging file check', () => { 6 | fakeTheLogger(); 7 | const configurationManager = configurationManagerFactory(); 8 | configurationManager.isConfigured(''); 9 | }); 10 | 11 | afterEach(tearDown); 12 | }); 13 | -------------------------------------------------------------------------------- /chapter_07-test-hierarchies-and-organization/abstractTestInfrastructureClassPattern/logAnalyzer.js: -------------------------------------------------------------------------------- 1 | const loggingFacility = require('./loggingFacility'); 2 | 3 | function logAnalyzer() { 4 | /** 5 | * @param {string} fileName 6 | */ 7 | function analyze(fileName) { 8 | if (fileName.length < 8) { 9 | loggingFacility.log(`Filename too short: ${fileName}`); 10 | } 11 | } 12 | 13 | return { 14 | analyze, 15 | }; 16 | } 17 | 18 | module.exports = logAnalyzer; 19 | -------------------------------------------------------------------------------- /chapter_07-test-hierarchies-and-organization/abstractTestInfrastructureClassPattern/logAnalyzer.test.js: -------------------------------------------------------------------------------- 1 | const logAnalyzerFactory = require('./logAnalyzer'); 2 | const { fakeTheLogger, tearDown } = require('./testsUtils/baseTests'); 3 | 4 | describe('analyze', () => { 5 | it('empty file throws exception', () => { 6 | fakeTheLogger(); 7 | const logAnalyzer = logAnalyzerFactory(); 8 | logAnalyzer.analyze(''); 9 | }); 10 | 11 | afterEach(tearDown); 12 | }); 13 | -------------------------------------------------------------------------------- /chapter_07-test-hierarchies-and-organization/abstractTestInfrastructureClassPattern/loggingFacility.js: -------------------------------------------------------------------------------- 1 | let logger = console.log; 2 | 3 | function log(text) { 4 | logger(text); 5 | } 6 | 7 | function getLogger() { 8 | return logger; 9 | } 10 | 11 | function setLogger(value) { 12 | logger = value; 13 | } 14 | 15 | module.exports = { 16 | log, 17 | getLogger, 18 | setLogger, 19 | }; 20 | -------------------------------------------------------------------------------- /chapter_07-test-hierarchies-and-organization/abstractTestInfrastructureClassPattern/testsUtils/baseTests.js: -------------------------------------------------------------------------------- 1 | let loggingFacility = require('../loggingFacility'); 2 | 3 | function fakeTheLogger() { 4 | loggingFacility.setLogger(function fakeLogger(value) { 5 | return value; 6 | }); 7 | } 8 | 9 | function tearDown() { 10 | loggingFacility.setLogger(null); 11 | } 12 | 13 | module.exports = { 14 | fakeTheLogger, 15 | tearDown, 16 | }; 17 | -------------------------------------------------------------------------------- /loggerWebService/index.js: -------------------------------------------------------------------------------- 1 | const server = require('./server'); 2 | const port = process.env.PORT || 3000; 3 | 4 | server.listen(port, function startServer() { 5 | console.log( 6 | `Webservice is ready to start listening requests on http://localhost:${port}` 7 | ); 8 | }); 9 | -------------------------------------------------------------------------------- /loggerWebService/server.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | 3 | const server = http.createServer(function getRequest(request, response) { 4 | if (request.method !== 'POST') { 5 | response.statusCode = 405; 6 | return response.end(); 7 | } 8 | 9 | request.setEncoding('utf8'); 10 | 11 | let data = ''; 12 | 13 | request.on('data', function getData(chunk) { 14 | data += chunk; 15 | }); 16 | 17 | request.on('end', function getServerResponse() { 18 | console.log( 19 | `Writing the error in the stdout, waiting that an hipotetic tool like influxdb read the info and save it: 20 | ${data} 21 | ` 22 | ); 23 | response.statusCode = 201; 24 | response.end(); 25 | }); 26 | }); 27 | 28 | module.exports = server; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "the_art_of_unit_testing", 3 | "version": "1.0.0", 4 | "description": "Repository that contains code in javascript from the book The Art of Unit Testing, Second Edition by Roy Osherove", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/devcorpio/the-art-of-unit-testing.git" 8 | }, 9 | "scripts": { 10 | "test": "jest" 11 | }, 12 | "keywords": [], 13 | "author": "devcorpio", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/devcorpio/the-art-of-unit-testing/issues" 17 | }, 18 | "homepage": "https://github.com/devcorpio/the-art-of-unit-testing#readme", 19 | "devDependencies": { 20 | "@types/node": "^10.12.18", 21 | "axios": "^0.18.0", 22 | "husky": "^1.3.1", 23 | "jest": "^23.6.0", 24 | "prettier": "1.15.3", 25 | "pretty-quick": "^1.8.0" 26 | }, 27 | "husky": { 28 | "hooks": { 29 | "pre-commit": "pretty-quick --staged" 30 | } 31 | } 32 | } 33 | --------------------------------------------------------------------------------