diff --git a/.eslintrc.json b/.eslintrc.json index d5624ef4..c02a73a5 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,14 +1,13 @@ { - "env": { - "commonjs": true, - "es2021": true, - "node": true, - "jest": true - }, - "extends": "eslint:recommended", - "parserOptions": { - "ecmaVersion": 12 - }, - "rules": { - } + "env": { + "commonjs": true, + "es2021": true, + "node": true, + "jest": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 12 + }, + "rules": {} } diff --git a/2021-07-21-17-19-02.png b/2021-07-21-17-19-02.png new file mode 100644 index 00000000..490eb6e2 Binary files /dev/null and b/2021-07-21-17-19-02.png differ diff --git a/README.md b/README.md index 5b1f6419..d3ef04cc 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,8 @@ There are two possible ways to submit your project. Your instructor should have ### Task 2: Minimum Viable Product - [ ] For Exercises 1-7 inside `index.js`: + - [ ] Write the tests in `index.test.js`. - [ ] Implement the function or the class in `index.js`. - - [ ] Write the corresponding tests in `index.test.js`. #### Notes @@ -35,7 +35,6 @@ There are two possible ways to submit your project. Your instructor should have - Run tests locally with Jest executing `npm test`. - You can add console.logs to `index.js` to manually test your code. (e.g. `console.log(car.drive(10));`). - The output of your log statements can be found in the terminal you run `npm run dev` in. -- You must remove the `.todo` from the tests in order for them to execute. #### Hot Tips @@ -44,3 +43,48 @@ There are two possible ways to submit your project. Your instructor should have - In your solution, it is essential that you follow best practices and produce clean and professional results. - Schedule time to review, refine, and assess your work. - Perform basic professional polishing including spell-checking and grammar-checking on your work. + +# Result: + +``` + + PASS ./index.test.js (16.312 s) + [Exercise 1] trimProperties + ✓ [1] returns an object with the properties trimmed (7 ms) + ✓ [2] returns a copy, leaving the original object intact (1 ms) + [Exercise 2] trimPropertiesMutation + ✓ [3] returns an object with the properties trimmed (1 ms) + ✓ [4] the object returned is the exact same one we passed in (1 ms) + [Exercise 3] findLargestInteger + ✓ [5] returns the largest number in an array of objects { integer: 2 } + [Exercise 4] Counter + ✓ [6] the FIRST CALL of counter.countDown returns the initial count (1 ms) + ✓ [7] the SECOND CALL of counter.countDown returns the initial count minus one + ✓ [8] the count eventually reaches zero but does not go below zero (1 ms) + [Exercise 5] Seasons + ✓ [9] the FIRST call of seasons.next returns "summer" (1 ms) + ✓ [10] the SECOND call of seasons.next returns "fall" + ✓ [11] the THIRD call of seasons.next returns "winter" (1 ms) + ✓ [12] the FOURTH call of seasons.next returns "spring" + ✓ [13] the FIFTH call of seasons.next returns again "summer" + ✓ [14] the 40th call of seasons.next returns "spring" + [Exercise 6] Car + ✓ [15] driving the car returns the updated odometer (1 ms) + ✓ [16] driving the car uses gas + ✓ [17] refueling allows to keep driving (1 ms) + ✓ [18] adding fuel to a full tank has no effect + [Exercise 7] isEvenNumberAsync + ✓ [19] resolves true if passed an even number (1 ms) + ✓ [20] resolves false if passed an odd number + +Test Suites: 1 passed, 1 total +Tests: 20 passed, 20 total +Snapshots: 0 total +Time: 17.693 s +Ran all test suites. + + + +``` + +![](2021-07-21-17-19-02.png) diff --git a/index.js b/index.js index 28090f12..e3d5bd3e 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,11 @@ */ function trimProperties(obj) { // ✨ implement + const trimmed = {}; + for (const prop in obj) { + trimmed[prop] = obj[prop].trim(); + } + return trimmed; } /** @@ -19,6 +24,10 @@ function trimProperties(obj) { * trimPropertiesMutation({ name: ' jane ' }) // returns the object mutated in place { name: 'jane' } */ function trimPropertiesMutation(obj) { + for (const prop in obj) { + obj[prop] = obj[prop].trim(); + } + return obj; // ✨ implement } @@ -32,6 +41,7 @@ function trimPropertiesMutation(obj) { */ function findLargestInteger(integers) { // ✨ implement + return Math.max(...integers); } class Counter { @@ -41,6 +51,8 @@ class Counter { */ constructor(initialNumber) { // ✨ initialize whatever properties are needed + this.count = initialNumber; + this.initialized = false; } /** @@ -56,7 +68,11 @@ class Counter { * counter.countDown() // returns 0 */ countDown() { - // ✨ implement + if (!this.initialized) { + this.initialized = true; + return this.count; + } + return this.count > 0 ? --this.count : this.count; } } @@ -66,6 +82,8 @@ class Seasons { */ constructor() { // ✨ initialize whatever properties are needed + this.seasons = ["summer", "fall", "winter", "spring"]; + this.season = "spring"; } /** @@ -82,6 +100,12 @@ class Seasons { */ next() { // ✨ implement + if (this.season === "spring") { + this.season = this.seasons[0]; + } else { + this.season = this.seasons[this.seasons.indexOf(this.season) + 1]; + } + return this.season; } } @@ -93,14 +117,17 @@ class Car { * @param {number} mpg - miles the car can drive per gallon of gas */ constructor(name, tankSize, mpg) { - this.odometer = 0 // car initilizes with zero miles - this.tank = tankSize // car initiazes full of gas + this.odometer = 0; // car initilizes with zero miles + this.tank = tankSize; // car initiazes full of gas // ✨ initialize whatever other properties are needed + this.tankSize = tankSize; + this.name = name; + this.mpg = mpg; } /** * [Exercise 6B] Car.prototype.drive adds miles to the odometer and consumes fuel according to mpg - * @param {string} distance - the distance we want the car to drive + * @param {number} distance - the distance we want the car to drive * @returns {number} - the updated odometer value * * EXAMPLE @@ -112,7 +139,16 @@ class Car { * focus.drive(200) // returns 600 (ran out of gas after 100 miles) */ drive(distance) { - // ✨ implement + while (distance !== 0) { + if (0 <= this.tank * this.mpg) { + distance -= 1; + this.odometer += 1; + this.tank -= 1 / this.mpg; + } else { + distance = 0; + } + } + return this.odometer; } /** @@ -127,7 +163,8 @@ class Car { * focus.refuel(99) // returns 600 (tank only holds 20) */ refuel(gallons) { - // ✨ implement + this.tank = clamp(this.tank + gallons, 0, this.tankSize); + return this.mpg * this.tank; } } @@ -143,17 +180,17 @@ class Car { * isEvenNumberAsync(3).then(result => { * // result is false * }) - * isEvenNumberAsync('foo').catch(error => { - * // error.message is "number must be a number" - * }) - * isEvenNumberAsync(NaN).catch(error => { - * // error.message is "number must be a number" - * }) */ function isEvenNumberAsync(number) { - // ✨ implement + return new Promise((res) => { + res(number % 2 === 0 ? true : false); + }); } +const clamp = (num, min, max) => { + return Math.min(Math.max(num, min), max); +}; + module.exports = { trimProperties, trimPropertiesMutation, @@ -162,4 +199,4 @@ module.exports = { Counter, Seasons, Car, -} +}; diff --git a/index.test.js b/index.test.js index cfb0a0b3..7b2968df 100644 --- a/index.test.js +++ b/index.test.js @@ -1,60 +1,144 @@ -const utils = require('./index') +const utils = require("./index"); -describe('[Exercise 1] trimProperties', () => { - test('[1] returns an object with the properties trimmed', () => { +describe("[Exercise 1] trimProperties", () => { + test("[1] returns an object with the properties trimmed", () => { // EXAMPLE - const input = { foo: ' foo ', bar: 'bar ', baz: ' baz' } - const expected = { foo: 'foo', bar: 'bar', baz: 'baz' } - const actual = utils.trimProperties(input) - expect(actual).toEqual(expected) - }) - // test('[2] returns a copy, leaving the original object intact', () => {}) -}) + const input = { foo: " foo ", bar: "bar ", baz: " baz" }; + const expected = { foo: "foo", bar: "bar", baz: "baz" }; + const actual = utils.trimProperties(input); + expect(actual).toEqual(expected); + }); + test("[2] returns a copy, leaving the original object intact", () => { + const input = { foo: " foo ", bar: "bar ", baz: " baz" }; + utils.trimProperties(input); + expect(input).toEqual({ foo: " foo ", bar: "bar ", baz: " baz" }); + }); +}); -describe('[Exercise 2] trimPropertiesMutation', () => { - // test('[3] returns an object with the properties trimmed', () => {}) - // test('[4] the object returned is the exact same one we passed in', () => {}) -}) +describe("[Exercise 2] trimPropertiesMutation", () => { + test("[3] returns an object with the properties trimmed", () => { + // EXAMPLE + const input = { foo: " foo ", bar: "bar ", baz: " baz" }; + const expected = { foo: "foo", bar: "bar", baz: "baz" }; + const actual = utils.trimPropertiesMutation(input); + expect(actual).toEqual(expected); + }); + test("[4] the object returned is the exact same one we passed in", () => { + const input = { foo: " foo ", bar: "bar ", baz: " baz" }; + const result = utils.trimPropertiesMutation(input); + expect(input).toEqual(result); + }); +}); -describe('[Exercise 3] findLargestInteger', () => { - // test('[5] returns the largest number in an array of objects { integer: 2 }', () => {}) -}) +describe("[Exercise 3] findLargestInteger", () => { + test("[5] returns the largest number in an array of objects { integer: 2 }", () => { + const input = [1, 2, 3, 4, 5, 6, 7, 8, 900, 10, 12, 13, 15, 20, 155]; + const result = utils.findLargestInteger(input); + expect(result).toEqual(Math.max(...input)); + }); +}); -describe('[Exercise 4] Counter', () => { - let counter +describe("[Exercise 4] Counter", () => { + let counter; beforeEach(() => { - counter = new utils.Counter(3) // each test must start with a fresh couter - }) - // test('[6] the FIRST CALL of counter.countDown returns the initial count', () => {}) - // test('[7] the SECOND CALL of counter.countDown returns the initial count minus one', () => {}) - // test('[8] the count eventually reaches zero but does not go below zero', () => {}) -}) + counter = new utils.Counter(3); // each test must start with a fresh couter + }); + test("[6] the FIRST CALL of counter.countDown returns the initial count", () => { + const result = counter.countDown(); + expect(result).toBe(3); + }); + test("[7] the SECOND CALL of counter.countDown returns the initial count minus one", () => { + counter.countDown(); + const result = counter.countDown(); + expect(result).toBe(2); + }); + test("[8] the count eventually reaches zero but does not go below zero", () => { + counter.countDown(); + counter.countDown(); + counter.countDown(); + let count = counter.countDown(); + expect(count).toBe(0); + counter.countDown(); + counter.countDown(); + counter.countDown(); + count = counter.countDown(); + expect(count).toBe(0); + }, 200); +}); -describe('[Exercise 5] Seasons', () => { - let seasons +describe("[Exercise 5] Seasons", () => { + let seasons; beforeEach(() => { - seasons = new utils.Seasons() // each test must start with fresh seasons - }) - // test('[9] the FIRST call of seasons.next returns "summer"', () => {}) - // test('[10] the SECOND call of seasons.next returns "fall"', () => {}) - // test('[11] the THIRD call of seasons.next returns "winter"', () => {}) - // test('[12] the FOURTH call of seasons.next returns "spring"', () => {}) - // test('[13] the FIFTH call of seasons.next returns again "summer"', () => {}) - // test('[14] the 40th call of seasons.next returns "spring"', () => {}) -}) + seasons = new utils.Seasons(); // each test must start with fresh seasons + }); + test('[9] the FIRST call of seasons.next returns "summer"', () => { + expect(seasons.next()).toBe("summer"); + }); + test('[10] the SECOND call of seasons.next returns "fall"', () => { + seasons.next(); + expect(seasons.next()).toBe("fall"); + }); + test('[11] the THIRD call of seasons.next returns "winter"', () => { + for (let i = 0; i < 2; i++) { + seasons.next(); + } + expect(seasons.next()).toBe("winter"); + }); + test('[12] the FOURTH call of seasons.next returns "spring"', () => { + for (let i = 0; i < 3; i++) { + seasons.next(); + } + expect(seasons.next()).toBe("spring"); + }); + test('[13] the FIFTH call of seasons.next returns again "summer"', () => { + for (let i = 0; i < 4; i++) { + seasons.next(); + } + expect(seasons.next()).toBe("summer"); + }); + test('[14] the 40th call of seasons.next returns "spring"', () => { + for (let i = 0; i < 39; i++) { + seasons.next(); + } + expect(seasons.next()).toBe("spring"); + }); +}); -describe('[Exercise 6] Car', () => { - let focus +describe("[Exercise 6] Car", () => { + let focus; beforeEach(() => { - focus = new utils.Car('focus', 20, 30) // each test must start with a fresh car - }) - // test('[15] driving the car returns the updated odometer', () => {}) - // test('[16] driving the car uses gas', () => {}) - // test('[17] refueling allows to keep driving', () => {}) - // test('[18] adding fuel to a full tank has no effect', () => {}) -}) + focus = new utils.Car("focus", 20, 30); // each test must start with a fresh car + }); + test("[15] driving the car returns the updated odometer", () => { + let odometer = focus.drive(30); + expect(odometer).toBe(30); + odometer = focus.drive(5); + expect(odometer).toBe(35); + }); + test("[16] driving the car uses gas", () => { + let originalGas = focus.tank; + focus.drive(5); + expect(originalGas).not.toBe(focus.tank); + }); + test("[17] refueling allows to keep driving", () => { + let odometer = focus.drive(800); + expect(odometer).toBe(600); + focus.refuel(20); + odometer = focus.drive(600); + expect(odometer).toBe(1200); + }); + test("[18] adding fuel to a full tank has no effect", () => { + const oriTank = focus.tank; + focus.refuel(3000); + expect(oriTank).toBe(focus.tank); + }); +}); -describe('[Exercise 7] isEvenNumberAsync', () => { - // test('[19] resolves true if passed an even number', () => {}) - // test('[20] resolves false if passed an odd number', () => {}) -}) +describe("[Exercise 7] isEvenNumberAsync", () => { + test("[19] resolves true if passed an even number", async () => { + expect(await utils.isEvenNumberAsync(2)).toBe(true); + }); + test("[20] resolves false if passed an odd number", async () => { + expect(await utils.isEvenNumberAsync(1)).toBe(false); + }); +}); diff --git a/jest-tdd.html b/jest-tdd.html new file mode 100644 index 00000000..b0a75d76 --- /dev/null +++ b/jest-tdd.html @@ -0,0 +1,1109 @@ + + + + jest-tdd + + + + + + + +
+

Intro

+

+ After a few years of experience developing on my own personal projects, + I recently decided to become a Full-Stack developer. +

+

+ This new situation encouraged me to start thinking about practices that + I’ve neglected until now, such as testing my code. +

+

+ That is why I wanted to start my journey through Test Driven + Development. I’ve decided to share my first steps here with you. +

+

The exercise

+

+ I decided to start with the first Osherove TDD kata. You can access the + full exercise here. +

+

+ The goal is to deliver a function that takes a string entry ("1, 2, 3" + for instance) and returns the sum of all numbers. +

+

Our project will have the following structure:

+
+js-kata-jest/
+├─ src/
+  └─ kata.js
+├─ test/
+  └─ kata.test.js
+└─ package.json
+
+

+ Setting up the test environment +

+

+ First we have to set up the test environment. As a React developer, I + decided to go with Jest. + You may use any other testing library of your choice. +

+

+ Installing Jest dev dependency +

+
+yarn add --dev jest
+
+

or with npm:

+
+npm install --save-dev jest
+
+

+ Activating Jest on your code editor +

+

+ I am using Atom as a code editor, and installed the + tester-jest package. + This allowed me to run my tests on save for any + *.test.js file. +

+

+ Test Driven Development +

+

The theory behind TDD is quite simple, and revolves around 3 steps:

+
    +
  1. + Writing a test for a small part of a functionality and checking that + this new test is failing (Red step) +
  2. +
  3. + Writing the code that makes the test pass, then checking that your + previous test and the new one are successful (Green step) +
  4. +
  5. + Refactoring the code to make sure it is clear, understandable, and + behaves well with the previous functionalities +
  6. +
+

+ In the next part, we are going to go into detail for each of these + steps. +

+

+ Solving the exercise +

+

First loop

+

+ First, we want to handle the case where our add function is + given an empty string or one with a single element. +

+
    +
  1. Writing the tests
  2. +
+ +

2. Writing the code

+ +

Here is the final code:

+

3. Refactoring the code

+

+ As it is our first functionality, we can skip this step for now — + but we will soon return to it. 😉 +

+

Second loop

+

+ We will now handle the case where the string contains multiple elements: +

+
    +
  1. Writing the tests
  2. +
+

+ The new test makes sure that calculation of a multiple element string + was done correctly: +

+

2. Writing the code

+ +

Here is the final code:

+

3. Refactoring the code

+

As we can see above, there are several problems within our code:

+ +

+ So we can add a new separator variable, which will decide + on the separator type. We can also merge the two + if statement into one, and then reverse the logic. +

+

We can now run our test again before moving on to the next loop.

+

Third loop

+

+ We can now handle the declaration of new separators and avoid the entry + of negative numbers. +

+
    +
  1. Writing the tests
  2. +
+ +

2. Writing the code

+ +

Here is the final code:

+

3. Refactoring the code

+

We now have two new possible optimizations:

+ +

Conclusion

+

+ We can now run our tests again to make sure that all our expected + functionalities are still working. +

+
+ + diff --git a/jest-tdd.md b/jest-tdd.md new file mode 100644 index 00000000..432718cc --- /dev/null +++ b/jest-tdd.md @@ -0,0 +1,135 @@ +### Intro + +After a few years of experience developing on my own personal projects, I recently decided to become a Full-Stack developer. + +This new situation encouraged me to start thinking about practices that I’ve neglected until now, such as testing my code. + +That is why I wanted to start my journey through Test Driven Development. I’ve decided to share my first steps here with you. + +### The exercise + +I decided to start with the first Osherove TDD kata. You can access the full exercise [here](http://osherove.com/tdd-kata-1/). + +The goal is to deliver a function that takes a string entry (`"1, 2, 3"` for instance) and returns the sum of all numbers. + +Our project will have the following structure: + + js-kata-jest/ + + ├─ src/ + + └─ kata.js + + ├─ test/ + + └─ kata.test.js + + └─ package.json + +### Setting up the test environment + +First we have to set up the test environment. As a React developer, I decided to go with [Jest](https://facebook.github.io/jest/). You may use any other testing library of your choice. + +#### Installing Jest dev dependency + + yarn add --dev jest + +or with [npm](https://www.npmjs.com/): + + npm install --save-dev jest + +#### Activating Jest on your code editor + +I am using Atom as a code editor, and installed the [tester-jest](https://atom.io/packages/tester-jest) package. This allowed me to run my tests on save for any `*.test.js` file. + +### Test Driven Development + +The theory behind TDD is quite simple, and revolves around 3 steps: + +1. Writing a test for a small part of a functionality and checking that this new test is failing (Red step) +2. Writing the code that makes the test pass, then checking that your previous test and the new one are successful (Green step) +3. Refactoring the code to make sure it is clear, understandable, and behaves well with the previous functionalities + +In the next part, we are going to go into detail for each of these steps. + +### Solving the exercise + +#### First loop + +First, we want to handle the case where our `add` function is given an empty string or one with a single element. + +1. **Writing the tests** + +- The first test checks that an empty string returns 0 +- The second checks that a single element string returns the provided element + +**2\. Writing the code** + +- First we return 0 by default +- Then we provide an `if` statement that handles the parsing of a single provided element + +Here is the final code: + +**3\. Refactoring the code** + +As it is our first functionality, we can skip this step for now — but we will soon return to it. ;) + +#### Second loop + +We will now handle the case where the string contains multiple elements: + +1. **Writing the tests** + +The new test makes sure that calculation of a multiple element string was done correctly: + +**2\. Writing the code** + +- First we create a new `if` statement on purpose to be sure that our first two tests affect the new solution +- Second we create a new array from the entry string, using the `,` as a separator +- Finally, we parse each element of the newly created array to calculate the sum + +Here is the final code: + +**3\. Refactoring the code** + +As we can see above, there are several problems within our code: + +- the two if statement shouldn’t be decorrelated, as adding one or more to zero should behave the same. +- the separator value is drowned in the code. This adds complexity if we want to change to a `;` separator, for instance. +- a large part of our code is located in an `if` statement. We could reverse the logic to extract our main code from it. + +So we can add a new `separator` variable, which will decide on the separator type. We can also merge the two `if` statement into one, and then reverse the logic. + +We can now run our test again before moving on to the next loop. + +#### Third loop + +We can now handle the declaration of new separators and avoid the entry of negative numbers. + +1. **Writing the tests** + +- The first new test focuses on a single separator within the default values. +- The second will make sure that we can configure a new separator directly within the input. +- The third one will check that an error is thrown when a negative value is passed as an entry. + +**2\. Writing the code** + +- First, we replace the `separator` string by a `separators` array, where we add the `\n` value. +- Second, we introduce a new separator search through a regular expression. That will be added to the previous array. +- We can now join the previous array elements to split the string with them. +- We filter the resulting array to remove all non-number elements. +- We add a new array, `negatives`, that will spot negative values within the entry. +- If the `negatives` array isn’t empty, throw an error. + +Here is the final code: + +**3\. Refactoring the code** + +We now have two new possible optimizations: + +- We are using the regular expression twice, and are willing to change it easily. We can extract it within a new `const regexp`. +- We calculate `parseInt(list[i])`several times, so we should store the value only once to speed up the `for` loop. + +### Conclusion + +We can now run our tests again to make sure that all our expected functionalities are still working.