In NgRx, how do you test a createSelector selector through its projector, and what does a projector test leave unproven?
answer
- the last argument to createSelector
- plain inputs, no state tree
- the inputs are not exercised
- wiring needs a whole-state call
- projector caches its last call too
basics
~10 sCall selector.projector(...) with plain input values to test the projection logic alone; it never runs the input selectors, so a whole-state call is still needed to prove the selector reads the right slices.
solid answer
~40 sEvery selector built with `createSelector` exposes `.projector`, the projection function you passed last, memoized. For `selectCartTotal = createSelector(selectCartItems, selectCouponPercentOff, (items, percentOff) => …)` the test calls `selectCartTotal.projector(items, 10)` and asserts the number, with no store and no state tree. That is the right place to cover rounding, empty carts and 100% coupons. What it cannot prove is the **wiring**: that the input selectors read the right feature key and arrive in the right order. For that I add one test that calls `selectCartTotal(state)` with a small but realistic state object. Two details: the projector keeps a last-arguments cache like the selector, so reusing a mutated input array returns a stale result; and an `overrideSelector` value set on the selector does not affect `.projector`.
code
ts · 21 linesimport { selectCartTotal } from './cart.selectors';
const items = () => [
{ sku: 'mug', price: 20, qty: 2 },
{ sku: 'tea', price: 10, qty: 1 },
];
describe('selectCartTotal', () => {
it('applies the coupon to the subtotal (projector)', () => {
expect(selectCartTotal.projector(items(), 10)).toBe(45);
expect(selectCartTotal.projector([], 10)).toBe(0);
});
it('reads the cart and coupon slices (whole selector)', () => {
const state = {
cart: { items: items() },
coupons: { code: 'SAVE10', percentOff: 10 },
};
expect(selectCartTotal(state)).toBe(45);
});
});go deeper
Recall that a selector made with createSelector has a projector member you can call with plain input values.
Explain what the projector runs and what it skips, and pair projector cases with one whole-state test for the input wiring.
Know the projector's own last-arguments cache and that setResult overrides do not touch it; keep selector tests isolated from component-test overrides.
Decide how selector logic is split and tested across a large state tree, so derivation bugs are caught cheaply and wiring bugs are caught at all.
## What the projector is In NgRx, `createSelector(input1, input2, …, projector)` builds a **memoized selector**: a function from the whole state to a derived value. The **input selectors** pull pieces out of the state; the **projection function** (the last argument) combines them. The returned selector object carries extra members, typed on `MemoizedSelector`: `projector`, `release()`, `setResult()` and `clearResult()`. `projector` is the projection function wrapped in its own memoization. Calling it runs only your combination logic, with arguments you supply directly. The input selectors are not called at all. ## Testing the logic with plain inputs For a checkout feature: ```ts export const selectCartTotal = createSelector( selectCartItems, selectCouponPercentOff, (items, percentOff) => { const subtotal = items.reduce((sum, i) => sum + i.price * i.qty, 0); return Math.round(subtotal * (100 - percentOff)) / 100; }, ); ``` A projector test passes an items array and a percentage and checks the total. That is where the logic cases belong: - an empty cart gives `0`; - a 10% coupon on a 50.00 subtotal gives `45`; - a 100% coupon gives `0`, not a negative number; - fractional prices round to cents. These tests need no `MockStore`, no `TestBed` and no knowledge of the state shape. They are as cheap as reducer tests. ## What the projector test leaves unproven Because `projector` skips the input selectors, several real bugs pass it: 1. An input selector reads the **wrong feature key**, for example `createFeatureSelector('carts')` when the slice is registered as `'cart'`. 2. The inputs are passed in the **wrong order**; the projector test calls the function the way you think it is wired, not the way it is. 3. An input selector returns the **wrong shape**, such as an entity dictionary where the projector expects an array. The remedy is one or two **whole-selector tests**: call `selectCartTotal(state)` with a small state object shaped like the real root state. Those run the full chain. A sensible split is many projector cases for the logic, and one whole-state case per selector for the wiring. ## Memoization inside the projector | Member | What it runs | Cached? | Affected by `setResult`? | |---|---|---|---| | `selectCartTotal(state)` | inputs then projection | yes, on the state argument | yes | | `selectCartTotal.projector(a, b)` | projection only | yes, on its own arguments | no | Two consequences for tests: - The projector remembers its last arguments and compares them by reference. If a test mutates an items array in place and calls `projector` again with the same array and the same percentage, it gets the previous result back. Build a new array per case. - An `overrideSelector` or `setResult` value lives on the outer selector function, so it does not change what `projector` returns. A projector test stays honest even in a file where a component test pinned the selector. `release()` clears the selector's cached arguments and results, including its projector's, and releases its memoized input selectors. MockStore's `resetSelectors()` calls it for every selector it overrode. ## Where this sits among NgRx tests - Reducer tests prove state transitions. - Projector tests prove derivation logic. - Whole-state selector tests prove the derivation reads the right state. - Component tests pin selectors with a mock store and do not re-prove any of the above. ## Interview angle Interviewers ask about projectors to see whether a candidate knows what each selector test layer proves. A strong answer names three things: 1. The projector is the fastest place to test **logic**, because it takes plain values. 2. It is blind to **wiring**, so at least one test must call the selector with a state object. 3. It is **memoized**, so tests should build fresh inputs and not rely on in-place mutation. A weak answer treats `projector` as a way to test "the selector" in general, or builds a full root state for every edge case and ends up with slow, brittle fixtures. Another weak signal is mocking the input selectors themselves with a mock store just to test arithmetic: the projector already isolates the arithmetic, with no store at all. Factory selectors (a function that takes a parameter and returns `createSelector(...)`) are tested the same way: call the factory with the parameter, then test the returned selector's `projector` and one whole-state call.
- A projector test mutates the items array and calls the NgRx projector again; the result does not change. Why?`projector` is the projection wrapped in NgRx's default memoization, which keeps the last arguments and compares them by reference. The same array and the same percentage count as unchanged arguments, so it returns the cached result without re-running. Build a fresh array for each case, or call `release()` on the selector between cases.
- Is a projector test still needed when a whole-state selector test exists?Usually yes, for coverage cost. Edge cases such as empty carts, rounding and full discounts are cheaper to express as plain arguments than as state trees. Keep one whole-state test per selector for wiring, and put the many logic cases on the projector.
saying these in an interview costs you the question
- A projector test proves the selector reads the right slice of state.
- You must build the full root state to test any NgRx selector.
- projector is the raw function, so it never returns a cached result.
- Selector projector tests need provideMockStore to run.
- An overrideSelector value also changes what the projector returns.