Fundamentals 47 min read

Mastering Effective Unit Tests: Principles, Practices, and Common Pitfalls

This article explains why unit tests should focus on verifying behavior rather than merely increasing coverage, outlines essential testing principles such as AIR, FIRST, and ASCII, provides concrete Java examples for writing, validating, and asserting tests, and offers detailed guidance on handling exceptions and method‑call verification to avoid ineffective testing.

Alibaba Cloud Developer
Alibaba Cloud Developer
Alibaba Cloud Developer
Mastering Effective Unit Tests: Principles, Practices, and Common Pitfalls

Preface

In the past, we learned procedural programming for credits, object‑oriented programming for jobs, salary‑driven programming for life, leadership‑driven programming for promotion, metric‑driven programming for targets, and eventually we ended up programming just to get by .

Now, leaders demand higher unit‑test coverage to meet corporate quality goals. We often add tests just to increase the coverage number, ignoring the real purpose of unit tests: verifying regression and catching bugs.

1. Unit Test Overview

1.1. What Is a Unit Test?

In computer programming, a unit test (also called a module test) verifies the correctness of a program module. The smallest testable part is a function, method, or procedure, depending on the programming paradigm.

1.2. Unit Test Example

1.2.1 Service Code Example

@Service
public class UserService {
    /** User DAO */
    @Autowired
    private UserDAO userDAO;

    /** Query users */
    public PageDataVO<UserVO> queryUser(Long companyId, Long startIndex, Integer pageSize) {
        Long totalSize = userDAO.countByCompany(companyId);
        List<UserVO> dataList = null;
        if (NumberHelper.isPositive(totalSize)) {
            dataList = userDAO.queryByCompany(companyId, startIndex, pageSize);
        }
        return new PageDataVO<>(totalSize, dataList);
    }
}

1.2.2 Integration Test Example

@Slf4j
@RunWith(PandoraBootRunner.class)
@DelegateTo(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {ExampleApplication.class})
public class UserServiceTest {
    @Autowired
    private UserService userService;

    @Test
    public void testQueryUser() {
        Long companyId = 123L;
        Long startIndex = 90L;
        Integer pageSize = 10;
        PageDataVO<UserVO> pageData = userService.queryUser(companyId, startIndex, pageSize);
        log.info("testQueryUser: pageData={}", JSON.toJSONString(pageData));
    }
}

Integration tests usually depend on external environment, need to start the application, and often rely on logging for manual verification.

1.2.3 Unit Test Example

@Slf4j
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
    private static final String RESOURCE_PATH = "testUserService/";

    @Mock
    private UserDAO userDAO;

    @InjectMocks
    private UserService userService;

    @Test
    public void testQueryUserWithoutData() {
        Long companyId = 123L;
        Long startIndex = 90L;
        Integer pageSize = 10;
        Mockito.doReturn(0L).when(userDAO).countByCompany(companyId);
        String path = RESOURCE_PATH + "testQueryUserWithoutData/";
        PageDataVO<UserVO> pageData = userService.queryUser(companyId, startIndex, pageSize);
        String text = ResourceHelper.getResourceAsString(getClass(), path + "pageData.json");
        Assert.assertEquals("分页数据不一致", text, JSON.toJSONString(pageData));
        Mockito.verify(userDAO).countByCompany(companyId);
        Mockito.verifyNoMoreInteractions(userDAO);
    }

    @Test
    public void testQueryUserWithData() {
        Long companyId = 123L;
        Mockito.doReturn(91L).when(userDAO).countByCompany(companyId);
        Long startIndex = 90L;
        Integer pageSize = 10;
        String path = RESOURCE_PATH + "testQueryUserWithData/";
        String text = ResourceHelper.getResourceAsString(getClass(), path + "dataList.json");
        List<UserVO> dataList = JSON.parseArray(text, UserVO.class);
        Mockito.doReturn(dataList).when(userDAO).queryByCompany(companyId, startIndex, pageSize);
        PageDataVO<UserVO> pageData = userService.queryUser(companyId, startIndex, pageSize);
        text = ResourceHelper.getResourceAsString(getClass(), path + "pageData.json");
        Assert.assertEquals("分页数据不一致", text, JSON.toJSONString(pageData));
        Mockito.verify(userDAO).countByCompany(companyId);
        Mockito.verify(userDAO).queryByCompany(companyId, startIndex, pageSize);
        Mockito.verifyNoMoreInteractions(userDAO);
    }
}

Unit tests are independent, fast, repeatable, self‑validating, and timely.

1.3. Unit Test Principles

1.3.1 AIR Principle

Automatic : Tests must run automatically without human interaction.

Independent : Tests should not depend on each other or external resources.

Repeatable : Tests must produce the same result every run.

1.3.2 FIRST Principle

Fast : Tests should execute quickly.

Independent : Same as above.

Repeatable : Same as above.

Self‑Validating : Assertions verify results automatically.

Timely : Tests are written and maintained alongside code changes.

1.3.3 ASCII Principle (Alibaba)

Automatic

Self‑Validating

Consistent : Input and output are deterministic.

Independent

Isolated : No external dependencies.

1.3.4 Comparison of Integration vs Unit Tests

Integration tests often do not satisfy all unit‑test principles, while unit tests generally do.

2. Ineffective Unit Tests

To identify ineffective tests, we must consider the whole test‑writing workflow and look for shortcuts that keep coverage high but reduce test quality.

2.1. Unit Test Coverage

Code coverage measures the proportion of source code executed by tests.

Line Coverage : Whether each line is executed.

Branch Coverage : Whether each branch is executed.

Condition Coverage : Whether each boolean sub‑expression is exercised.

Path Coverage : Whether each possible path through the code is exercised.

Other metrics include method coverage and class coverage.

2.2. Unit Test Writing Process

2.2.1 Define Objects

Define the object under test, mock dependencies, and inject them.

2.2.2 Simulate Methods

Mock dependency methods, set return values or exceptions.

2.2.3 Invoke Methods

Call the method under test and verify returned values or exceptions.

2.2.4 Verify Methods

Verify that dependency methods were called with expected arguments.

2.3 Can We Cut Corners?

We explore whether any step can be simplified without reducing coverage.

2.4 Final Conclusion

Most shortcuts concentrate on the verification phase, especially verifying data objects, exception throwing, and dependency method calls.

3. Validating Data Objects

Data‑object validation ensures that parameters, return values, and object fields meet expectations.

3.1 Sources of Data Objects

Returned from the method under test.

Captured from mocked dependency method arguments.

Read from the tested object's internal state.

Extracted from request parameters.

3.2 Verification Methods

3.2.1 Null Checks

// Verify null
Assert.assertNull("User ID must be null", userId);
// Verify not null
Assert.assertNotNull("User ID must not be null", userId);

3.2.2 Boolean Checks

// Verify true
Assert.assertTrue("Result must be true", NumberHelper.isPositive(1));
// Verify false
Assert.assertFalse("Result must be false", NumberHelper.isPositive(-1));

3.2.3 Reference Checks

// Same instance
Assert.assertSame("Users must be identical", expectedUser, actualUser);
// Different instance
Assert.assertNotSame("Users must not be identical", expectedUser, actualUser);

3.2.4 Value Checks

// Simple value
Assert.assertEquals("User name mismatch", "admin", userName);
// Array
Assert.assertArrayEquals("User ID list mismatch", new Long[]{1L,2L,3L}, userIds);
// Complex object (field‑by‑field)
Assert.assertEquals("User ID mismatch", 1L, user.getId());
Assert.assertEquals("User name mismatch", "admin", user.getName());

3.3 Common Pitfalls

Only checking for non‑null, or verifying a subset of fields, can miss bugs when new fields are added.

3.3.1 No Verification

userService.queryUser(companyId, startIndex, pageSize);

3.3.2 Non‑Null Only

PageDataVO<UserVO> pageData = userService.queryUser(...);
Assert.assertNotNull("Page data must not be null", pageData);

3.3.3 Partial Field Verification

Assert.assertEquals("Total size must not be null", totalSize, pageData.getTotalSize());

3.3.4 Full Field Verification

Assert.assertEquals("Total size mismatch", totalSize, pageData.getTotalSize());
Assert.assertEquals("Data list mismatch", dataList, pageData.getDataList());

If new fields are added to PageDataVO, the test will not catch them.

3.3.5 Perfect Verification via JSON

String expected = ResourceHelper.getResourceAsString(getClass(), path + "pageData.json");
Assert.assertEquals("Page data mismatch", expected, JSON.toJSONString(pageData));

3.4 Mock Data‑Object Guidelines

All properties of mocked objects (except those used only for branching) should be non‑null to avoid hidden bugs.

3.5 Validation Principles

Validate every data object source.

Use assertions with clear semantics.

Prefer whole‑object verification (e.g., JSON comparison) over field‑by‑field checks.

4. Validating Exceptions

Exception handling is a core part of robust Java code and must be verified in unit tests.

4.1 Sources of Exceptions

Invalid object fields.

Illegal method arguments.

Invalid return values.

Mocked methods that throw.

Static utility methods.

4.2 Exception Verification Methods

4.2.1 try‑catch

@Test
public void testCreateUserWithException() {
    Mockito.doReturn(true).when(userDAO).existName(Mockito.anyString());
    UserCreateVO vo = new UserCreateVO();
    try {
        userService.createUser(vo);
    } catch (ExampleException e) {
        Assert.assertEquals(ErrorCode.OBJECT_EXIST, e.getCode());
        Assert.assertEquals("User already exists", e.getMessage());
    }
    Mockito.verify(userDAO).existName(vo.getName());
}

4.2.2 @Test(expected=...)

@Test(expected = ExampleException.class)
public void testCreateUserWithException() {
    Mockito.doReturn(true).when(userDAO).existName(Mockito.anyString());
    UserCreateVO vo = new UserCreateVO();
    userService.createUser(vo);
    // No further verification possible
}

4.2.3 @Rule ExpectedException

@Rule
public ExpectedException exception = ExpectedException.none();

@Test
public void testCreateUserWithException1() {
    Mockito.doReturn(true).when(userDAO).existName(Mockito.anyString());
    UserCreateVO vo = new UserCreateVO();
    exception.expect(ExampleException.class);
    exception.expectMessage("User already exists");
    userService.createUser(vo);
}

4.2.4 Assert.assertThrows (JUnit 5 / JUnit 4.13+)

@Test
public void testCreateUserWithException() {
    Mockito.doReturn(true).when(userDAO).existName(Mockito.anyString());
    UserCreateVO vo = new UserCreateVO();
    ExampleException ex = Assert.assertThrows("Wrong exception type", ExampleException.class, () -> userService.createUser(vo));
    Assert.assertEquals(ErrorCode.OBJECT_EXIST, ex.getCode());
    Assert.assertEquals("User already exists", ex.getMessage());
    Mockito.verify(userDAO).existName(vo.getName());
}

Assert.assertThrows is the recommended approach because it allows full verification of type, message, code, and cause.

4.3 Common Mistakes

Using @Test(expected=Exception.class) with a generic Exception hides the real problem.

Only checking exception type, ignoring code or message.

Verifying only part of the exception attributes.

Not verifying the cause of the exception.

Skipping verification of dependent method calls that lead to the exception.

4.3.6 Perfect Exception Verification

Throwable cause = new RuntimeException();
Mockito.doThrow(cause).when(userDAO).create(Mockito.any(UserCreateVO.class));
String path = RESOURCE_PATH + "userCreateVO.json";
UserCreateVO vo = JSON.parseObject(ResourceHelper.getResourceAsString(getClass(), path), UserCreateVO.class);
ExampleException ex = Assert.assertThrows("Wrong exception type", ExampleException.class, () -> userService.createUser(vo));
Assert.assertEquals(ErrorCode.OBJECT_EXIST, ex.getCode());
Assert.assertEquals("User already exists", ex.getMessage());
Assert.assertEquals(cause, ex.getCause());
ArgumentCaptor<UserDO> captor = ArgumentCaptor.forClass(UserDO.class);
Mockito.verify(userDAO).create(captor.capture());
String expected = ResourceHelper.getResourceAsString(getClass(), "userCreateDO.json");
Assert.assertEquals(expected, JSON.toJSONString(captor.getValue()));

5. Verifying Method Calls

Method‑call verification confirms that mocked dependencies are invoked correctly.

5.1 Sources of Method Calls

Calls on injected mocks.

Calls on objects passed as parameters.

Calls on return values (e.g., service responses).

Static method calls.

5.2 Verification Techniques

5.2.1 Verify Arguments

// No‑arg method
Mockito.verify(userDAO).deleteAll();
// Specific argument
Mockito.verify(userDAO).delete(userId);
// Any argument
Mockito.verify(userDAO).delete(Mockito.anyLong());
// Nullable argument
Mockito.verify(userDAO).queryCompany(Mockito.anyLong(), Mockito.nullable(Long.class));
// Null argument
Mockito.verify(userDAO).queryCompany(Mockito.anyLong(), Mockito.isNull());
// Varargs
Mockito.verify(userService).delete(1L, 2L, 3L);
Mockito.verify(userService, Mockito.times(3)).delete(Mockito.anyLong());

5.2.2 Verify Call Count

Mockito.verify(userDAO).delete(userId); // default once
Mockito.verify(userDAO, Mockito.never()).delete(userId);
Mockito.verify(userDAO, Mockito.times(3)).delete(userId);
Mockito.verify(userDAO, Mockito.atLeastOnce()).delete(userId);
Mockito.verify(userDAO, Mockito.atLeast(2)).delete(userId);
Mockito.verify(userDAO, Mockito.atMostOnce()).delete(userId);
Mockito.verify(userDAO, Mockito.atMost(5)).delete(userId);
Mockito.verify(userDAO, Mockito.only()).delete(userId);

5.2.3 Capture Arguments

ArgumentCaptor<UserDO> captor = ArgumentCaptor.forClass(UserDO.class);
Mockito.verify(userDAO).modify(captor.capture());
UserDO captured = captor.getValue();

5.2.4 Verify Final Methods, Private Methods, Constructors, Static Methods (PowerMockito)

// Final method
Mockito.verify(finalObj).finalMethod();
// Private method
PowerMockito.verifyPrivate(mockClass, Mockito.times(1)).invoke("unload", Mockito.anyList());
// Constructor
PowerMockito.verifyNew(MockClass.class).withNoArguments();
// Static method
PowerMockito.verifyStatic(StringUtils.class);
StringUtils.isEmpty(str);

5.2.5 Verify No More Interactions

Mockito.verifyNoInteractions(idGenerator, userDAO);
Mockito.verifyNoMoreInteractions(idGenerator, userDAO);

5.3 Problems with Incomplete Verification

Examples show that skipping verification, using only atLeastOnce, or verifying a subset of calls can miss bugs such as missing loops, duplicated calls, or extra side‑effects.

5.3.6 Perfect Verification

ArgumentCaptor<Long> idCaptor = ArgumentCaptor.forClass(Long.class);
ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
Mockito.verify(userCache, Mockito.atLeastOnce()).set(idCaptor.capture(), userCaptor.capture());
Assert.assertEquals(expectedIdList, idCaptor.getAllValues());
Assert.assertEquals(expectedUserList, userCaptor.getAllValues());
Mockito.verifyNoMoreInteractions(userCache);

6. Postscript

《单元测试》 Unit tests have truth and falsehood, The craftsman's spirit runs through them. Coverage alone is not the goal, Regression verification shows the true merit.

In other words, understand how to distinguish effective from ineffective unit tests, keep the craftsman's spirit throughout, remember that coverage is not the purpose, and only regression verification demonstrates the real value of unit testing.

Related Articles:

Collection! Java Programming Tips – Unit Test Writing Process

Java Unit Test Tips – JSON Serialization

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

code coverageunit testingMockitomethod verification
Alibaba Cloud Developer
Written by

Alibaba Cloud Developer

Alibaba's official tech channel, featuring all of its technology innovations.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.