Three questions about unit testing

As a programmer, you’ve heard of unit testing, but many of you haven’t used it in your actual projects. This may be due to “misconceptions” about unit testing, such as:

  • Writing unit tests took more time. I had to work overtime every day to write production code, so I had no time to write tests.
  • Writing unit tests is not profitable and buggy;
  • Writing unit tests is a burden, changing the structure of the production code, and having to change the test code.

Try to answer these questions first.

It is not accurate to say that writing unit tests takes more time. To be precise, writing unit tests takes more “writing code time,” which is fine, after all, writing more test code. But when a programmer makes a requirement, he doesn’t spend much time writing pure code. You have to figure out the logic of the old code, design the classes and methods, and then you have to write the code, and then you have to test it manually, and then you have to debug it, fix it, test it. “There is very little time to actually write code.” Using unit tests, while it may take more time to write code, can help you cut back on other times, “reducing the total amount of time you need to do it.”

Are there bugs in writing unit tests? Of course there may be, and we won’t be able to achieve true bug free, but once the unit tests are written, the number of bugs can be significantly reduced. Because the cost of writing unit tests to find bugs is very low, it can find bugs at the development stage, and it can test many boundary conditions. But if you are “wrong about the requirements and the business”, unit testing can’t fix that, and bugs will naturally arise. Then again, the benefits of writing unit tests go far beyond just finding bugs. There is also the ability to “document code” and have a “safety net” for refactoring. It can even help you “figure out requirements” and “design code.”

Changing production code requires maintaining the corresponding test code, which does introduce additional costs. However, with the editor’s “refactoring feature,” it is relatively easy to batch change what needs to be changed, but the cost is not as big as expected. And after refactoring, run the unit tests again to see what failed and double-check the product code you changed for any problems. If we think of unit tests as “documentation of production code,” this maintenance cost might be more acceptable.

Why do we need unit tests?

As mentioned earlier, unit tests have many functions. Personally, unit tests are most useful as “code documentation” and “safety net for refactoring.” After all, software development is a long process of tinkering. If you don’t have enough testing, changing a piece of code is like clearing landmines. After you change it, you always have to do a silent prayer before you go online, for fear of triggering any bugs.

But if you have enough tests (not just unit tests), you can run the tests after you change the code to see what failed, whether your changes were the result, and how to fix the tests. That will give me a lot of confidence.

You know, code is written for people to see. Testing is more user-friendly than production code because it is simple, straightforward and described from the user’s point of view, so if you want to understand what a piece of production code does, it is more intuitive and comfortable to look at its unit tests.

Many teams do testing, but the vast majority of testing is done after development, with dedicated testing students responsible for end-to-end testing or API testing. In fact, the cost of end-to-end testing is very high, especially for certain boundary conditions, constructing data and scenarios is very troublesome. And once a bug is found, it takes a lot of time to communicate, modify, submit and deploy.

The biggest advantage of unit testing is the “low cost”, it is easier to test each branch of the product code, and unit testing is generally written by the developers themselves, so that bugs can be found in the least time and bugs can be modified at the lowest cost.

What are unit tests?

Test the Pyramids

Not all tests are unit tests, and there are many different kinds of tests. One of the more widely used “test pyramids” in the industry describes their differences and relationships:

Test the Pyramids

Based on the test pyramid model, testing at the lower level should have more coverage and lower cost. Unit tests are at the lowest end of the testing pyramid and form the foundation of the entire testing pyramid.

Of course, the testing pyramid does not have to have three layers, and there may be other tests in between, such as “contract tests”.

There is another “diamond testing” model, in which most tests are written in the interface testing layer, while UI tests and unit tests write only a small number of tests. This article does not discuss, interested students can know by themselves.

Characteristics of unit testing

Unit tests are, as their name suggests, “units,” small enough, fast enough, and dependency free. Unit tests only test the logic of the part of the production code you want to test. A unit test should only test a simple business logic. Generally speaking, running a unit test is fast, in the order of milliseconds to tens of milliseconds. If there are dependent classes, mock other classes to eliminate external dependencies.

What is not a unit test?

Many students tend to confuse other tests with unit tests, most commonly integration tests that launch the Spring context. For example, the @SpringbooTtest annotation can be used to enable the Spring context, which can test Spring features such as whether dependencies are properly injected, but it takes a lot of time to run once (because the Spring context is enabled) and is not really a “unit test” because it relies on the Spring framework.

How do I write unit tests

So how do you write unit tests? There is a methodology in our industry called “TDD” (Test Driven Development). At the heart of TDD is the word “drive”. The idea is to test and drive production code from a testing perspective. In the testing pyramid, unit tests are the most relevant to developers, so “testing” generally refers to unit tests.

TDD consists of the following steps:

  1. Sort out your requirements
  2. Design the outgoing and incoming parameters of classes and methods
  3. Write test code
  4. Drive out the production code
  5. Reconstruct, loop 3-5 steps.

The requirements need to be clarified first, because only by clarifying the requirements can we ensure that the TDD driven code is consistent with the business expectations. Then the second step is the process of designing classes and methods, also known as the Task List. This step allows you to design the relationships between classes and the outgoing and incoming parameters of methods. The first two steps can be done without TDD, but with TDD, you can take a business perspective and design everything you need to do first, rather than writing code by hand and having to change it when you’re halfway through.

Steps 3-5 are a circular process. Because the beginning of writing code may not pay too much attention to the format, style, performance of the code, write faster, let the test pass. Once the tests have passed, you can go back and refactor the code you wrote and then run all the unit tests again to see if there are any failed unit tests to see if the refactoring had any effect on the desired input and output.

Structure of unit tests

A complete unit test should be divided into four parts:

  1. Declarations and parameters
  2. Prepare inputs and mocks
  3. Calling product code
  4. Validation, also called assertion

In the case of Java, there are several unit testing frameworks, the most popular being JUnit and TestNG. I use JUnit a little more, and JUnit uses the @test annotation to declare a Test on a method. The latest version of JUnit is JUnit 5, which has many improvements over the previous version in parameterized testing so that we do not have to write many highly similar test methods.

For the JUnit 5 parameterization test, you can check out the official documentation, as well as the corresponding Chinese translation for easy reading. You can also go to my website and search for JUnit 5 Parameterized Testing.

In general, the method name needs to be as readable as possible. It may be long, but it clearly states the intent of the test, such as:

@Test

void shouldReturn5WhenCalculateSumGiven2And3(a) {}



@Test

void should_return_5_when_calculate_sum_given_2_and_3(a) {}

Copy the code

Whether to use hump or underline is up to your team’s specifications and should be consistent across all testing styles. (Personally prefer underline ~)

Input parameters are typically primitive types or POJO objects, and some parameters can be abstractions that may be needed later in the validation phase.

If your production code has external dependencies, you need to mock them out. Common Mock frameworks include EasyMock and “Mockito”. You can compare the differences between each Mock framework and choose a suitable one.

Many students can’t understand why mocks are needed when they first write unit tests. They think mocks are troublesome or even unnecessary. The point of a mock is that you “can make sure that your test only tests the part of the code that you want to test”. This way, if the test fails, you know that there must be a problem with the method you’re testing, and it can’t be an external dependency, so that you can really “unit” it, and make sure that each test is small and pure.

Once the input parameter and mock are ready, the method to be tested is explicitly called, usually on a single line.

Finally, verification is divided into several kinds of verification, the most commonly used is to verify that the parameter is in line with their expectations. Boundary cases such as exceptions are sometimes validated. JUnit and other testing frameworks basically have their own verification functions, but the API is relatively simple, personally feel not particularly easy to use, I recommend using “AssertJ”, powerful, API is more comfortable to use.

Here’s an example:

@Test

void shouldReturnUserWithOrgInfoWhenLoginWithUserId(a) {

    String userId = "userId";

    String orgId = "orgId";

    User user = UserFactory.getUser(userId);

    Org org = OrgFactory.getOrg(orgId);

    given(orgService.getOrgById(orgId)).willReturn(org);

    

    UserInfo userInfo = userService.login(userId);

    

    assertEquals(org, userInfo.getOrg());

}

Copy the code

Unit testing FaQs

Let’s talk about some common problems with unit testing.

Write tests first or production code first?

Will do. There is a saying, though, that TDD recommends writing tests first and implementing them later. But many of you who are new to writing unit tests are not used to it. Writing tests first has the advantage of allowing you to design your code from a business perspective, rather than an implementation perspective. You can try writing tests first and then implementing them to get a sense of what that feels like.

There is a video on the b website that uses TDD to calculate Fibonacci numbers. It is a standard TDD process. If you are interested in it, you can go and have a look.

A lot of extra time to write unit tests?

This was already discussed at the beginning of the article. Writing unit tests does take more “writing code” time, but overall it can shorten the overall requirements development cycle. So writing unit tests is a “good business.”

What code needs unit testing the most?

Insecure code, logically complex code, important code. Such as utility classes, the Service layer of the three-tier architecture, the aggregation root of DDD, and domain services, these should write adequate unit tests.

Too much trouble constructing an input object?

Constructing a suitable input object can be cumbersome, especially if some objects have so many parameters that each test has to construct from scratch, resulting in very bloated and unreadable test code. At this point you can use the factory class to mass-produce objects. This factory class is placed in the test directory and does not affect production code. In the previous example, UserFactory is a factory class for the User object.

Return void for what?

If the method returns void, there must be some behavior inside the method. It could be “changing the value of an internal property” or “calling a method of an external class”.

If you are changing an internal value, this can be asserted by the object’s GET argument. This is a problem in the domain model after DDD, because it is possible that the production code does not need to expose get methods, but for testing purposes exposes the GET methods of internal attributes. It is possible to retrieve values for internal attributes using reflection, but this is not necessary. On balance, a GET approach that exposes the domain model is better.

If you are calling an external method, you can use Verify to verify that a method was called. You can use Capture to verify that other methods were called. This also validates that the production code is working as intended.

How do static methods mock?

Static methods are bad for mocks; you need to use a special mock framework. Examples include PowerMock, JMockit. In general, the Utils class has a lot of static methods. The LocalDateTime class we use a lot to get the current time is also static. You need to mock it with a special mock framework.

How is multithreading tested?

Multithreading is also hard to test. If the program is simple, you can use a multithreaded tool class like Sleep or CountDownLatch to assist the testing, and wait for all threads to run out of validation.

If your program is relatively complex, you need to use specialized multithreaded testing frameworks such as Tempus-Fugit, Thread Weaver, MultithreadedTC, and the JCStress project of the OpenJDK.

Later, there will be time to write an introduction to common annotations on how the specific framework is used. In fact, there are official documents to write, we will follow the official website to write a few examples. The recommended base package is junit 5 + Mockito + AssertJ. Static methods and multithreaded testing frameworks, you need to learn more about them.

About the author

I’m Yasin, a good-looking and interesting programmer.

Wechat public number: made up a process

Personal website: https://yasinshaw.com

Pay attention to my public number, grow up with me ~

The public,