How to unit test private method in TypeScript (part 2)
Posted on: 2017-04-13
I already posted about how to write unit tests for private methods with TypeScript about one year ago. A few days ago, I had the same discussion that I had in my previous team concerning the private method. The situation is similar. Developers don't test private methods and rely on public methods to reach those private methods. At first, it may sound like we are going inside those private methods. Therefore, we are doing proper unit testing.
The problem with going through intermediate methods to access the one we want to test is that any change in intermediate methods will make multiple tests fail. When a unit tests fail, the goal is to know which unit of your code fails. Imagine you have class A
with methods a1
, a2
, and a3
. You want to to unit test a3
, but the only entry point is a1
, which is the only method public. This one call a2
, who call in some particular situation a3
. You have multiple conditions in a3
and you evaluate that you need 5 unit tests. The problem is that if a1
or a2
change in the future, all these 5 tests may fail when they should not.
At that point, most people understood the situation and agreed to test private methods. However, there are some good ways to do it and some bad ways. The worst way to do it to cast the class A
to be of type any and call a3
directly. Something like :
// Bad way:
var a = new A();
(a as any).a3();
The problem with the above code is that when you refactor a3
to have a better name, no tool will find out this instance. Moreover, this opens the door to accessing private fields or injecting new functions and fields into the class. In the end, it becomes a nightmare to maintain. We are using TypeScript to be strongly typed, so our tests should continue to be as strong.
In the previous article I wrote, I talked about 2 patterns. The first one is about working around encapsulation with an interface. The second had two variations that we will revisit.
Let's remember the first pattern. The first pattern is that class A should have an interface IA
that is used everywhere in the code, but not in the test. IA
would only expose the method a1
. Everywhere you use the interface, the only place it doesn't is when it's getting injected by the inversion of control container. However, we can leverage this abstraction to keep a strong encapsulation for the application and use the implementation with every public method. This way, developers still have only access to a1 in our example, but in our test, we have access to everything else (more than just what is exposed in the interface). This might not sound like a proper solution at first since we open the encapsulation on the implemented class, but it's the cheapest way to test unit tests. That said, I am all with you that there is another solution like the pattern #2 presented in the previous article.
The second pattern presented was about moving code around. In our example, a2
and a3
are private and could be moved outside another class. For example, let's say that A was a user class, a1
was a method to get the user information to display on the screen, a2
was a method to get the address information, and a3
was a method to format the street address. This could be refactored from :
class User{
public getUserInformationToDisplay(){
//...
this.getUserAddress();
//...
}
private getUserAddress(){
//...
this.formatStreet();
//...
}
private formatStreet(){
//...
}
}
to:
class User{
private address:Address;
public getUserInformationToDisplay(){
//...
address.getUserAddress();
//...
}
}
class Address{
private format: StreetFormatter;
public format(){
//...
format.ToString();
//...
}
}
class StreetFormatter{
public toString(){
// ...
}
}
Originally, we wanted to test the private method formatStreet
(a3
), and now it's very easy because I do not even need to care about all the classes or functions that call it, just to unit test the StreetFormatter class (which was the original a3
). The pattern is the best way to unit test private methods: divide the code correctly into specific classes. However, the process is also costly in terms of time.
I always prefer the second approach, but time constraints and the high velocity of shipping features are always a higher priority -- even in a software shop where the message is quality first. That said, I prefer using the first approach to not having any unit tests. It's a good compromise that works well, regardless of your framework. I used both approaches in TypeScript code using the proprietary framework, React, and now with Angular. In the end, the important is to have the best coverage while ensuring that everything tested is solid to help the software and not slow down the whole development.