【JAVA】单元测试的简单应用

单元测试是验证软件中最小可测试部分正确性的自动化测试。在Java中,单元测试通常针对类的方法或函数进行。以下是单元测试的一般写法,以及一些常用的单元测试框架。

1. 准备工作

在开始编写单元测试之前,需要确保项目中包含了单元测试框架。Java中常用的单元测试框架有:

  • JUnit: 最流行的Java单元测试框架。
  • TestNG: 功能强大的测试框架,与JUnit兼容但提供了更多特性。
  • Mockito: 主要用于模拟对象和行为的工具。

2. 创建测试类

测试类通常放在与源代码相同的包中,但位于不同的源集(source set)里。测试类的命名约定通常是在被测试类名后加上"Test"后缀。

3. 编写测试方法

测试方法通常遵循以下命名约定:test + 方法描述,必须使用public访问修饰符,并且是void返回类型。JUnit 4使用注解@Test来标识测试方法。

4. 断言(Assertions)

断言是单元测试中的核心,用于验证代码的预期输出。JUnit提供了多种断言方法,如:

  • assertEquals: 验证两个值是否相等。
  • assertTrue: 验证一个条件是否为真。
  • assertFalse: 验证一个条件是否为假。
  • assertNotNull: 验证对象不为null。
  • assertNull: 验证对象为null。

示例:使用JUnit 4编写单元测试

假设我们有以下简单的加法类:

java 复制代码
public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}

对应的单元测试可能如下:

java 复制代码
import static org.junit.Assert.assertEquals;
import org.junit.Test;

public class CalculatorTest {

    @Test
    public void testAdd() {
        Calculator calculator = new Calculator();
        assertEquals("10 + 5 must equal 15", 15, calculator.add(10, 5));
    }
    
    @Test
    public void testAddZero() {
        Calculator calculator = new Calculator();
        assertEquals("Adding zero must return the same number", 5, calculator.add(5, 0));
    }
    
    @Test(expected = IllegalArgumentException.class)
    public void testAddNegative() {
        Calculator calculator = new Calculator();
        calculator.add(-1, 5);
    }
}

使用Mockito进行单元测试

当测试涉及与其他对象交互或需要模拟外部依赖时,可以使用Mockito来创建模拟对象。

java 复制代码
import static org.mockito.Mockito.*;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

public class ServiceTest {

    @Mock
    private Dependency dependency;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testSomeBehavior() {
        Service service = new Service(dependency);
        
        // 设置模拟行为
        when(dependency.someMethod()).thenReturn("expected value");
        
        // 执行测试
        String result = service.someBehavior();
        
        // 验证方法调用
        verify(dependency).someMethod();
        
        // 断言结果
        assertEquals("Method should return expected value", "expected value", result);
    }
}

实例1:验证方法调用

假设我们有一个UserService类,它依赖于一个UserRepository接口来获取用户信息。

java 复制代码
public interface UserRepository {
    User findUserById(int id);
}

public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User getUser(int id) {
        return userRepository.findUserById(id);
    }
}

单元测试使用Mockito验证getUser方法是否正确调用了UserRepositoryfindUserById方法。

java 复制代码
import static org.mockito.Mockito.*;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    private UserService userService;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        userService = new UserService(userRepository);
    }

    @Test
    public void testGetUser() {
        User expectedUser = new User(1, "John Doe");
        when(userRepository.findUserById(anyInt())).thenReturn(expectedUser);

        User user = userService.getUser(1);

        verify(userRepository).findUserById(1);
        assertEquals("The returned user should be the expected user", expectedUser, user);
    }
}

实例2:测试异常处理

假设UserRepositoryfindUserById方法可能抛出一个自定义异常UserNotFoundException

java 复制代码
public class UserNotFoundException extends Exception {
    public UserNotFoundException(int id) {
        super("User with id " + id + " not found.");
    }
}

// UserService类保持不变

单元测试验证当用户未找到时,UserService是否正确地将异常传递出去。

java 复制代码
@Test(expected = UserNotFoundException.class)
public void testGetUserWhenUserNotFound() {
    when(userRepository.findUserById(anyInt())).thenThrow(new UserNotFoundException(1));

    userService.getUser(1);
}

实例3:使用Mockito验证顺序

假设OrderService类需要按照特定的顺序调用两个依赖项PaymentServiceEmailService

java 复制代码
public class OrderService {
    private final PaymentService paymentService;
    private final EmailService emailService;

    public OrderService(PaymentService paymentService, EmailService emailService) {
        this.paymentService = paymentService;
        this.emailService = emailService;
    }

    public void processOrder(Order order) {
        paymentService.processPayment(order);
        emailService.sendConfirmationEmail(order);
    }
}

单元测试验证processOrder方法中服务的调用顺序。

java 复制代码
import static org.mockito.Mockito.*;

@Test
public void testProcessOrder() {
    @Mock
    PaymentService paymentService;
    @Mock
    EmailService emailService;
    OrderService orderService = new OrderService(paymentService, emailService);
    Order order = new Order();

    orderService.processOrder(order);

    verify(paymentService).processPayment(order);
    verify(emailService).sendConfirmationEmail(order);
    // 验证调用顺序
    verify(paymentService, Mockito.ordering()).processPayment(order);
    verify(emailService, Mockito.ordering()).sendConfirmationEmail(order);
}

实例4:使用Spy进行部分模拟

假设我们想测试NetworkService类的真实行为,但同时要模拟一个方法。

java 复制代码
public class NetworkService {
    public String sendRequest(String url) {
        // 实际发送网络请求的代码
        return "response";
    }
}

单元测试中,我们使用Spy来模拟sendRequest方法,而其他方法则使用真实实现。

java 复制代码
@Test
public void testNetworkService() {
    NetworkService spyService = Mockito.spy(new NetworkService());
    doReturn("mocked response").when(spyService).sendRequest(anyString());

    String response = spyService.sendRequest("http://example.com");

    assertEquals("The response should be mocked", "mocked response", response);
    // 验证sendRequest是否被调用一次
    verify(spyService).sendRequest("http://example.com");
}

注意事项

  • 测试应该独立于彼此运行,不依赖于外部资源如数据库或网络服务。
  • 测试应该快速执行,以便频繁运行。
  • 测试代码应该具有可读性,易于理解。
  • 测试覆盖率是一个有用的指标,但100%的覆盖率并不总是必要的或最佳的。
相关推荐
SQL-First布道者7 分钟前
我为什么把 MyBatis 从项目中删了?
java·spring·tomcat·mybatis·mybatis plus·spring jdbc
公爵爱学习11 分钟前
无人机多点导航笔记
java·前端·笔记
萧瑟余晖41 分钟前
Java深入解析篇五十五之Unsafe 机制详解
java
kakawzw1 小时前
mybatis源码笔记1——JDBC和整体架构
java·mybatis
sunshine22 girl1 小时前
Java学习一 环境配置1 安装JDK,配置环境变量
java·开发语言·学习
秋饼1 小时前
Spring AI Session API 深度实战:从 ChatMemory 平滑迁移到事件溯源的企业级短期记忆
java·ai·技术分享·后端开发
QCodingDev1 小时前
Spring AI 2.0企业级RAG实战:引用校验、无依据拒答与知识治理怎么做?
java·人工智能·spring·ai
吴声子夜歌1 小时前
ApacheCommons——commons-math3(科学计算与线性代数)(一)
java·线性代数·算法·apache
砚底藏山河1 小时前
行情工程实战 M01|存储选型 CSVSQLiteMySQL
java·python·金融·maven
名字还没想好☜1 小时前
Java 用 LinkedHashMap 三行实现 LRU 缓存:accessOrder、removeEldestEntry 与线程安全
java·后端·安全·spring·缓存