单元测试是验证软件中最小可测试部分正确性的自动化测试。在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
方法是否正确调用了UserRepository
的findUserById
方法。
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:测试异常处理
假设UserRepository
的findUserById
方法可能抛出一个自定义异常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
类需要按照特定的顺序调用两个依赖项PaymentService
和EmailService
。
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%的覆盖率并不总是必要的或最佳的。