In a unit test, we need to replace a real behavior with a test object but the variable name is sometimes xxxxStub for mock and xxxxMock for stub. Stub and Mock are often mixed up and confused. Let’s recap the difference in this article and give a proper name to the variable.
Stub defines return value in advance
The function that we want to test has dependencies and it’s not always easy to use because
- the object might require a big preparation
- the object might try to connect somewhere that doesn’t exist in the unit tests
- the returned value of the function call changes from time to time
A test should not have any influence on other tests and the test result must always be the same. For that reason, we need to replace the object with a stub to control the behavior.
Stub defines a return value in advance and returns it once the stub function is called in the test function. Since it has no relationship to other modules, it can return the value quickly and control the result.
Let’s see the example code below.
// src\lib\typing-game\GameMonitor.ts
import { calculateScore } from "./ScoreCalculator";
import { SentenceGenerator } from "./SentenceGenerator";
import { promptUserInput } from "./UserInput";
export class GameMonitor {
constructor(private generator: SentenceGenerator) { }
public async start(): Promise<void> {
const text = this.generator.generate();
const startTime = Date.now();
console.log(text);
const userInput = await promptUserInput();
const elapsedTimeMs = Date.now() - startTime;
const elapsedTimeSec = elapsedTimeMs * 0.001;
console.log("----- Result -----");
console.log(text);
console.log(userInput);
const score = calculateScore({
original: text,
userInput,
time: elapsedTimeSec,
});
console.log(`\nYour score is: ${Math.trunc(score)}`);
console.log(`Time: ${elapsedTimeSec} (Sec)`);
}
}
If we want to write a test for this function in order to check if the score is shown correctly, we need to stub the following.
this.generator.generate()
: To control the text value and not to cause an errorpromptUserInput()
: It requires user input that takes seconds and a user inputs different values for the testcalculateScore
: To return a value like 12.45
If we use stub in TypeScript with sinon
module, the implementation looks like this.
describe("GameMonitor", () => {
let instance: GameMonitor;
let generator: SentenceGenerator;
before(() => {
use(chaiAsPromised);
use(sinonChai);
});
beforeEach(() => {
generator = new SentenceGenerator();
instance = new GameMonitor(generator);
});
describe("start", () => {
let consoleStub: sinon.SinonStub;
let generatorStub: sinon.SinonStub;
let userInputStub: sinon.SinonStub;
let nowStub: sinon.SinonStub;
let calcStub: sinon.SinonStub;
beforeEach(() => {
consoleStub = sinon.stub(console, "log");
generatorStub = sinon.stub(generator, "generate");
userInputStub = sinon.stub(UserInput, "promptUserInput");
nowStub = sinon.stub(Date, "now");
calcStub = sinon.stub(ScoreCalculator, "calculateScore");
});
afterEach(() => {
consoleStub.restore();
generatorStub.restore();
userInputStub.restore();
nowStub.restore();
calcStub.restore();
sinon.restore();
});
[12.45, 12.55].forEach((testValue) => {
it(`should show score 12 if the value is ${testValue}`, async () => {
generatorStub.returns("test text");
userInputStub.resolves("user input");
calcStub.returns(testValue);
await instance.start();
expect(consoleStub).to.be.calledWithMatch("Your score is: 12");
});
});
});
});
We can write unit tests by replacing the dependency with a stub even if we have don’t have the actual implementation.
Mock verifies the function call
Mock is used when we want to check if the target function is called with expected arguments for example. Mock has the same feature as stub which means that it can replace the actual behavior with test behavior if necessary.
When using Mock, it needs to define the expected result in advance and then call the verity
function.
describe("GameMonitor", () => {
let instance: GameMonitor;
let generator: SentenceGenerator;
before(() => {
use(chaiAsPromised);
use(sinonChai);
});
beforeEach(() => {
generator = new SentenceGenerator();
instance = new GameMonitor(generator);
});
describe("start", () => {
let consoleStub: sinon.SinonStub;
let generatorStub: sinon.SinonStub;
let userInputStub: sinon.SinonStub;
let nowStub: sinon.SinonStub;
let calcStub: sinon.SinonStub;
beforeEach(() => {
consoleStub = sinon.stub(console, "log");
generatorStub = sinon.stub(generator, "generate");
userInputStub = sinon.stub(UserInput, "promptUserInput");
nowStub = sinon.stub(Date, "now");
calcStub = sinon.stub(ScoreCalculator, "calculateScore");
});
afterEach(() => {
consoleStub.restore();
generatorStub.restore();
userInputStub.restore();
nowStub.restore();
calcStub.restore();
sinon.restore();
});
it("should pass original string, userInput and elapsedTime", async () => {
// sinon.mock throws an error without this because ScoreCalculator is already wrapped
calcStub.restore();
generatorStub.returns("test text");
userInputStub.resolves("user input");
nowStub.onFirstCall().returns(1000);
nowStub.onSecondCall().returns(3000);
const calcMock = sinon.mock(ScoreCalculator);
calcMock.expects("calculateScore").calledWith({
original: "test text",
userInput: "user input",
time: 2,
});
try {
await instance.start();
calcMock.verify();
} finally {
calcMock.restore();
}
});
});
});
Only one mock should be used in a single test. Don’t create two or more mocks. One test should test only one thing. Using two mocks means that the test case tests two things. If you don’t need to check the function call info, use stub instead.
Conclusion
I often use stub instead of mock because verification can be done without mock. The example in stub, I actually used stub to check if the function is called correctly. If it tests properly, it’s ok to use spy/stub instead of mock. However, if we can use the proper object depending on the test case, the intention is clear and the code is more readable for other developers.
Comments