The previous chapter covered “the place of unit testing in the overall testing architecture.” This chapter covers “common misconceptions and practices about unit testing.”
Many people have the wrong ideas and practices about unit testing. Typical misconceptions and practices are as follows:
1. Misconceptions
-
Testing is the job of the tester. Programmers should only write production code
Testers only care about whether the entire system meets the needs of customers and users in terms of functionality and external quality. They neither understand nor care about the code you write and the structure of your program, so they can only write black-box tests, not white-box tests. It is the programmer’s job to write the code and define the internal structure. It is also the programmer’s job to prove the correctness and reliability of your code and structure through unit tests.
-
Writing unit tests adds to the burden and slows things down
This is the biggest misconception about unit testing! Counter-intuitively, writing unit tests is a huge relief and keeps us moving at a steady pace throughout the software lifecycle. Without unit testing, we might start out fast, but later, as bugs continue to explode, a significant portion of our time will be spent finding, debugging, and fixing bugs, and less time will be spent writing production code, and the project will progress more slowly. Unit testing greatly reduces the possibility of bugs from the very beginning, and bugs can be found in time, accurately located, and fixed in the shortest time with the minimum cost.
First, the amount of time and effort required to write unit tests is trivial. On average, it takes about a minute to write a test. Writing 10,000 tests takes 10,000 minutes, or 7 days, or 21 working days on an 8-hour day. Compared to the revenue generated by 10,000 unit tests, 21 working days is really nothing. Without unit testing, it takes much more time and cost to locate and fix bugs through debugging and other methods after the product goes live.
Second, unit testing, especially with TDD, leads to better quality code. Testability forces you to write simple code, which makes it easier to understand, maintain, and modify. This will also improve product quality and speed up delivery.
-
The code is too simple to write unit tests
First, no matter how simple your code is, it can go wrong. For example, the dividend is zero in division operation, the amount is negative when depositing or withdrawing money, and the set is empty when selecting an optimal item in a set… Things like this can cause bugs to lurk in your code that will explode in the future.
Second, the code is not written once, and it may be changed several times over the life of the software, and it may not even be you in the future. Without unit test protection, future changes can introduce new bugs without detection.
Finally, since the code is simple, it’s even easier to write a test, and the cost is negligible. If so, why not write tests?
-
The code is too complex to write unit test tests
This misconception is the main reason many programmers are afraid to try to write unit tests. People who don’t use test-driven development write production code first and unit tests later. It is possible to write a method with hundreds or thousands of lines of code that have so many branches and loops that it is difficult to write tests. The solution to this problem is TDD, test first! Write unit tests first, then write production code to pass them. With a test-first approach, there is no “tests are hard to write” problem. Because you just write a test to express your requirements, and then write the simplest code to pass the test. If the test passes, the requirement is fulfilled.
-
Unit testing can be replaced with integration and functional testing
A lot of people say that you don’t need unit testing, you can find bugs in your code through integration testing and functional testing. This is wrong.
First, integration testing and functional testing cannot cover every situation. The execution result of the method under test is influenced by the internal state of the object, the state of object dependencies, the state of the environment, and method parameters. We need to write multiple test methods to test each case individually. If m classes are required to work together to implement a business function, and each class has n possible scenarios for the method called, we would need to write m× N test methods if we were to write functional tests to cover all possible states. If you write unit tests, you can write m+ N test methods, plus one or two functional tests, covering the main execution path, and proving that multiple objects are collaborating properly.
Second, integration and functional tests execute slowly. Integration testing and functional testing can take a long time because of the need to access databases, file systems, call third-party services over the network, and so on. This prevents testing from being performed too often and eliminates the need for quick feedback.
Third, in unit testing, you can simulate the dependencies of the classes under test using mock objects (which Mockito is used to create later), so you can start testing the dependencies before they are developed. Integration and functional testing, on the other hand, must wait for all dependencies to be developed before running tests. Simulation classes can also simulate extreme situations that don’t occur very often in real-world environments, and test in advance for those situations that integration and functional testing can only passively wait for before starting testing.
-
The benefit of unit testing is for the customer and the company. I don’t “dress for others.”
Unit testing can benefit multiple parties. Customers benefit by getting a better product, and companies benefit by delivering a better product. But the biggest beneficiaries are the programmers themselves. With unit testing, we can confidently code and steadily move forward. When bugs are caused by modification, reconstruction and expansion in the future, we can timely find them, accurately locate them and fix them at a low cost. After the correction, we can also confirm whether new bugs are accidentally introduced through regression testing. Without the protection of unit testing, we don’t know if there are bugs in the code we deliver, where the bugs are, and whether new bugs will be introduced after fixing bugs. With unit tests, we are confident, relaxed and happy to work. Without unit tests, we are nervous, tired and self-doubting. It’s two different worlds.
2. Wrong practices
-
Unit tests are written as integration tests
This is a common mistake many beginners make. Using real dependencies of the class under test in your tests instead of test surrogates, or connecting to a database, local file system, accessing third-party services, and so on, turns a unit test into an integration test. Rather than testing a single method of a single class, it tests multiple classes that work together, as well as the external environment.
-
Unit tests only cover “happy paths”
-
The response of the method under test is a function of:
-
The internal state of the class under test. For example, the account class under test has an internal state: the current balance. It affects the result of the withdrawal method call: when the withdrawal amount is not greater than the current balance, the withdrawal is successful and the current balance will be reduced accordingly. When the amount is greater than the current balance, failure of withdrawal, and throw BalanceInsufficientException anomalies. Another internal state of the account class: the account state also affects the result of the withdrawal method. When the account status is Locked, the withdrawal fails and an AccountLockedException is thrown.
-
The state of the dependencies of the class under test. For example, the order service class under test relies on the pricing service class to provide current commodity prices. When a customer places an order, the placeOrder() method under test of the Order service class calls the getCurrentPrice() method of the Pricing service class. Query the current unit price of each item in the shopping cart. This pricing service is a remote service. When the pricing service is available and there is a pricing for the goods, the order is created successfully; When pricing service is unavailable due to network reasons, shall be thrown PriceServiceUnavailableException; When the pricing service does not include certain items in the shopping cart, a PriceNotFoundException should be thrown.
-
Environment status. For example, our shopping website only accepts orders from 8:00 to 22:00 every day. So at 7:35, order with the measured method should throw OrderServiceUnavailableException anomalies.
-
Method parameters. For example, the Withdrawal amount parameter of a withdrawal method. When the withdrawal amount is less than the current balance of the account, the tested method will be executed normally; Is greater than the current balance, throws BalanceInsufficientException anomalies.
When writing unit tests, write tests for each of these possibilities. You can’t just test everything that’s normal (what’s called a “happy path,” such as the withdrawal method example, where the account is normal, the balance is sufficient, and the withdrawal amount is positive) and ignore all the exceptions. In recent history, many serious accidents (wenzhou high-speed train accident, us space shuttle accident) were caused by inadequate testing of equipment or control software abnormal conditions, resulting in disastrous consequences.
More on what we need to test later.
-
Write production code first, then unit tests
The traditional approach is to write production code and then write unit tests to see if the production code is correct. But Kent Beck, the founder of Extreme Programming (XP), believes in test-first, writing unit tests first and then writing code to pass them. The class and method under test may not exist at the time the unit tests are written! This disruptive approach can yield amazing benefits, expressing requirements through unit tests, setting acceptance criteria, and driving better design in the process! After testing, a unit test is just a validation tool; Test first, unit tests become design tools!
The second part of this tutorial covers test-first and test-driven development (TDD) in more detail.
That’s it for this chapter, and the next chapter will cover “the first unit test” by example!