Fundamentals 21 min read

Mastering Unit Testing: Overcome the Mental Barriers and Boost Code Quality

This article explores why unit testing is essential, how to conquer the psychological obstacles that prevent developers from writing tests, and provides practical methodologies such as TDD, BDD, the test pyramid, and the Fixture‑Scenario‑Case pattern with concrete Java examples.

Alibaba Cloud Developer
Alibaba Cloud Developer
Alibaba Cloud Developer
Mastering Unit Testing: Overcome the Mental Barriers and Boost Code Quality

Introduction

Testing should be a basic skill for every engineer, not a lofty art. The author shares personal experience of promoting unit testing across teams, discovering that the real obstacle is a mental block rather than time or project constraints.

TDD

Test‑Driven Development (TDD) follows a "red‑green‑refactor" cycle: write a failing test (red), implement the simplest code to pass it (green), then improve the code (refactor). It works best for well‑defined input‑output cases.

Red : test fails because no implementation exists.

Green : implement the simplest solution until the test passes.

Refactor : improve the code while keeping tests green.

BDD

Behavior‑Driven Development (BDD) extends TDD with a three‑part naming convention: should (expected result), when (method under test), given (scenario). Example code:

@RunWith(SpringBootRunner.class)
@DelegateTo(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = {Application.class})
public class ApiServiceTest {
    @Autowired
    ApiService apiService;

    @Test
    public void testMobileRegister() {
        AlispResult<Map<String, Object>> result = apiService.mobileRegister();
        System.out.println("result = " + result);
        Assert.assertNotNull(result);
        Assert.assertEquals(54, result.getAlispCode().longValue());
        // additional scenarios omitted for brevity
    }

    @Test
    public void should_return_mobile_is_not_correct_when_register_given_a_invalid_phone_number() {
        AlispResult<Map<String, Object>> result = apiService.mobileRegister();
        Assert.assertNotNull(result);
        Assert.assertFalse(result.isSuccess());
    }
}

Test Pyramid

The classic test pyramid (Unit → Service → UI) shows that deeper layers are slower and costlier. From a backend perspective the layers become:

Contract tests (most expensive, slowest)

Integration tests (spring container, middleware)

Unit tests (pure functions, no container)

Typical pyramid image (from Martin Fowler) is shown below:

Test Pyramid
Test Pyramid

Fixture‑Scenario‑Case (FSC) Pattern

FSC organizes test code into three layers:

Fixture : reusable fixed data or components.

Scenario : a specific situation built from fixtures.

Case : the actual test case that combines scenario and fixture.

Example:

public class GetUserInfoCase extends BaseTest {
    private String accessToken;
    @Autowired
    private UserFixture userFixture;

    @Before
    public void setUp() {
        userFixture.whenFetchUserInfoThenReturn("1", new UserVO());
        accessToken = new SimpleLoginScenario()
                .mobile("1234567890")
                .code("aaa")
                .login()
                .getAccessToken();
    }

    @Test
    public void should_return_user_info_when_user_login_given_a_effective_access_token() {
        Response userInfoResponse = new GetUserInfoScenario()
                .accessToken(accessToken)
                .getUserInfo();
        assertThat(userInfoResponse.jsonPath().getString("id"), equals("1"));
    }
}

Scenario Example (REST‑Assured)

@Data
public class SimpleLoginScenario {
    private String mobile;
    private String code;
    private String accessToken;

    public SimpleLoginScenario mobile(String mobile) { this.mobile = mobile; return this; }
    public SimpleLoginScenario code(String code) { this.code = code; return this; }
    public SimpleLoginScenario login() {
        Response response = loginWithResponse();
        this.accessToken = response.jsonPath().getString("accessToken");
        return this;
    }
    private Response loginWithResponse() {
        return RestAssured.get(API_PATH, ImmutableMap.of("mobile", mobile, "code", code)).thenReturn();
    }
}

Mocking Tools

Mockito is the go‑to library for mocking beans in Spring tests:

public class MockitoTest {
    @MockBean(classes = CacheImpl.class)
    private Cache cache;

    @Test
    public void should_return_success() {
        Mockito.when(cache.get("KEY")).thenReturn("VALUE");
        Mockito.when(cache.get(Mockito.anyString())).thenReturn("VALUE");
        Mockito.when(cache.get(Mockito.anyString())).then((invocation) -> {
            String key = (String) invocation.getArguments()[0];
            return "VALUE";
        });
        Mockito.when(cache.get("KEY")).thenThrow(new RuntimeException("ERROR"));
        Mockito.verify(cache.get("KEY"), Mockito.times(1));
    }
}

PowerMock can mock static methods when necessary:

PowerMockito.mockStatic(C.class);
PowerMockito.when(C.isTrue()).thenReturn(true);

Stubbing with Spring

@Primary
@Component("cache")
public class CacheStub implements Cache {
    @Override public String get(String key) { return null; }
    @Override public int setex(String key, Integer ttl, String element) { return 0; }
    @Override public int incr(String key, Integer ttl) { return 0; }
    @Override public int del(String key) { return 0; }
}

Data Builders

@Builder
@Data
public class UserVO {
    private String name;
    private int age;
    private Date birthday;
}

public class UserVOFixture {
    public static Supplier<UserVO.UserVOBuilder> DEFAULT_BUILDER = () ->
        UserVO.builder().name("test").age(11).birthday(new Date());
}

Reading Test Data from Files

public class UserVOFixture {
    public static UserVO readUser(String filename) {
        return readJsonFromResource(filename, UserVO.class);
    }
    public static <T> T readJsonFromResource(String filename, Class<T> clazz) {
        try {
            String jsonString = StreamUtils.copyToString(new ClassPathResource(filename).getInputStream(), Charset.defaultCharset());
            return JSON.parseObject(jsonString, clazz);
        } catch (IOException e) {
            return null;
        }
    }
}

Common Problems & Solutions

Too many tests in one class – split by case or scenario.

Unclear mock data – adopt naming conventions and reuse fixtures.

Complex test structure – simplify or refactor using FSC.

Tests failing mysteriously – check transaction rollbacks, data isolation, and mock configurations.

Best Practices

Focus on clear, isolated unit tests for core logic, use integration tests for service contracts, and apply fixtures to share mock data. Adopt the 3F principle (Focus, Feedback, Fix) and practice deliberately to improve testing skills.

Related links: Martin Fowler – Test Pyramid , Practical Test Pyramid
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.

JavaspringBDDMockitotest pyramidTDDfixture
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.