Testing Insights

How to improve the testability of code

2025-10-29

As is well known, writing unit tests has many benefits, but some supervisors may ask why writing tests requires engineers to spend so much extra time? In addition to being unfamiliar with unit testing techniques, the root cause is the low testability of product code, which makes it difficult for engineers to focus their energy on the right areas when writing tests, and even leads to giving up writing tests. Writing excellent unit tests has a certain level of difficulty and threshold, and the key lies in how to improve the testability of the code. This article introduces the experience of improving the testability of Java unit testing.

1.  What is code testability?

There are various definitions of code testability, including the SOCK Model proposed by the renowned Microsoft testing architect Dave Catlett. Although different definitions exist, almost all concepts revolve around the same thing, which is the ease of testing a software in a certain testing environment, or the amount of testing costs invested.

It is not difficult to notice that the testability of a program is highly correlated with design quality. For example, programs with low cohesion, high coupling, and code smells are often difficult to test. Therefore, if we can improve testability and enable engineers to write more useful and valuable test cases, it will be easy to quickly discover and eliminate bugs during the development process.

Programs with high testability typically have several key characteristics:

(1). Simple design

If the program under test only focuses on doing one thing, the things and situations that need to be tested are relatively simple, and its readability and expressiveness will also be better. Therefore, it is better to design the program as simple, straightforward, and without unnecessary complexity (KISS principle). On the contrary, the more complex and not simple the program, the more difficult it is to test. For complex programs such as overly lengthy methods, my suggestion is that the method should not be too long and the number of lines should be limited to 30 to be considered qualified.

(2). Easy to initialize

In unit testing, it is easy to initialize the test program, in other words, it can be easily generated during testing. In this case, the program under test usually has few dependencies or can easily isolate external dependencies. If it is difficult to initialize, it indicates that it may be a poorly designed structure. Easy initialization is the first step in writing unit tests. If even this is difficult to achieve, then you won't reap the benefits of writing unit tests.

(3). Controllable input parameters

This means that we can easily simulate different testing scenarios in unit testing. For example, a certain bank system will issue a warning notice to the bank when a customer withdraws more than 100 million yuan. At this point, we can control the customer's withdrawal amount in unit testing to simulate the scenario of a large withdrawal by the customer, without actually preparing a large sum of money in the bank account beforehand. In Java testing, mock technology is used to simulate various scenarios in practice to improve the controllability of the program. If the input parameters of the program are controllable, it can be repeatedly tested and then incorporated into the Continuous Integration (CI) process to achieve the level of automated testing.

(4). The output result is easy to verify

The program must be able to produce the expected output for any operation, and the output is usually represented in the form of predictable return values, internal states, and external behaviors. Regardless of how it is represented, it must be traceable in order to be easily verified. If it is easy to verify whether the program results meet expectations, it can reduce the difficulty of testing, for example, using simple verification methods such as assertEqual() and verifiy() to obtain test results. If the output result of the program is not easy to verify, it means it is not easy to test.

2.  What is the importance of code testability?

High software testability usually means that the quality of the code is also high, and the resulting product will be easy to use.

If the software has high testability, it is easy to test left shift. The essence of testing left shift is to detect and prevent problems as early as possible. If we can detect problems earlier, the cost of solving them will be cheaper, and we can deliver products faster and receive customer feedback more quickly, which is more in line with the spirit of agile development.

If the software has low testability, engineers will have to spend a lot of time writing tests, and usually the unit tests written in this situation will be messy. Not only will the test results be poor, but it will also be difficult to test the key points and prone to test failures, resulting in low test quality. For example, if there are too many dependencies on mock in testing, it will make the testing very complex, and the testing program also needs to pay attention to quality. If writing tests is like a loss making business, it will make engineers unwilling to write tests or delay writing tests until very late, which goes against the principle of left shift testing and the original intention of unit testing.

3.  How to improve the testability of code

(1). Single Responsibility Principle (SRP)

Single responsibility means that each class should have one and only one responsibility (or reason for change), which also means high cohesion, making unit testing easier.

If a class/method operates multiple functions simultaneously, it will greatly increase the difficulty of testing because with more functions, there will be more dependencies and more results that need to be verified, making testing more complex. But practicing SRP is not as simple as imagined. The key is how to control the granularity. If the cutting is too fine, it will create redundant classes; Cutting too big makes it less simple. So how to allocate responsibilities appropriately still depends on the actual situation.

Taking the example of a certain bank system just now, the withdrawal function should belong to the responsibility of WithdrawService, and the internal warning function should belong to the responsibility of NotifyService. Both have their own responsibilities, clear rights and responsibilities, and are independent of each other.

(2). Dependency Injection definition: The dependencies required by the test program are provided externally, without the test program having to create them themselves.

This is an important means to significantly improve testability, reduce coupling between programs, and avoid mixing the new keyword with important business logic. By injecting dependency programs externally, the controllability of the test program is improved, and we can easily create test avatars and inject them into the test program.

Generally speaking, there are three types of dependency injection patterns, and I recommend using the pattern of injecting dependencies through constructors.

(3). Removing Bad Smells that can easily affect testability includes: Long Parameter List, Divergent Change, Long Method, Large Class

One solution is for the team to frequently conduct code reviews, and team members need to be familiar with refactoring techniques. If not refactored, besides being difficult to test, it will eventually become a technical debt.

(4). YAGNI principle

Engineers should only perform corresponding functions when faced with specific requirements. For example, a team member added an if branch in the method that is currently not needed but may be used in the future. Should we test it now?

(5). Constructor does not contain any logic

Constructor should only focus on initialization without any logic. If the constructor not only initializes if-else, Also calling APIs and querying DBs increases initialization costs, makes it difficult to isolate external dependencies, and reduces testability.

In addition, it is not easy to rewrite or mock constructors in unit testing, although some testing frameworks can now replace the behavior of constructors, it is usually not recommended to use them.

A good constructor should have no logic and only perform dependency injection and state initialization:

//img.enjoy4fun.com/news_icon/d40rm58vterc72pjhdvg.png

(6). Reduce the use of Singleton/Static

It is difficult to replace a static method in testing. In addition, abusing the Singleton pattern can easily lead to a difficult to maintain global state. for example:

//img.enjoy4fun.com/news_icon/d40rmf8vterc72pjhn9g.png

Although Singleton/static is convenient, it also brings about the problem of increasing coupling invisibly, both of which are reasons for being difficult to measure. It may make it difficult for us to immediately detect problems, as the essence of unit testing is to explore how to isolate external dependencies. Sometimes it can be difficult to completely avoid using static methods, but if possible, try to minimize their frequency of use.

However, if it is a simple static method like Strings. isBlank() or Math. abs(), which has no shared state internally and is not dependent on the external environment, I don't think it will cause any difficult to test issues.

But if we really have to, Mockito 3.4 also provides Mockito. mockStatic, which allows us to replace static behavior in unit testing. The cost is that the testing program will become more complex and the execution time will increase.

(7). Test-Driven Development (TDD)

TDD is a development approach that starts by writing tests from the user's perspective and then goes back to writing product code. Because TDD allows developers to think from the user's perspective, changing positions means changing their minds, making it easier to understand how to design classes/APIs that are more user-friendly. In order to write tests first, developers must first think about how to conduct testing. They not only understand the requirements, but also gradually break them down into simple, small test cases. If proficient in TDD technology, software testability can be greatly improved.

more stories
See more