Promise TDD Workshop
- In this exercise, you will learn how to build a Promise-like object, which we will call
MyPromise
, using Test-Driven Development (TDD). We will follow the three-step process:
- Write a failing test
- Implement the minimal code to make the test pass
- Refactor the code (if needed)
Prerequisites: Basic knowledge of JavaScript and TDD using a testing framework like Jest.
Step 1: Write a failing test
1.1. Create a new folder called my-promise
and navigate to it:
mkdir my-promise
cd my-promise
1.2. Initialize the project with npm:
npm init -y
1.3. Install Jest as a development dependency:
npm install --save-dev jest
1.4. Update the scripts
section in the package.json
file:
"scripts": {
"test": "jest"
}
1.5. Create a test file called my-promise.test.js
:
touch my-promise.test.js
1.6. Write the first test to check if MyPromise
is a constructor:
// my-promise.test.js
const MyPromise = require('./my-promise');
test('MyPromise should be a constructor', () => {
expect(typeof MyPromise).toBe('function');
});
Step 2: Implement the minimal code to make the test pass
2.1. Create a new file called my-promise.js
:
touch my-promise.js
2.2. Implement a basic MyPromise
constructor:
// my-promise.js
function MyPromise() {}
module.exports = MyPromise;
2.3. Run the tests:
npm test
Step 3: Continue adding tests and implementing features
3.1. Write a test for the then
method:
// my-promise.test.js
test('MyPromise should have a "then" method', () => {
const promise = new MyPromise();
expect(typeof promise.then).toBe('function');
});
3.2. Implement the then
method:
// my-promise.js
function MyPromise() {
this.then = function () {};
}
module.exports = MyPromise;
3.3. Run the tests:
npm test
3.4. Continue this process, adding more tests for features such as:
- Resolving a promise
- Rejecting a promise
- Chaining
then
calls - Error handling with
catch
MyPromise.resolve()
andMyPromise.reject()
static methodsMyPromise.all()
andMyPromise.race()
static methods
Remember to follow the TDD process for each feature:
- Write a failing test
- Implement the minimal code to make the test pass
- Refactor the code (if needed)
By the end of the exercise, you will have built a Promise-like object using Test-Driven Development.
3.4.1. Resolving a promise:
3.4.1.1. Write a test for resolving a promise:
// my-promise.test.js
test('MyPromise should resolve with a value', done => {
const promise = new MyPromise(resolve => {
resolve('Resolved value');
});
promise.then(value => {
expect(value).toBe('Resolved value');
done();
});
});
3.4.1.2. Implement resolving a promise:
// my-promise.js
function MyPromise(executor) {
this.then = function (onResolved) {
executor(onResolved);
};
}
module.exports = MyPromise;
3.4.1.3. Run the tests:
npm test
3.4.2. Rejecting a promise:
3.4.2.1. Write a test for rejecting a promise:
// my-promise.test.js
test('MyPromise should reject with a reason', done => {
const promise = new MyPromise((_, reject) => {
reject('Rejected reason');
});
promise.then(null, reason => {
expect(reason).toBe('Rejected reason');
done();
});
});
3.4.2.2. Implement rejecting a promise:
// my-promise.js
function MyPromise(executor) {
this.then = function (onResolved, onRejected) {
executor(onResolved, onRejected);
};
}
module.exports = MyPromise;
3.4.2.3. Run the tests:
npm test
3.4.3. Chaining then
calls:
3.4.3.1. Write a test for chaining then
calls:
// my-promise.test.js
test('MyPromise should allow chaining of then calls', done => {
const promise = new MyPromise(resolve => {
resolve(1);
});
promise
.then(value => {
return value + 1;
})
.then(value => {
expect(value).toBe(2);
done();
});
});
3.4.3.2. Implement chaining then
calls:
// my-promise.js
function MyPromise(executor) {
let onResolved = null;
let onRejected = null;
let resolvedValue = null;
this.then = function (nextOnResolved, nextOnRejected) {
onResolved = nextOnResolved;
onRejected = nextOnRejected;
if (resolvedValue !== null) {
onResolved(resolvedValue);
}
return this;
};
const resolve = value => {
resolvedValue = value;
if (onResolved !== null) {
onResolved(resolvedValue);
}
};
const reject = reason => {
if (onRejected !== null) {
onRejected(reason);
}
};
executor(resolve, reject);
}
module.exports = MyPromise;
3.4.3.3. Run the tests:
npm test
3.4.4. Error handling with catch
:
3.4.4.1. Write a test for error handling with catch
:
// my-promise.test.js
test('MyPromise should handle errors with catch', done => {
const promise = new MyPromise((_, reject) => {
reject('Error message');
});
promise.catch(reason => {
expect(reason).toBe('Error message');
done();
});
});
- 3.4.4.2. Implement error handling with
catch
:
// my-promise.js
function MyPromise(executor) {
// ...
this.catch = function (onRejected) {
return this.then(null, onRejected);
};
// ...
}
module.exports = MyPromise;
3.4.4.3. Run the tests:
npm test
3.4.5. MyPromise.resolve()
and MyPromise.reject()
static methods:
3.4.5.1. Write tests for MyPromise.resolve()
and MyPromise.reject()
:
// my-promise.test.js
test('MyPromise.resolve should return a resolved promise', done => {
const promise = MyPromise.resolve('Resolved value');
promise.then(value => {
expect(value).toBe('Resolved value');
done();
});
});
test('MyPromise.reject should return a rejected promise', done => {
const promise = MyPromise.reject('Rejected reason');
promise.catch(reason => {
expect(reason).toBe('Rejected reason');
done();
});
});
3.4.5.2. Implement MyPromise.resolve()
and MyPromise.reject()
static methods:
// my-promise.js
function MyPromise(executor) {
// ...
}
MyPromise.resolve = function (value) {
return new MyPromise(resolve => {
resolve(value);
});
};
MyPromise.reject = function (reason) {
return new MyPromise((_, reject) => {
reject(reason);
});
};
module.exports = MyPromise;
3.4.5.3. Run the tests:
npm test
3.4.6. MyPromise.all()
and MyPromise.race()
static methods:
3.4.6.1. Write tests for MyPromise.all()
and MyPromise.race()
:
// my-promise.test.js
test('MyPromise.all should resolve when all promises resolve', done => {
const promises = [
MyPromise.resolve('Value 1'),
MyPromise.resolve('Value 2')
];
MyPromise.all(promises).then(values => {
expect(values).toEqual(['Value 1', 'Value 2']);
done();
});
});
test('MyPromise.race should resolve or reject as soon as one promise resolves or rejects', done => {
const resolvePromise = new MyPromise(resolve => {
setTimeout(() => resolve('Resolved value'), 100);
});
const rejectPromise = new MyPromise((_, reject) => {
setTimeout(() => reject('Rejected reason'), 50);
});
MyPromise.race([resolvePromise, rejectPromise]).catch(reason => {
expect(reason).toBe('Rejected reason');
done();
});
});
3.4.6.2. Implement MyPromise.all()
and MyPromise.race()
static methods:
// my-promise.js
function MyPromise(executor) {
// ...
}
// ...
MyPromise.all = function (promises) {
return new MyPromise((resolve, reject) => {
const results = [];
let completed = 0;
promises.forEach((promise, index) => {
promise
.then(value => {
results[index] = value;
completed += 1;
if (completed === promises.length) {
resolve(results);
}
})
.catch(reject);
});
});
};
MyPromise.race = function (promises) {
return new MyPromise((resolve, reject) => {
promises.forEach(promise => {
promise.then(resolve).catch(reject);
});
});
};
module.exports = MyPromise;
3.4.6.3. Run the tests:
npm test
By following these steps and implementing the features listed in step 3.4, you will have built a Promise-like object using Test-Driven Development. Keep in mind that this implementation is a simplified version of JavaScript Promises, and some advanced features like proper error handling and performance optimizations are not included. However, this exercise should help you understand the core concepts and mechanics of Promises and TDD.