Graduate Program KB

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:
  1. Write a failing test
  2. Implement the minimal code to make the test pass
  3. 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() and MyPromise.reject() static methods
  • MyPromise.all() and MyPromise.race() static methods

Remember to follow the TDD process for each feature:

  1. Write a failing test
  2. Implement the minimal code to make the test pass
  3. 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.