This is 4th post in Become a better and decent programmer
series.
In the first post, I explained how to extract logic in a big function into small functions. The goal was to have small and readable functions.
In the second post, I explained how to introduce unit test and move the functions into another class. The scale was bigger scope than the first post because we needed to consider the relationships between the classes.
In the third post, I explains how to reduce/remove switch-case clause for keeping complexity and make it more testable.
But there are some classes which are not testable because they use console output function. I will explain ways how to replace static function and inject fake object for test.
You can download the complete source code here
The final code is in src/shopping-app/ver7-1, ver7-2 and ver7-3
If you haven’t checked the last post, please go there first.
How to make a static function testable
We refactored the code in the last post. Shop class and command classes are short enough and readable. We can write unit test for them but not for some classes which call console output. Since this application is console application its output is one of important things to test. In Typescript/Javascript, it may be possible to replace console.log
without refactoring but some languages may not allow us to do that in the current implementation.
I will show following two ways.
- Using static function wrapped by a class
- Dependency injection
Using static function wrapped by a class
You can find the complete code here (ver7-1)
First way is to define a static function in a class. I’m not sure whether we can replace the function in a unit test in other languages but we can do it in Typescript with sinon, for example. We don’t have to refactor the code in this way because we can call the function directly everywhere. By the way, top class function, which isn’t defined in a class, cannot be replaced with stub/mock/fake object in Typescript as far as I know. The code looks like this.
export class StaticConsole {
public static log(...args: any[]): void {
console.log(args);
}
public static error(...args: any[]): void {
console.error(args);
}
}
export class AddCommand extends ArgsCommandBase<AddCommandArgs> {
...
protected process(args: AddCommandArgs): void {
if (!(Object.values(ItemName) as string[]).includes(args.itemName)) {
// console.log(`${args.itemName} doesn't exist.`);
StaticConsole.log(`${args.itemName} doesn't exist.`);
return;
}
this.shoppingCart.addItem(args.itemName as ItemName, args.numberOfItems);
}
...
}
It’s very simple. console.log
is replaced with StaticConsole.log
in AddCommand.process
function. Then, see the unit test.
describe("AddCommand", () => {
let shoppingCart: ShoppingCart;
let command: AddCommand;
let cartStub: sinon.SinonStub;
let consoleStub: sinon.SinonStub;
beforeEach(() => {
shoppingCart = new ShoppingCart();
cartStub = sinon.stub(shoppingCart, "addItem");
command = new AddCommand(shoppingCart);
consoleStub = sinon.stub(StaticConsole, "log");
});
afterEach(() => {
cartStub.restore();
consoleStub.restore();
})
describe("execute", () => {
it("should not call addItem when specified item doesn't exist", () => {
command.execute(["Table", "1"]);
expect(cartStub.notCalled).to.be.true;
});
it("should output message when specified item doesn't exist", () => {
command.execute(["Table", "1"]);
expect(consoleStub.calledWith("Table doesn't exist.")).to.be.true;
});
});
});
First test is what I wrote in the previous post. You may think We can remove it now because what we want to test is whether the function displays the corresponding message. However, I want to keep it because addItem function may be called if someone else removes return keyword for some reason in the future. This test case detects such a coding error. It is useful, isn’t it?
For the new test, the StaticConsole.log
function is replaced with stub function in beforeEach
function and restored in afterEach
. Don’t forget to restore it. Since it’s static function its change can break other tests if you don’t restore it although this case doesn’t break other tests if other tests don’t stub/mock/spy it. Sinon complains if the same object is about to be stubbed/mocked/spied twice.
This technique is useful when we want to use static or top level function provided by a imported module. Use this technique if it is not possible to replace the target function called in the function which you want to test. This way works like a proxy.
Dependency injection
You can find the complete code here (ver7-2)
You may have heard of Dependency injection. I think this is classic way to make it testable and extensible. We need define interface for console and implement it.
export interface MyConsole {
log(message?: any, ...optionalParams: any[]): void;
error(message?: any, ...optionalParams: any[]): void;
}
export class ShoppingConsole implements MyConsole {
public log(message?: any, ...optionalParams: any[]): void {
console.log(message, ...optionalParams);
}
public error(message?: any, ...optionalParams: any[]): void {
console.error(message, ...optionalParams);
}
}
Console
interface is already defined in Node.js but it has about 20 functions. I defined MyConsole
interface since what we need to implement is only 2 functions. Call the actual console functions in ShoppinConsole
which are used in production mode. Create the instance in app.ts and pass it to Shop class like this below.
// app.ts
const shoppingConsole = new ShoppingConsole();
let shop = new Shop(shoppingConsole);
shop.run();
// Shop.ts
export class Shop {
private shoppingCart = new ShoppingCart();
private commandHolder: CommandHolder;
constructor(private shoppingConsole: ShoppingConsole) {
this.commandHolder = new CommandHolder(
this.shoppingCart,
this.shoppingConsole,
);
}
public run() {
this.shoppingConsole.log("Welcome to special shop. This is what you can do.");
...
}
}
Since command classes use the console it is passed to the CommandHolder
class too. In the constructor we can pass the same instance to the all command classes.
export class CommandHolder {
private commands: Map<CommandName | string, Command>;
private undefinedCommand: Command;
constructor(shoppingCart: ShoppingCart, shoppingConsole: ShoppingConsole) {
this.commands = new Map();
this.commands.set(CommandName.Command, new DisplayCommand(shoppingConsole));
this.commands.set(CommandName.List, new ListCommand(shoppingConsole));
this.commands.set(CommandName.Add, new AddCommand(shoppingCart, shoppingConsole));
this.commands.set(CommandName.Remove, new RemoveCommand(shoppingCart));
this.commands.set(CommandName.Cart, new ShowItemsCommand(shoppingCart, shoppingConsole));
this.commands.set(CommandName.Pay, new PayCommand(shoppingCart, shoppingConsole));
this.commands.set(CommandName.Exit, new ExitCommand(shoppingConsole));
this.undefinedCommand = new UndefinedCommand(shoppingConsole);
}
...
I don’t show command classes implementation here because I just added an argument and replace console.log
with this.shoppingConsole.log
. That’s all. It is easy. Let’s see the unit test.
describe("AddCommand", () => {
let shoppingCart: ShoppingCart;
let shoppingConsole: ShoppingConsole;
let command: AddCommand;
let cartStub: sinon.SinonStub;
let consoleStub: sinon.SinonStub;
beforeEach(() => {
shoppingCart = new ShoppingCart();
cartStub = sinon.stub(shoppingCart, "addItem");
shoppingConsole = new ShoppingConsole();
consoleStub = sinon.stub(shoppingConsole, "log");
command = new AddCommand(shoppingCart, shoppingConsole);
});
afterEach(() => {
cartStub.restore();
consoleStub.restore();
});
describe("execute", () => {
...
it("should not call addItem when specified item doesn't exist", () => {
command.execute(["Table", "1"]);
expect(cartStub.notCalled).to.be.true;
});
it("should output message when specified item doesn't exist", () => {
command.execute(["Table", "1"]);
expect(consoleStub.calledWith("Table doesn't exist.")).to.be.true;
});
...
});
The unit test looks almost the same as using StaticConsole. It is possible to replace actual behavior at runtime in Typescript/Javascript but not for some languages. In this case, we need to create a stub object in preparation step and pass the object to the test class.
it("should pass test without using sinon", () => {
const testConsole: MyConsole = {
log: (message?: any, ...optionalParams: any[]) => {
expect(message).to.equal("Table doesn't exist.");
},
error: (message?: any, ...optionalParams: any[]) => { },
};
const testClass = new AddCommand({ shoppingCart, shoppingConsole: testConsole });
testClass.execute(["Table", "1"]);
});
AddCommand class calls testConsole.log
function in execute function and it checks the specified argument in it. This technique can be applied to other languages.
Variety of dependency injection
In the previous section, we used dependency injection. When a class needs a dependency we somehow need to pass it to the class. There are several ways for it. Let’s have a look at other techniques for dependency injection. By the way, Dependency Injection is often abbreviated to DI. Some techniques may not be called DI but I explain them as DI because its goal is the same.
- Instantiate it in the class
- Pass the instance from Constructor argument (Constructor injection)
- Pass the instance from setter (Setter injection)
- Call a factory function in the class
- Using DI Container
- Creating subclass to expose target members
Instantiate it in the class
This is start point and not DI. Foo is strongly dependent to Hoge class. I propose to use Call a factory function instead because this is not testable.
class Foo {
private hoge: Hoge = new Hoge();
...
}
Pass the instance from Constructor argument (Constructor injection)
Apply this technique when Foo doesn’t need another instance in the life time. This is testable.
class Foo {
constructor(private hoge: Hoge){}
...
}
const hoge = new Hoge();
const foo = new Foo(hoge);
Pass the instance from setter (Setter injection)
Apply this technique when Foo needs to use different instance according to specific case. For example, when Foo wants to use Hoge like a plug-in. This is also testable.
class Foo {
private hoge: Hoge;
public set hoge(value: Hoge){
this.hoge = value;
}
...
}
const hoge = new Hoge();
const foo = new Foo();
foo.hoge = hoge;
Call a factory function
Apply this technique if constructor/setter injection makes code complex or if you don’t want to let a client class have the target class instance. This is also testable because create function can be replaced with stub object by sinon in Typescript.
class HogeFactory{
public static create(): Hoge{
return new Hoge();
}
}
class Foo {
private hoge: Hoge = HogeFactory.create();
...
}
Using DI Container
I don’t show the example of DI Container because I don’t have enough experience with it. You can easily find it if you google it. However, I prefer another ways because it looks complicated and it requires learning the framework to use it! If we change the framework or programming language we need to learn another framework again. I think constructor/setter injection is more readable and easy to stick.
Creating subclass to expose target members
Following way can be another option if all of them don’t fit to your case. Steps are following.
- Change the access modifier from private to protected
- Create a new class which extends the class which you want to test
- Define the same member as public
- Access the target member in a test via the public member in the extended class
// original class which we want to test
class Foo {
protected hoge: Hoge = new Hoge();
...
}
// create the extended class in test directory
class TestableFoo extends Foo {
// change the access modifier to public
public hoge: Hoge;
// this is another way to expose the target member
public get hogeExposed(): Hoge{
return super.hoge;
}
public set hogeExposed(value: Hoge){
super.hoge = value;
}
}
When we use this TestableFoo instance instead of Foo class we can easily write unit tests for Foo class. If you don’t use any libraries for writing unit test like sinon you can use setter to assign fake object.
How to choose suitable technique
It may be hard which one to choose at the beginning. I wrote down suitable questions here for you. Check these questions when you get lost.
- Do you need to replace the object to test?
- Yes: Go to next question
- No: Instantiate it in the class
- Do you need to reassign the object in the life time?
- Yes: Pass the instance from setter (Setter injection)
- No: Go to next question
- Do you think it’s meaningful that client class knows which object to pass?
- Yes: Pass the instance from Constructor argument (Constructor injection)
- No: Go to next question
- Is it possible to replace the static function in your environment (framework, language, etc…)?
You may want to read this post too. It’s the same topic.
Put arguments into one
You can find the complete code here (ver7-3)
You have already satisfied with the refactoring? How do you think if we need to add additional argument to Command classes? Let’s see the CommandHolder class again.
export class CommandHolder {
private commands: Map<CommandName | string, Command>;
private undefinedCommand: Command;
constructor(shoppingCart: ShoppingCart, shoppingConsole: ShoppingConsole) {
this.commands = new Map();
this.commands.set(CommandName.Command, new DisplayCommand(shoppingConsole));
this.commands.set(CommandName.List, new ListCommand(shoppingConsole));
this.commands.set(CommandName.Add, new AddCommand(shoppingCart, shoppingConsole));
this.commands.set(CommandName.Remove, new RemoveCommand(shoppingCart));
this.commands.set(CommandName.Cart, new ShowItemsCommand(shoppingCart, shoppingConsole));
this.commands.set(CommandName.Pay, new PayCommand(shoppingCart, shoppingConsole));
this.commands.set(CommandName.Exit, new ExitCommand(shoppingConsole));
this.undefinedCommand = new UndefinedCommand(shoppingConsole);
}
}
Some classes require only one argument but others two. Isn’t it cumbersome to change it? interface can be used to reduce this process in Typescript.
export interface CommandRequiredArgs {
shoppingCart: ShoppingCart;
shoppingConsole: ShoppingConsole;
}
export class CommandHolder {
private commands: Map<CommandName | string, Command>;
private undefinedCommand: Command;
constructor(args: CommandRequiredArgs) {
this.commands = new Map();
this.commands.set(CommandName.Command, new DisplayCommand(args));
this.commands.set(CommandName.List, new ListCommand(args));
this.commands.set(CommandName.Add, new AddCommand(args));
this.commands.set(CommandName.Remove, new RemoveCommand(args));
this.commands.set(CommandName.Cart, new ShowItemsCommand(args));
this.commands.set(CommandName.Pay, new PayCommand(args));
this.commands.set(CommandName.Exit, new ExitCommand(args));
this.undefinedCommand = new UndefinedCommand(args);
}
...
}
export class RemoveCommand extends ArgsCommandBase<RemoveCommandArgs> {
constructor(private args: { shoppingCart: ShoppingCart }) {
super();
}
protected process(args: RemoveCommandArgs): void {
this.args.shoppingCart.removeItem(args.itemName as ItemName, args.numberOfItems);
}
...
}
As you can see, CommandHolder
class requires only one arguments which contains all necessary objects. It is specified in each Command class constructor. Even if the number of properties in CommandRequiredArgs
increases we don’t have to change anything in CommandHolder’s constructor. A constructor or function requires one or two arguments at first but the number of arguments increases when the application gets bigger. This technique makes the management easier than specifying one by one.
What you learnt
Current implementation looks much better than the first implementation. I explained how to refactor from beginner’s code and current code is good enough. If you have read through this series you learnt following.
- How to extract logic into a smaller function
- How to separate class
- How to make it testable (dependency injection)
Even if you haven’t written any unit test in your project you can write it from today. Let’s start writing unit test for quality and readability.
You might think that console log and error shouldn’t be called inside of command class. Yes, that’s true. Instead, return the text to the shop class and call console log or error there. It’s better way.
Comments