- Parts of the code are under-tested
- Parts of the code are "hard to reason about"
- Parts of the code are slow
const state = {
counter: 123,
};
function increase(num = 1) {
state.counter += num;
}
function decrease(num = 1) {
state.counter -= num;
}
describe('Test Counter', () => {
it('works', () => {
increase(1);
expect(state.counter).toEqual(124);
});
it('can increment by 87', () => {
increase(87);
expect(state.counter).toEqual(210);
});
});
Now try to change the order of the it
functions. The test will break.
What are the issues?
- How to test
increment()
? The order ofit
calls matters. - Multiple functions work on the same state
Decouple the function from the state
function increase(state: State, num = 1) {
state.counter += num;
}
function decrease(state: State, num = 1) {
state.counter -= num;
}
describe('Test Counter', () => {
it('works', () => {
const state = { counter: 123 };
increment(state, 1);
expect(state.counter).toEqual(124);
});
it('can increment by 87', () => {
const state = { counter: 123 };
increment(state, 87);
expect(state.counter).toEqual(210);
});
});
We make the state immutable.
function increase(state: State, num = 1) {
return {
...state,
counter: state.counter + num,
};
}
function decrease(state: State, num = 1) {
return {
...state,
counter: state.counter + num,
};
}
describe('Test Counter', () => {
it('works', () => {
const state = { counter: 123 };
const newState = increment(state, 1);
expect(newState.counter).toEqual(124);
// The old state does not change
expect(state.counter).toEqual(123);
});
it('can increment by 87', () => {
const state = { counter: 123 };
const newState = increment(state, 87);
expect(newState.counter).toEqual(210);
// The old state does not change
expect(state.counter).toEqual(123);
});
});
const taxonomyExpensive = {
makes: [
{ id: 9, name: 'Audi' },
{ id: 13, name: 'BMW' },
{ id: 47, name: 'Mercedes-Benz' },
// ...
]
};
const findMakeExpensive = (taxonomy: Taxonomy, id: number) => {
const make = taxonomyExpensive.makes.find(m => m.id);
return make;
};
VS.
const taxonomyCheap = {
makes: new Map([
[9, { id: 9, name: 'Audi' }],
[13, { id: 13, name: 'BMW' }],
[47, { id: 47, name: 'Mercedes-Benz' }],
// ...
]),
};
const findMakeExpensive = (taxonomy: Taxonomy, id: number) => {
const make = taxonomy.makes.get(id);
return make;
};
What is the difference?
- Number of comparisions? The expensive find needs to compare each make's id with the one we are looking for.
- The more makes we have the more costly it gets.
- Lookup in a Map is O(1) => very cheap
Set().has is also O(1)
- We don't want to block the event loop => otherwise other operations need to wait
- Pagespeed
- Costs
function createSlug(str: string) { return 'lorem-ipsum-dolor'; }
const taxonomy = {
makes: new Map([
[9, { id: 9, name: 'Audi' }],
[13, { id: 13, name: 'BMW' }],
[47, { id: 47, name: 'Mercedes-Benz' }],
// ...
]),
};
function Component({ make }) {
return <a href={`/lst/${createSlug(make.name)}`}>{make.name}</a>
}
Every time the component runs we execute createSlug(make.name)
. But the result of this function never changes (it's a stateless function).
What could be better?
function createSlug(str: string) { return 'lorem-ipsum-dolor'; }
const taxonomy = {
makes: new Map([
[9, { id: 9, name: 'Audi', slug: createSlug('Audi'), }],
[13, { id: 13, name: 'BMW', createSlug('BMW'), }],
[47, { id: 47, name: 'Mercedes-Benz', createSlug('Mercedes-Benz') }],
// ...
]),
};
function Component({ make }) {
return <a href={`/lst/${make.slug}`}>{make.name}</a>
}
- We only create Taxonomy once, but we use it millions of times.
- With this pattern we only pay the price of
createSlug
once per make instead of once per rendering. - We also pay in advance (during application startup) and not while we need to be as fast as possible to deliver the response to the client.
import resolveUrlToLsApiQueryParams from '@/services/resolveUrlToLsApiQueryParams';
describe('resolveUrlToLsApiQueryParams', () => {
it('can resolve make, model, bodytype', () => {
const url = `/lst/auti/a3/bt_cabrio`;
const mockedTaxonomy = {};
const mockedGeoLocationService = {};
const queryParamsForLsApi = resolveUrlToLsApiQueryParams(url, mockedTaxonomy, mockedGeoLocationService);
expect(queryParamsForLsApi).toEqual({
atype: 'C',
mmmmv: '9|||',
body: 2,
});
});
it('can resolve make, model, city', () => {
const url = `/lst/auti/a3/city`;
const mockedTaxonomy = {};
const mockedGeoLocationService = {
async resolve(location) {
if (location === 'berlin') {
return {
zip: 'Berlin',
lon: '13.41144',
lat: '52.52343',
};
}
}
};
const queryParamsForLsApi = resolveUrlToLsApiQueryParams(url, mockedTaxonomy, mockedGeoLocationService);
expect(queryParamsForLsApi).toEqual({
mmmmv: '9|||',
body: 2,
zip: 'Berlin',
lon: '13.41144',
lat: '52.52343',
});
});
});