Reading notes by Benjamin Fan
# CHAPTER 1
Dependency:
Services/components that you can't fully control in the SUT include loggers, file access, network interaction, and DB access.
Source: The Art of Unit Testing p.62 Figure 3.1
# CHAPTER 1
# CHAPTER 1
# CHAPTER 1
// in main.ts
import * as core from '@actions/core';
async function run() {
const defaultBranch = await octokit.rest.repos.get({
owner: targetOwner,
repo: targetRepo,
headers: {
accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
},
});
// use the defaultBranch to do something
}
# CHAPTER 2
Breaks the incoming dependency.
# CHAPTER 2
import * as core from '@actions/core';
async function run() {
const defaultBranch = await octokit.rest.repos.get({
owner: targetOwner,
repo: targetRepo,
headers: {
accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
},
});
// use the defaultBranch to do something
}
What if you want to test without the internet?
What if you want to test without a GitHub API token?
# CHAPTER 3
There are three ways to adopt stubs in your unit tests. Some of these require more refactoring of your production code.
Functional approaches
Modular approach
Object-oriented approaches
# CHAPTER 3
# CHAPTER 3
async function filterByDate(branches: any, expiryDays: int) {
const one_day = 1000 * 60 * 60 * 24;
const latest_commit_date = new Date (
branch.data.commit.commit.committer?.date ?? '',
);
const result = Math.round(
(new Date() -
latest_commit_date.setDate(
latest_commit_date.getDate() + expiryDays,
)) /
one_day,
);
return result >= 0;
}
What if you want to test the expiry scenario?
# CHAPTER 3
async function filterByDate(branches: any, expiryDays: int, present_date: Date) {
const one_day = 1000 * 60 * 60 * 24;
const latest_commit_date = new Date (
branch.data.commit.commit.committer?.date ?? '',
);
const result = Math.round(
(present_date -
latest_commit_date.setDate(
latest_commit_date.getDate() + expiryDays,
)) /
one_day,
);
return result >= 0;
}
it.each(branchDetail)('Branch is expiry', async branchDetail => {
const present_date = new Date(Date.UTC(2021, 1, 1, 0, 00, 00));
const result = await filterByDate(branchDetail, 10, present_data);
expect(result).toBe(false);
})
# CHAPTER 3
async function filterByDate(branches: any, expiryDays: int, getDayFn: Function) {
const one_day = 1000 * 60 * 60 * 24;
const latest_commit_date = new Date (
branch.data.commit.commit.committer?.date ?? '',
);
const present_date = getDayFn();
const result = Math.round(
(present_date -
latest_commit_date.setDate(
latest_commit_date.getDate() + expiryDays,
)) /
one_day,
);
return result >= 0;
}
it.each(branchDetail)('Branch is expiry', async branchDetail => {
const present_date = () => new Date(Date.UTC(2021, 1, 1, 0, 00, 00));
const result = await filterByDate(branchDetail, 10, present_data);
expect(result).toBe(false);
})
# CHAPTER 3
async function filterByDate(branches: any, expiryDays: int) {
return function(getDayFn: Function) {
const one_day = 1000 * 60 * 60 * 24;
const latest_commit_date = new Date (
branch.data.commit.commit.committer?.date ?? '',
);
const present_date = getDayFn();
const result = Math.round(
(present_date -
latest_commit_date.setDate(
latest_commit_date.getDate() + expiryDays,
)) /
one_day,);
return result >= 0;
}}
it.each(branchDetail)('Branch is expiry', async branchDetail => {
const filterFn = await filterByDate(branchDetail, 10);
const present_date = () => new Date(Date.UTC(2021, 1, 1, 0, 00, 00));
const result = await filterFn(present_data);
expect(result).toBe(false);
})
# CHAPTER 3
const moment = require('moment');
const SUNDAY = 0; const SATURDAY = 6;
const verifyPassword = (input, rules) => {
const dayOfWeek = moment().day();
if ([SATURDAY, SUNDAY].includes(dayOfWeek)) {
throw Error("It's the weekend!");
}
// more code goes here...
// return list of errors found..
return [];
};
This is the sample code used here
This code will verify whether today is a weekend
# CHAPTER 3
const originalDependencies = {
moment: require(‘moment’),
};
let dependencies = { ...originalDependencies };
const inject = (fakes) => {
Object.assign(dependencies, fakes);
return function reset() {
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!");
}
//...
return [];
}
module.exports = {
SATURDAY,
verifyPassword,
inject
};
# CHAPTER 3
const { inject, verifyPassword, SATURDAY } = require('./password-verifier-
time00-modular');
const injectDate = (newDay) => {
const reset = inject({
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);
expect(() => verifyPassword('any input'))
.toThrow("It's the weekend!");
reset();
});
});
});
# CHAPTER 3
class PasswordVerifier {
constructor(rules, dayOfWeekFn) {
this.rules = rules;
this.dayOfWeek = dayOfWeekFn;
}
verify(input) {
if ([SATURDAY, SUNDAY].includes(this.dayOfWeek())) {
throw new Error("It's the weekend!");
}
const errors = [];
//more code goes here..
return errors;
};
}
test('class constructor: on weekends, throws exception', () => {
const alwaysSunday = () => SUNDAY;
const verifier = new PasswordVerifier([], alwaysSunday);
expect(() => verifier.verify('anything'))
.toThrow("It's the weekend!");
});
# CHAPTER 3
import moment from "moment";
const RealTimeProvider = () => {
this.getDay = () => moment().day()
};
const SUNDAY = 0, MONDAY=1, SATURDAY = 6;
class PasswordVerifier {
constructor(rules, timeProvider) {
this.rules = rules;
this.timeProvider = timeProvider;
}
verify(input) {
if ([SATURDAY, SUNDAY].includes(this.timeProvider.getDay())) {
throw new Error("It's the weekend!");
}
const errors = [];
//more code goes here..
return errors;
};
}
# CHAPTER 3
function FakeTimeProvider(fakeDay) {
this.getDay = function () {
return fakeDay;
}
}
describe('verifier', () => {
test('class constructor: on weekends, throws exception', () => {
const verifier =
new PasswordVerifier([], new FakeTimeProvider(SUNDAY));
expect(() => verifier.verify('anything'))
.toThrow("It's the weekend!");
});
});
# CHAPTER 3
export interface TimeProviderInterface { getDay(): number; }
import * as moment from "moment";
import {TimeProviderInterface} from "./time-provider-interface";
export class RealTimeProvider implements TimeProviderInterface {
getDay(): number {
return moment().day();
}
}
export class PasswordVerifier {
private _timeProvider: TimeProviderInterface;
constructor(rules: any[], timeProvider: TimeProviderInterface) {
this._timeProvider = timeProvider;
}
verify(input: string):string[] {
const isWeekened = [SUNDAY, SATURDAY]
.filter(x => x === this._timeProvider.getDay())
.length > 0;
if (isWeekened) {throw new Error("It's the weekend!")}
// more logic goes here
return [];
}}
# CHAPTER 3
class FakeTimeProvider implements TimeProviderInterface{
fakeDay: number;
getDay(): number {
return this.fakeDay;
}
}
describe('password verifier with interfaces', () => {
test('on weekends, throws exceptions', () => {
const stubTimeProvider = new FakeTimeProvider();
stubTimeProvider.fakeDay = SUNDAY;
const verifier = new PasswordVerifier([], stubTimeProvider);
expect(() => verifier.verify('anything'))
.toThrow("It's the weekend!");
});
});
# CHAPTER 4
Two types to break the dependencies: mocks and stubs |
---|
Mocks break outgoing dependencies |
Stubs break incoming dependencies |
Avoid using modular injection |
Object as parameter (aka duck typing) like Golang style |
Common interface as parameter - same as object as parameter |