In this TDD exercise, we will implement a selector factory similar to Redux's createSelector. We will use Jest for testing purposes. We will follow the TDD process and write one test at a time, then implement the feature to pass the test.
- First, let's write a test to ensure that our selector factory can accept input selectors and a result function:
// selectorFactory.test.js
import selectorFactory from './selectorFactory';
describe('selectorFactory', () => {
test('accepts input selectors and a result function', () => {
const inputSelector1 = state => state.input1;
const inputSelector2 = state => state.input2;
const resultFunction = (input1, input2) => input1 + input2;
const selector = selectorFactory(inputSelector1, inputSelector2, resultFunction);
expect(selector).toBeDefined();
});
});
- Next, let's implement the selectorFactory function to pass the test:
// selectorFactory.js
const selectorFactory = (...args) => {
const inputSelectors = args.slice(0, -1);
const resultFunction = args[args.length - 1];
const selector = state => {
const inputs = inputSelectors.map(inputSelector => inputSelector(state));
return resultFunction(...inputs);
};
return selector;
};
export default selectorFactory;
- Now, let's write a test to ensure that the selector returns the correct result:
// selectorFactory.test.js
test('selector returns the correct result', () => {
const inputSelector1 = state => state.input1;
const inputSelector2 = state => state.input2;
const resultFunction = (input1, input2) => input1 + input2;
const selector = selectorFactory(inputSelector1, inputSelector2, resultFunction);
const state = { input1: 2, input2: 3 };
expect(selector(state)).toEqual(5);
});
- Our current implementation should pass this test. Let's write a test for memoization:
// selectorFactory.test.js
test('selector memoizes the result function', () => {
const inputSelector1 = state => state.input1;
const inputSelector2 = state => state.input2;
const resultFunction = jest.fn((input1, input2) => input1 + input2);
const selector = selectorFactory(inputSelector1, inputSelector2, resultFunction);
const state = { input1: 2, input2: 3 };
selector(state);
selector(state);
expect(resultFunction).toHaveBeenCalledTimes(1);
});
- Now, let's implement memoization in our selectorFactory:
// selectorFactory.js
const selectorFactory = (...args) => {
const inputSelectors = args.slice(0, -1);
const resultFunction = args[args.length - 1];
let lastInputs = [];
let lastResult;
const selector = state => {
const inputs = inputSelectors.map(inputSelector => inputSelector(state));
if (inputs.length === lastInputs.length && inputs.every((input, index) => input === lastInputs[index])) {
return lastResult;
}
lastInputs = inputs;
lastResult = resultFunction(...inputs);
return lastResult;
};
return selector;
};
Now, our selectorFactory should pass all the tests, including memoization.
- Let's add a test to ensure that memoization works with multiple selectors created using the same selectorFactory:
// selectorFactory.test.js
test('memoization works with multiple selectors created using the same selectorFactory', () => {
const inputSelector1 = state => state.input1;
const inputSelector2 = state => state.input2;
const resultFunction = jest.fn((input1, input2) => input1 + input2);
const selector1 = selectorFactory(inputSelector1, inputSelector2, resultFunction);
const selector2 = selectorFactory(inputSelector1, inputSelector2, resultFunction);
const state1 = { input1: 2, input2: 3 };
const state2 = { input1: 4, input2: 5 };
selector1(state1);
selector1(state1);
selector2(state2);
selector2(state2);
expect(resultFunction).toHaveBeenCalledTimes(2);
});
Our current implementation should pass this test as well.
- Now let's write a test to check if the selector recalculates the result when the input values change:
// selectorFactory.test.js
test('selector recalculates the result when the input values change', () => {
const inputSelector1 = state => state.input1;
const inputSelector2 = state => state.input2;
const resultFunction = jest.fn((input1, input2) => input1 + input2);
const selector = selectorFactory(inputSelector1, inputSelector2, resultFunction);
const state1 = { input1: 2, input2: 3 };
const state2 = { input1: 4, input2: 5 };
selector(state1);
selector(state2);
expect(resultFunction).toHaveBeenCalledTimes(2);
});
- Our current implementation should pass this test as well, and we've covered the main features of createSelector. The final implementation of our selectorFactory is:
// selectorFactory.js
const selectorFactory = (...args) => {
const inputSelectors = args.slice(0, -1);
const resultFunction = args[args.length - 1];
let lastInputs = [];
let lastResult;
const selector = state => {
const inputs = inputSelectors.map(inputSelector => inputSelector(state));
if (inputs.length === lastInputs.length && inputs.every((input, index) => input === lastInputs[index])) {
return lastResult;
}
lastInputs = inputs;
lastResult = resultFunction(...inputs);
return lastResult;
};
return selector;
};
We'll now demonstrate how to compose selectors together using our selectorFactory
. Composing selectors allows us to create more complex and reusable selectors by combining simpler selectors. We'll follow the TDD process, writing one test at a time, and then implementing the feature to pass the test.
- Let's write a test to ensure that our selectors can be composed together:
// selectorFactory.test.js
test('selectors can be composed together', () => {
const inputSelector1 = state => state.input1;
const inputSelector2 = state => state.input2;
const composedResultFunction = (input1, input2) => input1 * input2;
const composedSelector = selectorFactory(inputSelector1, inputSelector2, composedResultFunction);
const inputSelector3 = state => state.input3;
const finalResultFunction = (composedResult, input3) => composedResult - input3;
const finalSelector = selectorFactory(composedSelector, inputSelector3, finalResultFunction);
const state = { input1: 2, input2: 3, input3: 4 };
expect(finalSelector(state)).toEqual(2);
});
- Our current implementation of the
selectorFactory
should pass this test, as it already supports composing selectors together:
// selectorFactory.js
const selectorFactory = (...args) => {
const inputSelectors = args.slice(0, -1);
const resultFunction = args[args.length - 1];
let lastInputs = [];
let lastResult;
const selector = state => {
const inputs = inputSelectors.map(inputSelector => inputSelector(state));
if (inputs.length === lastInputs.length && inputs.every((input, index) => input === lastInputs[index])) {
return lastResult;
}
lastInputs = inputs;
lastResult = resultFunction(...inputs);
return lastResult;
};
return selector;
};
- Now let's write a test to ensure that memoization works when selectors are composed:
// selectorFactory.test.js
test('memoization works with composed selectors', () => {
const inputSelector1 = state => state.input1;
const inputSelector2 = state => state.input2;
const composedResultFunction = jest.fn((input1, input2) => input1 * input2);
const composedSelector = selectorFactory(inputSelector1, inputSelector2, composedResultFunction);
const inputSelector3 = state => state.input3;
const finalResultFunction = jest.fn((composedResult, input3) => composedResult - input3);
const finalSelector = selectorFactory(composedSelector, inputSelector3, finalResultFunction);
const state = { input1: 2, input2: 3, input3: 4 };
finalSelector(state);
finalSelector(state);
expect(composedResultFunction).toHaveBeenCalledTimes(1);
expect(finalResultFunction).toHaveBeenCalledTimes(1);
});
- Our current implementation of the
selectorFactory
should pass this test as well, as it supports memoization even when selectors are composed together.
Now we have demonstrated that our selectorFactory
can be used to compose selectors together while preserving memoization functionality. The selectors can be combined to create more complex and reusable selectors from simpler ones, following the Test-Driven Development methodology.
Now we have a selectorFactory that is similar to Redux's createSelector.