Dependency Injection is one of the important techniques to develop testable software. This technique which passes the dependency from outside of the class is I think basic technique to write tests and an extendable class. If a class instantiates the dependency directly by using new
keyword it’s not possible to inject a fake object into the target class that you want to write unit tests. By applying this technique, you can also loose the coupling to other components which is one of the good habits.
Code to make it better
The following example can often be my first implementation in order to check the basic implementation.
// Ability.ts
export enum Rank {
A = "A",
B = "B",
C = "C",
}
export class Ability {
public getAbility(): Rank {
const date = new Date();
const hours = date.getHours();
if (hours < 8) {
return Rank.C;
} else if (hours < 12) {
return Rank.A;
}
return Rank.B
}
}
// Person.ts
export class Person {
private ability = new Ability();
public calculatePoint(): number {
const rank = this.ability.getAbility();
const base = 5;
let additionalPoint = 0;
if (rank === Rank.A) {
additionalPoint = 10;
} else if (rank === Rank.B) {
additionalPoint = 5;
}
return base + additionalPoint;
}
}
// Person_spec.ts
describe("Person - original", () => {
let fakeTimer: sinon.SinonFakeTimers;
afterEach(() => {
fakeTimer.restore();
});
describe("calculatePoint", () => {
it("should return 15 when Rank is A", () => {
const fakeTime = new Date(2020, 10, 10, 9);
fakeTimer = sinon.useFakeTimers({ now: fakeTime });
const instance = new Person();
const result = instance.calculatePoint();
expect(result).to.equal(15);
});
});
});
But there is no seam to pass the dependency in this example which means you have to use an actual class that may require a complicated setup to get the desired value. Ability class is not complicated and we can write tests easily by using sinon.useFakeTimer()
but this code tests a combination of Ability and Person classes. If you modify the Ability class the test may fail even though you don’t make any changes to the Person class. We should somehow move the instantiation from this class to another class and pass the instance to the Person class. There are several ways to do this but the tequnique that I often apply is the following.
- Constructor Injection
- Setter Injection
- Factory Method
The factory Method may not be categorized as Dependency Injection but it can solve the same problem.
Constructor Injection
This is suitable when you need to determine which class to use according to the input value and you don’t have to change the dependent instance after creating the class (Person class here).
// Person.ts
export class Person {
// pass the ability from outside
constructor(private ability: Ability) { }
public calculatePoint(): number {
const rank = this.ability.getAbility();
const base = 5;
let additionalPoint = 0;
if (rank === Rank.A) {
additionalPoint = 10;
} else if (rank === Rank.B) {
additionalPoint = 5;
}
return base + additionalPoint;
}
}
// Person_spec.ts
describe("Person - constructor", () => {
let ability: Ability;
beforeEach(() => {
ability = new Ability();
})
describe("calculatePoint", () => {
it("should return 15 when Rank is A", () => {
sinon.stub(ability).getAbility.returns(Rank.A);
const instance = new Person(ability);
const result = instance.calculatePoint();
expect(result).to.equal(15);
});
});
});
As you can see, the Ability class is passed to the Person class in the test. The behavior of the Ability class is specified by sinon.stub
. So you can control the returned value. The test still succeeds even though the logic in the Ability class changes.
You can also set the initial value to the parameter. In this case, this parameter is used only in the unit tests.
export class Person {
constructor(private ability: Ability = new Ability()) { }
...
Setter Injection
This is suitable when you need to update the dependent instance while running the program. This way requires null check in each function because it is undefined unless the value is set via the setter. If it’s not necessary to update the instance I apply Constructor Injection because of the null check. You can of course set the initial value but setter injection has more risk than constructor injection because it can be updated anytime. If someone writes test code in production code and forgets to remove it, it could not work as expected.
Additionally, the unnecessary seam can cause a problem. For example, one developer assigns an instance somewhere in order to make sure that the instance is assigned before a function call whereas another developer has already assigned a different instance. In this case, some data stored in the dependent instance is discarded and the target function may not work as expected. It can happen when the developer has not been familiar with the project.
// Person.ts
export class Person {
private _ability?: Ability
// new instance can be assigned here
public set ability(value: Ability) {
this._ability = value;
}
public calculatePoint(): number {
if (!this._ability) {
throw new Error("ability instance is undefined.");
}
const rank = this._ability.getAbility();
const base = 5;
let additionalPoint = 0;
if (rank === Rank.A) {
additionalPoint = 10;
} else if (rank === Rank.B) {
additionalPoint = 5;
}
return base + additionalPoint;
}
}
// Person_spec.ts
describe("Person - setter", () => {
let ability: Ability;
beforeEach(() => {
ability = new Ability();
});
describe("calculatePoint", () => {
it("should throw an error when ability is undefined", () => {
sinon.stub(ability).getAbility.returns(Rank.A);
const instance = new Person();
const result = () => instance.calculatePoint();
expect(result).to.throw("ability instance is undefined")
});
it("should return 15 when Rank is A", () => {
sinon.stub(ability).getAbility.returns(Rank.A);
const instance = new Person();
instance.ability = ability;
const result = instance.calculatePoint();
expect(result).to.equal(15);
});
});
});
Factory Method
I often create factory classes because a caller doesn’t have to know which class to pass in many cases. In this example, the Ability class is only one class to get rank
value. The Person class knows which class to use but if the Person class instantiates the class, we go back to the starting point.
Therefore we should create a Factory class and return a new Ability instance in create function. By doing this, you can replace the actual implementation with a fake object by sinon
. You can also pass the config parameters from the constructor to the factory class if some parameters are necessary for setup.
The function should be in either a class or object since it is not possible to replace top level-exported function. To be precise, it’s possible but there are some points to know. If you are interested in that topic, check the following posts.
// AbilityFactory.ts
export class AbilityFactory {
public static create(): Ability {
return new Ability();
}
}
// Person.ts
export class Person {
private ability: Ability
constructor() {
this.ability = AbilityFactory.create();
}
public calculatePoint(): number {
const rank = this.ability.getAbility();
const base = 5;
let additionalPoint = 0;
if (rank === Rank.A) {
additionalPoint = 10;
} else if (rank === Rank.B) {
additionalPoint = 5;
}
return base + additionalPoint;
}
}
// Person_spec.ts
describe("Person - factory", () => {
let ability: Ability;
let stubFactory: sinon.SinonStub;
beforeEach(() => {
ability = new Ability();
// set a fake object here.
stubFactory = sinon.stub(AbilityFactory, "create");
stubFactory.returns(ability);
});
afterEach(() => {
stubFactory.restore();
});
describe("calculatePoint", () => {
it("should return 15 when Rank is A", () => {
sinon.stub(ability).getAbility.returns(Rank.A);
const instance = new Person();
const result = instance.calculatePoint();
expect(result).to.equal(15);
}); });
});
If you use a different language that can’t replace a static function behavior, this post can help you to split static class.
Other ways
DI container
I have a little experience with DI containers. I used MEF in C# when I didn’t know about clear architecture well. But I haven’t had any issues so far for DI without DI Container. I think using DI container requires learning cost and it is not intuitive. We can simply inject dependencies without it. But if I use it and learn something I will write something about it.
Parameter Injection
I’m not sure if the word exists. You can pass the dependency by specifying it in the function parameter. I’ve never used this technique but I guess it can be applied when calling the target function frequently with a different instance.
Conclusion
I explained 3 ways to inject dependency which I often apply.
- Constructor Injection
when you need to assign an instance only once - Setter Injection
when you need to assign an instance multiple times - Factory Method
when you need to assign an instance only once and the instance doesn’t need to be determined outside of the class.
The complete source code can be found here.
Comments