liveBook · Manning

Abstraction

As explained in The Art of Unit Testing 3e

A simplified boundary that hides implementation details while exposing a controlled interface to underlying behavior or dependencies.

An abstraction hides a dependency's detailed API behind a smaller interface that your code controls. Instead of importing a third-party logger or date library everywhere, centralize that dependency behind an adapter. This limits the damage when the external API changes and keeps tests focused on your own interface. section 5.2.2 section 3.5

Prefer an abstraction when direct dependencies would spread implementation details across the codebase:

  • Direct imports couple production code and tests to a third-party API, so an upgrade can require changing many files. section 5.2.2
  • An internal API provides a stable boundary: its implementation can change while callers and tests remain unchanged. section 3.5
  • Do not confuse abstraction level with test type; unit and component tests are classified by their dependencies, not by how high-level they appear. section 10.1.2

Wrapping a date dependency

A password verifier can depend on a controlled `dependencies.moment` interface instead of importing `moment` directly. Tests then inject a tiny fake whose `day()` method returns Saturday, verify the weekend error, and call the returned reset function afterward. The fake models only the behavior the verifier needs. section 3.5

Listing 3.7 Abstracting the required dependencies
const originalDependencies = {                        #1
    moment: require(‘moment’),                        #1
};                                                    #1
 
let dependencies = { ...originalDependencies };       #2
 
const inject = (fakes) => {                           #3
    Object.assign(dependencies, fakes);
    return function reset() {                         #4
        dependencies = { ...originalDependencies };
    }
};
 
const SUNDAY = 0; const SATURDAY = 6;
 
const verifyPassword = (input, rules) => {
    const dayOfWeek = dependencies.moment().day();
    if ([SATURDAY, SUNDAY].includes(dayOfWeek)) {
        throw Error("It's the weekend!");
    }
    // more code goes here...
    // return list of errors found..
    return [];
};
 
module.exports = {
    SATURDAY,
    verifyPassword,
    inject
};
#1 Wrapping moment.js with an intermediary object
#2 The object containing the current dependency, either real or fake
#3 A function that replaces the real dependency with a fake one
#4 A function that resets the dependency back to the real one

Shows the abstraction boundary around `moment`, including dependency storage, replacement through `inject`, and restoration through `reset`. section 3.5

Listing 3.8 Injecting a fake module with inject()
const { inject, verifyPassword, SATURDAY } = require('./password-verifier-time00-modular');
 
const injectDate = (newDay) => {                    #1
    const reset = inject({                          #2
        moment: function () {
            //we're faking the moment.js module's API here.
            return {
                day: () => newDay
            }
        }
    });
    return reset;
};
 
describe('verifyPassword', () => {
    describe('when its the weekend', () => {
        it('throws an error', () => {
            const reset = injectDate(SATURDAY);     #3
 
            expect(() => verifyPassword('any input'))
                .toThrow("It's the weekend!");
 
            reset();                                #4
        });
    });
});
#1 A helper function
#2 Injecting a fake API instead of moment.js
#3 Providing a fake day
#4 Resetting the dependency

Shows a test injecting a fake date API rather than faking the entire `moment.js` module, then restoring the original dependency. section 3.5

How The Art of Unit Testing 3e covers it

How deep it goes
Demonstrates adapters, injected fakes, and reset functions, without detailing broader architectural implementation.
Examples
Uses logging through an adapter and date checking through an injected `moment` fake.
What it assumes
Assumes JavaScript modules, third-party APIs, and basic test doubles; teaches dependency boundaries through small examples.
What you can do after
Apply controlled adapters and injected fakes to reduce upgrade risk and make dependency-focused tests less brittle.
How it connects
Connects abstraction with dependency injection, Ports and Adapters architecture, Hexagonal architecture, Onion architecture, and test maintainability.

Also explained in

Compare how 15 books explain abstraction →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of The Art of Unit Testing 3e. The section links point to the parts of the book it draws on.