├── .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 |
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 |
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 |
--------------------------------------------------------------------------------