Unit test for singleton class

Singleton class problem in terms of tests

Do you write tests for singleton class too? If the logic is simple enough the test is also simple and easy to write. The class may not require dependency injection and it always returns the same result. But what if the class requires dependency or returns the different result depending on call count or something like that? Maybe we can write some tests but the tests are maybe not isolated because the singleton instance has static members. If each First implementation is following.

// FirstSingleton.ts
export class FirstSingleton {
private static _instance?: FirstSingleton;
public static get instance(): FirstSingleton {
if (!this._instance) {
this._instance = new FirstSingleton();
}
return this._instance;
}

private constructor() { };

private callCount = 0;
public greet(): string {
this.callCount++;
if (this.callCount === 1) {
return "hello";
} else if (this.callCount === 2) {
return "how are you";
}
return "see you again";
}
}
// FirstSingleton_spec.ts
describe("FirstSingleton", () => {
describe("greet", () => {
it("should return hello for the first call", () => {
const result = FirstSingleton.instance.greet();
expect(result).to.equal("hello");
});
it("should return how are you for the second call", () => {
FirstSingleton.instance.greet();
const result = FirstSingleton.instance.greet();
expect(result).to.equal("how are you"); // error!! -> result is see you again
});
it("should return see you again for the third call", () => {
FirstSingleton.instance.greet();
FirstSingleton.instance.greet();
const result = FirstSingleton.instance.greet();
expect(result).to.equal("see you again");
});
});
});

If we want to turn these tests green we need to call greet function only once in each test. However, if we run only the second test it fails because it depends on preceding tests. We should somehow get rid of the dependency.

How to write singleton class

I try not to create Singleton class because it is not easy to write tests. But singleton class is sometimes necessary. We can easily write tests if the class is not a singleton. Let’s separate the singleton class into two classes. One is for handling singleton instances and another is for logic. The second implementation is the following.

// SecondSingleton.ts
export class SingletonHolder {
private static _instance?: SecondSingleton;
public static get instance(): SecondSingleton {
if (!this._instance) {
this._instance = new SecondSingleton();
}
return this._instance;
}

private constructor() { };
}

export class SecondSingleton {
private callCount = 0;
public greet(): string {
this.callCount++;
if (this.callCount === 1) {
return "hello";
} else if (this.callCount === 2) {
return "how are you";
}
return "see you again";
}
}
// SecondSingleton_spec.ts
describe("SecondSingleton", () => {
let instance: SecondSingleton;
beforeEach(() => {
instance = new SecondSingleton();
});

describe("greet", () => {
it("should return hello for the first call", () => {
const result = instance.greet();
expect(result).to.equal("hello");
});
it("should return how are you for the second call", () => {
instance.greet();
const result = instance.greet();
expect(result).to.equal("how are you");
});
it("should return see you again for the third call", () => {
instance.greet();
instance.greet();
const result = instance.greet();
expect(result).to.equal("see you again");
});
});
});

You can now easily write tests for both the main logic and singleton logic. This separation makes the code better because each class has single responsibility whereas FirstSingleton class had two responsibilities.

Cheating in Typescript

In typescript, actually, we don’t necessarily have to separate the class to write the test in this example. We can cheat for it in the following way.

describe("greet - cheat", () => {
beforeEach(() => {
(FirstSingleton.instance as any).callCount = 0;
});

it("should return hello for the first call", () => {
const result = FirstSingleton.instance.greet();
expect(result).to.equal("hello");
});
it("should return how are you for the second call", () => {
FirstSingleton.instance.greet();
const result = FirstSingleton.instance.greet();
expect(result).to.equal("how are you");
});
it("should return see you again for the third call", () => {
FirstSingleton.instance.greet();
FirstSingleton.instance.greet();
const result = FirstSingleton.instance.greet();
expect(result).to.equal("see you again");
});
});

Conslusion

The main point here is to separate the main logic from the singleton holder class. By following this way, you can easily write tests without thinking singleton.

Complete source code can be found here

BlogPost/src/WhenShouldWeUseForWhileForEach at master · yuto-yuto/BlogPost
Contribute to yuto-yuto/BlogPost development by creating an account on GitHub.